From 36fadf19d13c3d6036a8b4d068e87f636e16fa4c Mon Sep 17 00:00:00 2001 From: Mitch Seaman Date: Thu, 23 Jan 2025 08:33:26 +0100 Subject: [PATCH 001/894] Docs: add user de-duplication description, update usage billing instructions (#99333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Irene Rodríguez --- .../enterprise-licensing/_index.md | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/sources/administration/enterprise-licensing/_index.md b/docs/sources/administration/enterprise-licensing/_index.md index 131ec8d0f79..e42696f97ec 100644 --- a/docs/sources/administration/enterprise-licensing/_index.md +++ b/docs/sources/administration/enterprise-licensing/_index.md @@ -256,14 +256,24 @@ The system creates a session when a user signs in to Grafana from a new device, When a user reaches the session limit, the fourth connection succeeds and the longest inactive session is signed out. -### Request usage billing - -You can request Grafana Labs to activate usage billing which allows an unlimited number of active users. When usage billing is enabled, Grafana does not enforce active user limits or display warning banners. Instead, you are charged for active users that exceed the limit, according to your customer contract. - -Usage billing involves a contractual agreement between you and Grafana Labs, and it is only available if Grafana Enterprise is configured to [automatically refresh its license token]({{< relref "../../setup-grafana/configure-grafana/enterprise-configuration/#auto_refresh_license" >}}). - ### Request a change to your license To increase the number of licensed users within Grafana, extend a license, or change your licensed URL, contact [Grafana support](/profile/org#support) or your Grafana Labs account team. They will update your license, which you can activate from within Grafana. For instructions about how to activate your license after it is updated, refer to [Activate an Enterprise license]({{< relref "#activate-an-enterprise-license" >}}). + +## Usage billing + +Standard Grafana Enterprise licenses include a certain number of seats that can be used, and prevent more users logging into Grafana than have been licensed. This makes sense if you prefer a predictable bill. It can however be a problem if you anticipate uneven usage patterns over time or when it's critical that no user ever be prevented from logging into Grafana due to capacity constraints. + +For those use-cases we support usage-based billing, where your license includes a certain number of included users and you are billed on a monthly basis for any excess active users during the month. + +Usage billing involves a contractual agreement between you and Grafana Labs and an update to your license, and it is only available if Grafana Enterprise version 10.0.0 or higher is configured to [automatically refresh its license token]({{< relref "../../setup-grafana/configure-grafana/enterprise-configuration/#auto_refresh_license" >}}). + +### User deduplication + +If your organization has multiple Grafana Enterprise instances with usage billing enabled, then each active user counts only once toward your license, regardless of how many instances that user signs into. Each Grafana Enterprise instance submits a hashed list of users to Grafana Labs via API every day. Each user email address or anonymous device ID is hashed using a one-way sha256 algorithm, and submitted to Grafana where the hashed users are deduplicated across instances. + +### Request usage billing + +To request usage billing, contact your Grafana Labs account team or [submit a support ticket](https://grafana.com/profile/org#support). From d740f9fc60de6a6bfa8fea60cb050e9739e14092 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:23:00 +0100 Subject: [PATCH 002/894] Authz: Simplify mapper and only check folders if its supported (#99357) * Simplify mapper and only check folders if its supported --- pkg/services/authz/mappers/rbac_mapper.go | 72 -------------------- pkg/services/authz/rbac/mapper.go | 81 +++++++++++++++++++++++ pkg/services/authz/rbac/service.go | 78 ++++++++++++++-------- pkg/services/authz/rbac/service_test.go | 19 +++--- 4 files changed, 142 insertions(+), 108 deletions(-) delete mode 100644 pkg/services/authz/mappers/rbac_mapper.go create mode 100644 pkg/services/authz/rbac/mapper.go diff --git a/pkg/services/authz/mappers/rbac_mapper.go b/pkg/services/authz/mappers/rbac_mapper.go deleted file mode 100644 index 86b97b95ef0..00000000000 --- a/pkg/services/authz/mappers/rbac_mapper.go +++ /dev/null @@ -1,72 +0,0 @@ -package mappers - -import ( - "fmt" - - "github.com/grafana/grafana/pkg/apimachinery/utils" -) - -const defaultAttribute = "uid" - -type VerbMapping map[string]string // e.g. "get" -> "read" -type ResourceVerbMapping map[string]VerbMapping // e.g. "dashboards" -> VerbToAction -type GroupResourceVerbMapping map[string]ResourceVerbMapping // e.g. "dashboard.grafana.app" -> ResourceVerbToAction - -type ResourceAttributeMapping map[string]string // e.g. "dashboards" -> "uid" -type GroupResourceAttributeMapping map[string]ResourceAttributeMapping // e.g. "dashboard.grafana.app" -> ResourceToAttribute - -type K8sRbacMapper struct { - GroupResourceVerbMapping GroupResourceVerbMapping - GroupResourceAttributeMapping GroupResourceAttributeMapping -} - -func NewK8sRbacMapper() *K8sRbacMapper { - defaultMapping := func(r string) VerbMapping { - return map[string]string{ - utils.VerbGet: fmt.Sprintf("%s:read", r), - utils.VerbList: fmt.Sprintf("%s:read", r), - utils.VerbWatch: fmt.Sprintf("%s:read", r), - utils.VerbCreate: fmt.Sprintf("%s:create", r), - utils.VerbUpdate: fmt.Sprintf("%s:write", r), - utils.VerbPatch: fmt.Sprintf("%s:write", r), - utils.VerbDelete: fmt.Sprintf("%s:delete", r), - utils.VerbDeleteCollection: fmt.Sprintf("%s:delete", r), - utils.VerbGetPermissions: fmt.Sprintf("%s.permissions:read", r), - utils.VerbSetPermissions: fmt.Sprintf("%s.permissions:write", r), - } - } - - return &K8sRbacMapper{ - GroupResourceAttributeMapping: GroupResourceAttributeMapping{}, - GroupResourceVerbMapping: GroupResourceVerbMapping{ - "dashboard.grafana.app": ResourceVerbMapping{"dashboards": defaultMapping("dashboards")}, - "folder.grafana.app": ResourceVerbMapping{"folders": defaultMapping("folders")}, - }, - } -} - -func (m *K8sRbacMapper) Action(group, resource, verb string) (string, bool) { - if resourceActions, ok := m.GroupResourceVerbMapping[group]; ok { - if actions, ok := resourceActions[resource]; ok { - if action, ok := actions[verb]; ok { - // If the action is explicitly set empty - // it means that the action is not allowed - if action == "" { - return "", false - } - return action, true - } - } - } - return "", false -} - -func (m *K8sRbacMapper) Scope(group, resource, name string) (string, bool) { - if resourceAttributes, ok := m.GroupResourceAttributeMapping[group]; ok { - if attribute, ok := resourceAttributes[resource]; ok { - return resource + ":" + attribute + ":" + name, true - } - } - - return resource + ":" + defaultAttribute + ":" + name, true -} diff --git a/pkg/services/authz/rbac/mapper.go b/pkg/services/authz/rbac/mapper.go new file mode 100644 index 00000000000..7635a193f7f --- /dev/null +++ b/pkg/services/authz/rbac/mapper.go @@ -0,0 +1,81 @@ +package rbac + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +type translation struct { + resource string + attribute string + verbMapping map[string]string + folderSupport bool +} + +func (t translation) action(verb string) (string, bool) { + action, ok := t.verbMapping[verb] + return action, ok +} + +func (t translation) scope(name string) string { + return t.resource + ":" + t.attribute + ":" + name +} + +func (t translation) prefix() string { + return t.resource + ":" + t.attribute + ":" +} + +func newResourceTranslation(resource string, attribute string, folderSupport bool) translation { + defaultMapping := func(r string) map[string]string { + return map[string]string{ + utils.VerbGet: fmt.Sprintf("%s:read", r), + utils.VerbList: fmt.Sprintf("%s:read", r), + utils.VerbWatch: fmt.Sprintf("%s:read", r), + utils.VerbCreate: fmt.Sprintf("%s:create", r), + utils.VerbUpdate: fmt.Sprintf("%s:write", r), + utils.VerbPatch: fmt.Sprintf("%s:write", r), + utils.VerbDelete: fmt.Sprintf("%s:delete", r), + utils.VerbDeleteCollection: fmt.Sprintf("%s:delete", r), + utils.VerbGetPermissions: fmt.Sprintf("%s.permissions:read", r), + utils.VerbSetPermissions: fmt.Sprintf("%s.permissions:write", r), + } + } + + return translation{ + resource: resource, + attribute: attribute, + verbMapping: defaultMapping(resource), + folderSupport: folderSupport, + } +} + +type mapper map[string]map[string]translation + +func newMapper() mapper { + return map[string]map[string]translation{ + "dashboard.grafana.app": { + "dashboards": newResourceTranslation("dashboards", "uid", true), + }, + "folder.grafana.app": { + "folders": newResourceTranslation("folders", "uid", true), + }, + "iam.grafana.app": { + "teams": newResourceTranslation("teams", "id", false), + }, + } +} + +func (m mapper) translation(group, resource string) (translation, bool) { + resources, ok := m[group] + if !ok { + return translation{}, false + } + + t, ok := resources[resource] + if !ok { + return translation{}, false + } + + return t, true +} diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index e600d43426a..b6a9bd9b863 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -23,7 +23,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/common" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/authz/mappers" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/rbac/store" "github.com/grafana/grafana/pkg/services/dashboards" @@ -44,7 +43,8 @@ type Service struct { store store.Store permissionStore store.PermissionStore identityStore legacy.LegacyIdentityStore - actionMapper *mappers.K8sRbacMapper + + mapper mapper logger log.Logger tracer tracing.Tracer @@ -72,9 +72,9 @@ func NewService( store: store.NewStore(sql, tracer), permissionStore: permissionStore, identityStore: identityStore, - actionMapper: mappers.NewK8sRbacMapper(), logger: logger, tracer: tracer, + mapper: newMapper(), idCache: localcache.New(longCacheTTL, longCleanupInterval), permCache: localcache.New(shortCacheTTL, shortCleanupInterval), teamCache: localcache.New(shortCacheTTL, shortCleanupInterval), @@ -236,14 +236,19 @@ func (s *Service) validateSubject(ctx context.Context, subject string) (string, func (s *Service) validateAction(ctx context.Context, group, resource, verb string) (string, error) { ctxLogger := s.logger.FromContext(ctx) - if group == "" || resource == "" || verb == "" { - return "", status.Error(codes.InvalidArgument, "group, resource and verb are required") - } - action, ok := s.actionMapper.Action(group, resource, verb) + + t, ok := s.mapper.translation(group, resource) if !ok { - ctxLogger.Error("could not find associated rbac action", "group", group, "resource", resource, "verb", verb) - return "", status.Error(codes.NotFound, "could not find associated rbac action") + ctxLogger.Error("unsupport resource", "group", group, "resource", resource) + return "", status.Error(codes.NotFound, "unsupported resource") } + + action, ok := t.action(verb) + if !ok { + ctxLogger.Error("unsupport verb", "group", group, "resource", resource, "verb", verb) + return "", status.Error(codes.NotFound, "unsupported verb") + } + return action, nil } @@ -437,15 +442,20 @@ func (s *Service) checkPermission(ctx context.Context, scopeMap map[string]bool, return true, nil } - scope, has := s.actionMapper.Scope(req.Group, req.Resource, req.Name) - if !has { - ctxLogger.Error("could not get attribute for resource", "resource", req.Resource) - return false, fmt.Errorf("could not get attribute for resource") + t, ok := s.mapper.translation(req.Group, req.Resource) + if !ok { + ctxLogger.Error("unsupport resource", "group", req.Group, "resource", req.Resource) + return false, status.Error(codes.NotFound, "unsupported resource") } - if scopeMap[scope] { + + if scopeMap[t.scope(req.Name)] { return true, nil } + if !t.folderSupport { + return false, nil + } + return s.checkInheritedPermissions(ctx, scopeMap, req) } @@ -556,25 +566,37 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, defer span.End() ctxLogger := s.logger.FromContext(ctx) - folderMap, err := s.buildFolderTree(ctx, req.Namespace) - if err != nil { - ctxLogger.Error("could not build folder and dashboard tree", "error", err) - return nil, err + t, ok := s.mapper.translation(req.Group, req.Resource) + if !ok { + ctxLogger.Error("unsupport resource", "group", req.Group, "resource", req.Resource) + return nil, status.Error(codes.NotFound, "unsupported resource") + } + + var folderMap map[string]FolderNode + if t.folderSupport { + var err error + folderMap, err = s.buildFolderTree(ctx, req.Namespace) + if err != nil { + ctxLogger.Error("could not build folder and dashboard tree", "error", err) + return nil, err + } } folderSet := make(map[string]struct{}, len(scopeMap)) - dashSet := make(map[string]struct{}, len(scopeMap)) + + prefix := t.prefix() + itemSet := make(map[string]struct{}, len(scopeMap)) for scope := range scopeMap { if strings.HasPrefix(scope, "folders:uid:") { - identifier := scope[len("folders:uid:"):] + identifier := strings.TrimPrefix(scope, "folders:uid:") if _, ok := folderSet[identifier]; ok { continue } folderSet[identifier] = struct{}{} getChildren(folderMap, identifier, folderSet) - } else if strings.HasPrefix(scope, "dashboards:uid:") { - identifier := scope[len("dashboards:uid:"):] - dashSet[identifier] = struct{}{} + } else { + identifier := strings.TrimPrefix(scope, prefix) + itemSet[identifier] = struct{}{} } } @@ -583,13 +605,13 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, folderList = append(folderList, folder) } - dashList := make([]string, 0, len(dashSet)) - for dash := range dashSet { - dashList = append(dashList, dash) + itemList := make([]string, 0, len(itemSet)) + for item := range itemSet { + itemList = append(itemList, item) } - span.SetAttributes(attribute.Int("num_folders", len(folderList)), attribute.Int("num_dashboards", len(dashList))) - return &authzv1.ListResponse{Folders: folderList, Items: dashList}, nil + span.SetAttributes(attribute.Int("num_folders", len(folderList)), attribute.Int("num_items", len(itemList))) + return &authzv1.ListResponse{Folders: folderList, Items: itemList}, nil } func getChildren(folderMap map[string]FolderNode, folderUID string, folderSet map[string]struct{}) { diff --git a/pkg/services/authz/rbac/service_test.go b/pkg/services/authz/rbac/service_test.go index 32c4cdf46dd..63d5f393cab 100644 --- a/pkg/services/authz/rbac/service_test.go +++ b/pkg/services/authz/rbac/service_test.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/authz/mappers" "github.com/grafana/grafana/pkg/services/authz/rbac/store" ) @@ -150,7 +149,11 @@ func TestService_checkPermission(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - s := &Service{logger: log.New("test"), actionMapper: mappers.NewK8sRbacMapper(), tracer: tracing.NewNoopTracerService()} + s := &Service{ + logger: log.NewNopLogger(), + tracer: tracing.NewNoopTracerService(), + mapper: newMapper(), + } got, err := s.checkPermission(context.Background(), getScopeMap(tc.permissions), &tc.check) require.NoError(t, err) assert.Equal(t, tc.expected, got) @@ -366,9 +369,9 @@ func TestService_getUserPermissions(t *testing.T) { store: store, permissionStore: store, identityStore: &fakeIdentityStore{teams: []int64{1, 2}}, - actionMapper: mappers.NewK8sRbacMapper(), - logger: log.New("test"), + logger: log.NewNopLogger(), tracer: tracing.NewNoopTracerService(), + mapper: newMapper(), idCache: localcache.New(longCacheTTL, longCleanupInterval), permCache: cacheService, sf: new(singleflight.Group), @@ -649,10 +652,10 @@ func TestService_listPermission(t *testing.T) { folderCache.Set(folderCacheKey("default"), tc.folderTree, 0) } s := &Service{ - logger: log.New("test"), - actionMapper: mappers.NewK8sRbacMapper(), - folderCache: folderCache, - tracer: tracing.NewNoopTracerService(), + logger: log.New("test"), + folderCache: folderCache, + mapper: newMapper(), + tracer: tracing.NewNoopTracerService(), } tc.list.Namespace = claims.NamespaceInfo{Value: "default", OrgID: 1} got, err := s.listPermission(context.Background(), getScopeMap(tc.permissions), &tc.list) From c862aa4d68b5ab9030a3ccfa0df94a84ba323748 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Thu, 23 Jan 2025 09:46:25 +0100 Subject: [PATCH 003/894] LibraryPanel: Fallback to panel title if library panel title is not set (#99377) --- .../dashboard-scene/inspect/InspectJsonTab.test.tsx | 2 +- .../dashboard-scene/panel-edit/PanelEditor.test.ts | 4 +--- .../dashboard-scene/panel-edit/PanelOptions.test.tsx | 1 - .../scene/AddLibraryPanelDrawer.test.tsx | 2 +- .../scene/DashboardDatasourceBehaviour.test.tsx | 3 --- .../dashboard-scene/scene/DashboardScene.test.tsx | 10 +++++----- .../scene/LibraryPanelBehavior.test.tsx | 2 +- .../dashboard-scene/scene/LibraryPanelBehavior.tsx | 6 ++---- .../serialization/transformSceneToSaveModel.test.ts | 6 ++---- .../serialization/transformSceneToSaveModel.ts | 2 +- .../utils/PanelModelCompatibilityWrapper.test.ts | 1 - 11 files changed, 14 insertions(+), 25 deletions(-) diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index 7d93518167e..582e0855008 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -223,7 +223,7 @@ async function buildTestSceneWithLibraryPanel() { title: 'Panel A', pluginId: 'table', key: 'panel-12', - $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' })], + $behaviors: [new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: '111' })], titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], $data: new SceneDataTransformer({ transformations: [ diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index fa309c8bd21..f204eae7138 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -200,7 +200,6 @@ describe('PanelEditor', () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: true, - title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, @@ -239,7 +238,7 @@ describe('PanelEditor', () => { // Wait for mock api to return and update the library panel expect(libPanelBehavior.state._loadedPanel?.version).toBe(2); expect(libPanelBehavior.state.name).toBe('changed name'); - expect(libPanelBehavior.state.title).toBe('changed title'); + expect(panel.state.title).toBe('changed title'); expect((gridItem.state.body as VizPanel).state.title).toBe('changed title'); }); @@ -258,7 +257,6 @@ describe('PanelEditor', () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: true, - title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx index 264c053de17..3f54a519b70 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx @@ -177,7 +177,6 @@ describe('PanelOptions', () => { const libraryPanel = new LibraryPanelBehavior({ isLoaded: true, - title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx index f298e7b19f9..e8a0054a731 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx @@ -96,7 +96,7 @@ describe('AddLibraryPanelWidget', () => { title: 'Panel Title', pluginId: 'table', key: 'panel-1', - $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: 'uid' })], }); addLibPanelDrawer = new AddLibraryPanelDrawer({ panelToReplaceRef: libPanel.getRef() }); diff --git a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx index 393f3cfaea7..1b183577b1f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx @@ -408,7 +408,6 @@ describe('DashboardDatasourceBehaviour', () => { it('should re-run queries when library panel re-runs query', async () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: false, - title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, @@ -469,7 +468,6 @@ describe('DashboardDatasourceBehaviour', () => { jest.spyOn(console, 'error').mockImplementation(); const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: false, - title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, @@ -519,7 +517,6 @@ describe('DashboardDatasourceBehaviour', () => { // Simulate library panel being loaded libPanelBehavior.setState({ isLoaded: true, - title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index c0ac4ea98c7..149cba2953a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -489,7 +489,7 @@ describe('DashboardScene', () => { title: 'Library Panel', pluginId: 'table', key: 'panel-4', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }); scene.copyPanel(libVizPanel); @@ -544,7 +544,7 @@ describe('DashboardScene', () => { title: 'Library Panel', pluginId: 'table', key: 'panel-4', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }), }) ); @@ -563,7 +563,7 @@ describe('DashboardScene', () => { const libPanel = new VizPanel({ title: 'Panel B', pluginId: 'table', - $behaviors: [new LibraryPanelBehavior({ title: 'title', name: 'lib panel', uid: 'abc', isLoaded: true })], + $behaviors: [new LibraryPanelBehavior({ name: 'lib panel', uid: 'abc', isLoaded: true })], }); const scene = buildTestScene({ @@ -928,7 +928,7 @@ function buildTestScene(overrides?: Partial) { title: 'Library Panel', pluginId: 'table', key: 'panel-5', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }), }), ], @@ -946,7 +946,7 @@ function buildTestScene(overrides?: Partial) { title: 'Library Panel', pluginId: 'table', key: 'panel-6', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }), }), ], diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx index 02a363d5d3a..b6eb6872566 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx @@ -167,7 +167,7 @@ describe('LibraryPanelBehavior', () => { }); async function buildTestSceneWithLibraryPanel() { - const behavior = new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' }); + const behavior = new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: '111' }); const vizPanel = new VizPanel({ title: 'Panel A', diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx index 423cc2c34ee..44a1c0de5a8 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx @@ -17,8 +17,6 @@ import { AngularDeprecation } from './angular/AngularDeprecation'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; export interface LibraryPanelBehaviorState extends SceneObjectState { - // Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it. - title?: string; uid: string; name: string; isLoaded?: boolean; @@ -66,7 +64,7 @@ export class LibraryPanelBehavior extends SceneObjectBase { $behaviors: [ new LibraryPanelBehavior({ name: 'Some lib panel panel', - title: 'A panel', uid: 'lib-panel-uid', }), ], @@ -399,7 +398,7 @@ describe('transformSceneToSaveModel', () => { x: 0, y: 0, }); - expect(result.title).toBe('A panel'); + expect(result.title).toBe('Panel blahh blah'); expect(result.transformations).toBeUndefined(); expect(result.fieldConfig).toBeUndefined(); expect(result.options).toBeUndefined(); @@ -851,7 +850,6 @@ describe('transformSceneToSaveModel', () => { $behaviors: [ new LibraryPanelBehavior({ name: 'Some lib panel panel', - title: 'A panel', uid: 'lib-panel-uid', }), ], @@ -865,7 +863,7 @@ describe('transformSceneToSaveModel', () => { expect(result[0]).toMatchObject({ id: 4, - title: 'A panel', + title: 'Panel blahh blah', libraryPanel: { name: 'Some lib panel panel', uid: 'lib-panel-uid', diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index e14e778650f..235f5038dc3 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -186,7 +186,7 @@ export function vizPanelToPanel( panel = { id: getPanelIdForVizPanel(vizPanel), - title: libPanel!.state.title, + title: vizPanel.state.title, gridPos: gridPos, libraryPanel: { name: libPanel!.state.name, diff --git a/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts b/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts index e6e44c7a583..02dcc897936 100644 --- a/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts +++ b/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts @@ -17,7 +17,6 @@ describe('PanelModelCompatibilityWrapper', () => { const libPanel = new LibraryPanelBehavior({ uid: 'a', name: 'aa', - title: 'a', }); panel.setState({ From db3dcd4f7daed8ecaebb12c4194b1e54ec7cae96 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:50:25 +0100 Subject: [PATCH 004/894] Update dependency knip to v5.43.1 (#99381) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0687b253263..e681945b881 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20560,8 +20560,8 @@ __metadata: linkType: hard "knip@npm:^5.10.0": - version: 5.42.2 - resolution: "knip@npm:5.42.2" + version: 5.43.1 + resolution: "knip@npm:5.43.1" dependencies: "@nodelib/fs.walk": "npm:3.0.1" "@snyk/github-codeowners": "npm:1.1.0" @@ -20585,7 +20585,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10/1e540ad66e8e5cd2dfceb0c333ca46446300f4e40a51599ca6cd8de705bf0f928332ca108de28306ae7b54cc8fc1f66667135193baf32d4ccd30560797825935 + checksum: 10/068e4145371cf3a4434d07a206eddf8f1d509541482d76252440484562f0b989c11c3efb9c4083d8b5854a90758d3bbcc4a228fe935f6e90ecc9ef2c9f9da8a7 languageName: node linkType: hard From 25447ea93a36e23c063ab40c54049932d768e75a Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Thu, 23 Jan 2025 10:38:43 +0100 Subject: [PATCH 005/894] Fix: Update yarn.lock so CI can pass again (#99416) Chore: Update yarn.lock `yarn install && yarn build` --- yarn.lock | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/yarn.lock b/yarn.lock index e681945b881..2cc6e403916 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3491,36 +3491,6 @@ __metadata: languageName: node linkType: hard -"@grafana/plugin-ui@npm:^0.9.6": - version: 0.9.6 - resolution: "@grafana/plugin-ui@npm:0.9.6" - dependencies: - "@hello-pangea/dnd": "npm:^17.0.0" - lodash: "npm:^4.17.21" - prismjs: "npm:^1.29.0" - prompts: "npm:^2.4.2" - rc-cascader: "npm:1.0.1" - react-awesome-query-builder: "npm:^5.3.1" - react-popper-tooltip: "npm:^4.4.2" - react-use: "npm:17.3.1" - react-virtualized-auto-sizer: "npm:^1.0.6" - sql-formatter-plus: "npm:^1.3.6" - uuid: "npm:^8.3.2" - peerDependencies: - "@changesets/cli": ">=2.x" - "@grafana/data": ^10.4.0 || ^11.0.0 - "@grafana/e2e-selectors": ^10.4.0 || ^11.0.0 - "@grafana/runtime": ^10.4.0 || ^11.0.0 - "@grafana/ui": ^10.4.0 || ^11.0.0 - react: ^18.2.0 - react-dom: ^18.2.0 - rxjs: ^7.8.1 - bin: - changeset-improved: dist/utils/changeset/index.js - checksum: 10/260317fd20becd3bed4b7beb758e855075c707952622169e1cd1c8624ff40e2d297b8b92584abb55a012cecbf81f1712372bc5420c3b93cfb0079504e649f858 - languageName: node - linkType: hard - "@grafana/prometheus@workspace:*, @grafana/prometheus@workspace:packages/grafana-prometheus": version: 0.0.0-use.local resolution: "@grafana/prometheus@workspace:packages/grafana-prometheus" From d476b65d344176ef5e3ee73a15266c810f7f7125 Mon Sep 17 00:00:00 2001 From: Nick Botticelli Date: Thu, 23 Jan 2025 02:46:32 -0700 Subject: [PATCH 006/894] Docs: Fix chmod command for LetsEncrypt cert/key file access (#98354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jack Baldry Co-authored-by: Irene Rodríguez --- docs/sources/setup-grafana/set-up-https.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/setup-grafana/set-up-https.md b/docs/sources/setup-grafana/set-up-https.md index 20ce98bb2a0..4c9d39d8085 100644 --- a/docs/sources/setup-grafana/set-up-https.md +++ b/docs/sources/setup-grafana/set-up-https.md @@ -209,7 +209,7 @@ To adjust permissions, perform the following steps: $ sudo chgrp -R grafana /etc/letsencrypt/* $ sudo chmod -R g+rx /etc/letsencrypt/* $ sudo chgrp -R grafana /etc/grafana/grafana.crt /etc/grafana/grafana.key - $ sudo chmod 400 /etc/grafana/grafana.crt /etc/grafana/grafana.key + $ sudo chmod 440 /etc/grafana/grafana.crt /etc/grafana/grafana.key ``` 1. Run the following command to verify that the `grafana` group can read the symlinks: From 4fb7b47971dc19ead5152e40e4fdf0f9ef5ba985 Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Thu, 23 Jan 2025 11:02:23 +0100 Subject: [PATCH 007/894] Trivy: Document Vulnerability Observability (#99414) We use Vulnerability Observability for Docker images. The current comments say we simply don't scan them at all, so let's make it clear for future readers that we do, in fact, scan Docker images, too. --- .github/workflows/trivy-scan.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index b1346236770..25104af8f29 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -27,11 +27,14 @@ jobs: trivy fs --no-progress --download-db-only --db-repository public.ecr.aws/aquasecurity/trivy-db - name: Run Trivy vulnerability scanner (table output) # Use the trivy binary rather than the aquasecurity/trivy-action action - # to avoid a few bugs - # scan the filesystem, rather than building a Docker image prior - the - # downside is we won't catch dependencies that are only installed in the - # image, but the upside is we'll only catch vulnerabilities that are - # explicitly in the our dependencies + # to avoid a few bugs. + # + # We scan the file system rather than building the Docker image to only scan + # our direct dependencies. The Docker images are still scanned by + # Vulnerability Observability: + # - OSS: https://ops.grafana-ops.net/a/grafana-vulnerabilityobs-app/projects/sources/1 + # - Enterprise: https://ops.grafana-ops.net/a/grafana-vulnerabilityobs-app/projects/sources/12 + # (If these links are outdated, just go to the list and find the images manually.) run: | trivy fs \ --scanners vuln \ From 723fa7ddf92911b1eacaf9dbeebe12db08073089 Mon Sep 17 00:00:00 2001 From: Ieva Date: Thu, 23 Jan 2025 10:21:43 +0000 Subject: [PATCH 008/894] MT AuthZ: Resolve renderer permissions in MT authZ service (#99362) * resolve renderer permissions in MT authZ service * also include DS read perms * fix tests and linting --- pkg/services/authn/clients/render.go | 6 +++- pkg/services/authn/clients/render_test.go | 28 +++++++++++++++-- pkg/services/authz/rbac/service.go | 37 ++++++++++++++++++----- pkg/services/authz/rbac/service_test.go | 2 +- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/pkg/services/authn/clients/render.go b/pkg/services/authn/clients/render.go index d5ecf60fc41..9e77053b893 100644 --- a/pkg/services/authn/clients/render.go +++ b/pkg/services/authn/clients/render.go @@ -41,9 +41,13 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide } if renderUsr.UserID <= 0 { + identityType := claims.TypeAnonymous + if org.RoleType(renderUsr.OrgRole) == org.RoleAdmin { + identityType = claims.TypeRenderService + } return &authn.Identity{ ID: "0", - Type: claims.TypeRenderService, + Type: identityType, OrgID: renderUsr.OrgID, OrgRoles: map[int64]org.RoleType{renderUsr.OrgID: org.RoleType(renderUsr.OrgRole)}, ClientParams: authn.ClientParams{SyncPermissions: true}, diff --git a/pkg/services/authn/clients/render_test.go b/pkg/services/authn/clients/render_test.go index 994edbd7fdd..a84c48ee68e 100644 --- a/pkg/services/authn/clients/render_test.go +++ b/pkg/services/authn/clients/render_test.go @@ -29,7 +29,29 @@ func TestRender_Authenticate(t *testing.T) { tests := []TestCase{ { - desc: "expect valid render key to return render user identity", + desc: "expect valid render key to return anonymous user identity for org role Viewer", + renderKey: "123", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{"Cookie": {"renderKey=123"}}, + }, + }, + expectedIdentity: &authn.Identity{ + ID: "0", + Type: claims.TypeAnonymous, + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + AuthenticatedBy: login.RenderModule, + ClientParams: authn.ClientParams{SyncPermissions: true}, + }, + expectedRenderUsr: &rendering.RenderUser{ + OrgID: 1, + UserID: 0, + OrgRole: "Viewer", + }, + }, + { + desc: "expect valid render key to return render user identity for org role Admin", renderKey: "123", req: &authn.Request{ HTTPRequest: &http.Request{ @@ -40,14 +62,14 @@ func TestRender_Authenticate(t *testing.T) { ID: "0", Type: claims.TypeRenderService, OrgID: 1, - OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, AuthenticatedBy: login.RenderModule, ClientParams: authn.ClientParams{SyncPermissions: true}, }, expectedRenderUsr: &rendering.RenderUser{ OrgID: 1, UserID: 0, - OrgRole: "Viewer", + OrgRole: "Admin", }, }, { diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index b6a9bd9b863..9ccd1427035 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -98,7 +98,7 @@ func (s *Service) Check(ctx context.Context, req *authzv1.CheckRequest) (*authzv } ctx = request.WithNamespace(ctx, req.GetNamespace()) - permissions, err := s.getUserPermissions(ctx, checkReq.Namespace, checkReq.IdentityType, checkReq.UserUID, checkReq.Action) + permissions, err := s.getIdentityPermissions(ctx, checkReq.Namespace, checkReq.IdentityType, checkReq.UserUID, checkReq.Action) if err != nil { ctxLogger.Error("could not get user permissions", "subject", req.GetSubject(), "error", err) return deny, err @@ -124,7 +124,7 @@ func (s *Service) List(ctx context.Context, req *authzv1.ListRequest) (*authzv1. } ctx = request.WithNamespace(ctx, req.GetNamespace()) - permissions, err := s.getUserPermissions(ctx, listReq.Namespace, listReq.IdentityType, listReq.UserUID, listReq.Action) + permissions, err := s.getIdentityPermissions(ctx, listReq.Namespace, listReq.IdentityType, listReq.UserUID, listReq.Action) if err != nil { ctxLogger.Error("could not get user permissions", "subject", req.GetSubject(), "error", err) return nil, err @@ -226,8 +226,8 @@ func (s *Service) validateSubject(ctx context.Context, subject string) (string, if err != nil { return "", "", err } - // Permission check currently only checks user, anonymous user and service account permissions - if !(identityType == claims.TypeUser || identityType == claims.TypeServiceAccount || identityType == claims.TypeAnonymous) { + // Permission check currently only checks user, anonymous user, service account and renderer permissions + if !(identityType == claims.TypeUser || identityType == claims.TypeServiceAccount || identityType == claims.TypeAnonymous || identityType == claims.TypeRenderService) { ctxLogger.Error("unsupported identity type", "type", identityType) return "", "", status.Error(codes.PermissionDenied, "unsupported identity type") } @@ -252,8 +252,8 @@ func (s *Service) validateAction(ctx context.Context, group, resource, verb stri return action, nil } -func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInfo, idType claims.IdentityType, userID, action string) (map[string]bool, error) { - ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserPermissions") +func (s *Service) getIdentityPermissions(ctx context.Context, ns claims.NamespaceInfo, idType claims.IdentityType, userID, action string) (map[string]bool, error) { + ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getIdentityPermissions") defer span.End() // When checking folder creation permissions, also check edit and admin action sets for folder, as the scoped folder create actions aren't stored in the DB separately @@ -263,9 +263,21 @@ func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInf actionSets = append(actionSets, "folders:admin") } - if idType == claims.TypeAnonymous { + switch idType { + case claims.TypeAnonymous: return s.getAnonymousPermissions(ctx, ns, action, actionSets) + case claims.TypeRenderService: + return s.getRendererPermissions(ctx, action) + case claims.TypeUser, claims.TypeServiceAccount: + return s.getUserPermissions(ctx, ns, userID, action, actionSets) + default: + return nil, fmt.Errorf("unsupported identity type: %s", idType) } +} + +func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInfo, userID, action string, actionSets []string) (map[string]bool, error) { + ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserPermissions") + defer span.End() userIdentifiers, err := s.GetUserIdentifiers(ctx, ns, userID) if err != nil { @@ -342,6 +354,17 @@ func (s *Service) getAnonymousPermissions(ctx context.Context, ns claims.Namespa return res.(map[string]bool), nil } +// Renderer is granted permissions to read all dashboards and folders, and no other permissions +func (s *Service) getRendererPermissions(ctx context.Context, action string) (map[string]bool, error) { + _, span := s.tracer.Start(ctx, "authz_direct_db.service.getRendererPermissions") + defer span.End() + + if action == "dashboards:read" || action == "folders:read" || action == "datasources:read" { + return map[string]bool{"*": true}, nil + } + return map[string]bool{}, nil +} + func (s *Service) GetUserIdentifiers(ctx context.Context, ns claims.NamespaceInfo, userUID string) (*store.UserIdentifiers, error) { uidCacheKey := userIdentifierCacheKey(ns.Value, userUID) if cached, ok := s.idCache.Get(uidCacheKey); ok { diff --git a/pkg/services/authz/rbac/service_test.go b/pkg/services/authz/rbac/service_test.go index 63d5f393cab..f4f301008e3 100644 --- a/pkg/services/authz/rbac/service_test.go +++ b/pkg/services/authz/rbac/service_test.go @@ -379,7 +379,7 @@ func TestService_getUserPermissions(t *testing.T) { teamCache: localcache.New(shortCacheTTL, shortCleanupInterval), } - perms, err := s.getUserPermissions(ctx, ns, claims.TypeUser, userID.UID, action) + perms, err := s.getIdentityPermissions(ctx, ns, claims.TypeUser, userID.UID, action) require.NoError(t, err) require.Len(t, perms, len(tc.expectedPerms)) for _, perm := range tc.permissions { From fc7db91bf1eda86b29a56912be4762d665f193d0 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Thu, 23 Jan 2025 10:26:37 +0000 Subject: [PATCH 009/894] Use tags for `documentation-ci` actions (#99419) --- .github/workflows/documentation-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/documentation-ci.yml b/.github/workflows/documentation-ci.yml index 0f041220ec9..4c688968a09 100644 --- a/.github/workflows/documentation-ci.yml +++ b/.github/workflows/documentation-ci.yml @@ -10,10 +10,10 @@ jobs: container: image: grafana/vale:latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@v4 with: persist-credentials: false - - uses: grafana/writers-toolkit/vale-action@13205961f20ad13843505a9b84fdf032f911a3f4 # vale-action/v1.1.0 + - uses: grafana/writers-toolkit/vale-action@vale-action/v1 with: filter: '.Name in ["Grafana.WordList", "Grafana.Spelling", "Grafana.ProductPossessives"]' token: ${{ secrets.GITHUB_TOKEN }} From c4c934e0bd24e8ec5c0d8b38a4cd3f97ca9cf6c0 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 23 Jan 2025 13:34:48 +0300 Subject: [PATCH 010/894] SQL/Storage: Remove SkipDataMigration flag (#99404) --- pkg/storage/unified/sql/backend.go | 27 +++++++++++-------------- pkg/storage/unified/sql/backend_test.go | 11 ++++------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 2ece455f44c..d56cd87b2e1 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -34,10 +34,9 @@ type Backend interface { } type BackendOptions struct { - DBProvider db.DBProvider - Tracer trace.Tracer - PollingInterval time.Duration - SkipDataMigration bool + DBProvider db.DBProvider + Tracer trace.Tracer + PollingInterval time.Duration } func NewBackend(opts BackendOptions) (Backend, error) { @@ -54,13 +53,12 @@ func NewBackend(opts BackendOptions) (Backend, error) { pollingInterval = defaultPollingInterval } return &backend{ - done: ctx.Done(), - cancel: cancel, - log: log.New("sql-resource-server"), - tracer: opts.Tracer, - dbProvider: opts.DBProvider, - pollingInterval: pollingInterval, - skipDataMigration: opts.SkipDataMigration, + done: ctx.Done(), + cancel: cancel, + log: log.New("sql-resource-server"), + tracer: opts.Tracer, + dbProvider: opts.DBProvider, + pollingInterval: pollingInterval, }, nil } @@ -76,10 +74,9 @@ type backend struct { tracer trace.Tracer // database - dbProvider db.DBProvider - db db.DB - dialect sqltemplate.Dialect - skipDataMigration bool + dbProvider db.DBProvider + db db.DB + dialect sqltemplate.Dialect // watch streaming //stream chan *resource.WatchEvent diff --git a/pkg/storage/unified/sql/backend_test.go b/pkg/storage/unified/sql/backend_test.go index 25cfdbcd440..1703c34384b 100644 --- a/pkg/storage/unified/sql/backend_test.go +++ b/pkg/storage/unified/sql/backend_test.go @@ -62,10 +62,7 @@ func setupBackendTest(t *testing.T) (testBackend, context.Context) { ctx := testutil.NewDefaultTestContext(t) dbp := test.NewDBProviderMatchWords(t) - b, err := NewBackend(BackendOptions{ - DBProvider: dbp, - SkipDataMigration: true, // Calling migrations makes startup SQL calls (avoid the mock) - }) + b, err := NewBackend(BackendOptions{DBProvider: dbp}) require.NoError(t, err) require.NotNil(t, b) @@ -112,7 +109,7 @@ func TestBackend_Init(t *testing.T) { ctx := testutil.NewDefaultTestContext(t) dbp := test.NewDBProviderWithPing(t) - b, err := NewBackend(BackendOptions{DBProvider: dbp, SkipDataMigration: true}) + b, err := NewBackend(BackendOptions{DBProvider: dbp}) require.NoError(t, err) require.NotNil(t, b) @@ -169,7 +166,7 @@ func TestBackend_Init(t *testing.T) { ctx := testutil.NewDefaultTestContext(t) dbp := test.NewDBProviderWithPing(t) - b, err := NewBackend(BackendOptions{DBProvider: dbp, SkipDataMigration: true}) + b, err := NewBackend(BackendOptions{DBProvider: dbp}) require.NoError(t, err) require.NotNil(t, dbp.DB) @@ -185,7 +182,7 @@ func TestBackend_IsHealthy(t *testing.T) { ctx := testutil.NewDefaultTestContext(t) dbp := test.NewDBProviderWithPing(t) - b, err := NewBackend(BackendOptions{DBProvider: dbp, SkipDataMigration: true}) + b, err := NewBackend(BackendOptions{DBProvider: dbp}) require.NoError(t, err) require.NotNil(t, dbp.DB) From 750027d0a7ac86b7ec937a9c1318278ae17683b2 Mon Sep 17 00:00:00 2001 From: Anton Engelhardt <106314688+antonengelhardt@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:03:56 +0100 Subject: [PATCH 011/894] fix(docs): add team call returns uid (#99425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Irene Rodríguez --- docs/sources/developers/http_api/team.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/developers/http_api/team.md b/docs/sources/developers/http_api/team.md index f7eec3d6260..e2c4cc654b4 100644 --- a/docs/sources/developers/http_api/team.md +++ b/docs/sources/developers/http_api/team.md @@ -179,7 +179,7 @@ Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt HTTP/1.1 200 Content-Type: application/json -{"message":"Team created","teamId":2} +{"message":"Team created","teamId":2,"uid":"ceaulqadfoav4e"} ``` Status Codes: From 83bbdbf8b6ea8a884cf25065cdb583efd35c1c44 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Thu, 23 Jan 2025 12:57:45 +0100 Subject: [PATCH 012/894] LibraryPanel: Use id and title from panel model (#99281) --- .../dashboard/v2alpha0/dashboard.schema.cue | 14 ++++++++++- .../src/schema/dashboard/v2alpha0/examples.ts | 12 ++++++--- .../schema/dashboard/v2alpha0/types.gen.ts | 25 ++++++++++++++++--- .../transformSaveModelSchemaV2ToScene.test.ts | 6 ++--- .../transformSaveModelSchemaV2ToScene.ts | 13 +++++++--- .../transformSceneToSaveModelSchemaV2.ts | 20 +++++++-------- .../dashboard-scene/v2schema/test-helpers.ts | 7 +++--- .../api/ResponseTransformers.test.ts | 18 +++++++++---- .../dashboard/api/ResponseTransformers.ts | 22 ++++++++-------- 9 files changed, 92 insertions(+), 45 deletions(-) diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index d153d6beeb0..4b52f6757b4 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -64,9 +64,21 @@ LibraryPanelKind: { } LibraryPanelSpec: { + // Panel ID for the library panel in the dashboard + id: number + // Title for the library panel in the dashboard + title: string + + libraryPanel: LibraryPanelRef +} + +// A library panel is a reusable panel that you can use in any dashboard. +// When you make a change to a library panel, that change propagates to all instances of where the panel is used. +// Library panels streamline reuse of panels across multiple dashboards. +LibraryPanelRef: { // Library panel name name: string - // Library panel UID + // Library panel uid uid: string } diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts index c031ed9e6ef..b96fbd6f362 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts @@ -181,11 +181,15 @@ export const handyTestingSchema: DashboardV2Spec = { }, }, }, - 'library-panel-1': { + 'panel-2': { kind: 'LibraryPanel', spec: { - uid: 'library-panel-1', - name: 'Library Panel', + id: 2, + title: 'Test Library Panel', + libraryPanel: { + uid: 'uid-for-library-panel', + name: 'Library Panel', + }, }, }, }, @@ -216,7 +220,7 @@ export const handyTestingSchema: DashboardV2Spec = { spec: { element: { kind: 'ElementReference', - name: 'library-panel-1', + name: 'panel-2', }, height: 100, width: 200, diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index 8d960e9affb..bd6dba3279d 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -70,13 +70,30 @@ export const defaultLibraryPanelKind = (): LibraryPanelKind => ({ }); export interface LibraryPanelSpec { - // Library panel name - name: string; - // Library panel UID - uid: string; + // Panel ID for the library panel in the dashboard + id: number; + // Title for the library panel in the dashboard + title: string; + libraryPanel: LibraryPanelRef; } export const defaultLibraryPanelSpec = (): LibraryPanelSpec => ({ + id: 0, + title: "", + libraryPanel: defaultLibraryPanelRef(), +}); + +// A library panel is a reusable panel that you can use in any dashboard. +// When you make a change to a library panel, that change propagates to all instances of where the panel is used. +// Library panels streamline reuse of panels across multiple dashboards. +export interface LibraryPanelRef { + // Library panel name + name: string; + // Library panel uid + uid: string; +} + +export const defaultLibraryPanelRef = (): LibraryPanelRef => ({ name: "", uid: "", }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 9352284b4fc..71f622c7eba 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -239,14 +239,14 @@ describe('transformSaveModelSchemaV2ToScene', () => { validateVizPanel(vizPanel, dash); // Library Panel - const libraryPanel = getLibraryPanelElement(dash, 'library-panel-1')!; - expect(layout.state.grid.state.children[1].state.key).toBe(`grid-item-${libraryPanel.spec.uid}`); + const libraryPanel = getLibraryPanelElement(dash, 'panel-2')!; + expect(layout.state.grid.state.children[1].state.key).toBe(`grid-item-${libraryPanel.spec.id}`); const libraryGridLayoutItemSpec = dash.layout.spec.items[1].spec; expect(layout.state.grid.state.children[1].state.width).toBe(libraryGridLayoutItemSpec.width); expect(layout.state.grid.state.children[1].state.height).toBe(libraryGridLayoutItemSpec.height); expect(layout.state.grid.state.children[1].state.x).toBe(libraryGridLayoutItemSpec.x); expect(layout.state.grid.state.children[1].state.y).toBe(libraryGridLayoutItemSpec.y); - const vizLibraryPanel = vizPanels.find((p) => p.state.key === 'library-panel-1')!; + const vizLibraryPanel = vizPanels.find((p) => p.state.key === 'panel-2')!; validateVizPanel(vizLibraryPanel, dash); // Transformations diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 6c9af20e122..65f333c88e8 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -259,7 +259,7 @@ function createSceneGridLayoutForItems(dashboard: DashboardV2Spec): SceneGridIte const libraryPanel = buildLibraryPanel(panel); return new DashboardGridItem({ - key: `grid-item-${panel.spec.uid}`, + key: `grid-item-${panel.spec.id}`, x: element.spec.x, y: element.spec.y, width: element.spec.width, @@ -293,12 +293,17 @@ function buildLibraryPanel(panel: LibraryPanelKind): VizPanel { titleItems.push(new PanelNotices()); const vizPanelState: VizPanelState = { - key: panel.spec.uid, + key: getVizPanelKeyForPanelId(panel.spec.id), titleItems, - $behaviors: [new LibraryPanelBehavior({ uid: panel.spec.uid, name: panel.spec.name })], + $behaviors: [ + new LibraryPanelBehavior({ + uid: panel.spec.libraryPanel.uid, + name: panel.spec.libraryPanel.name, + }), + ], extendPanelContext: setDashboardPanelContext, pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID, - title: '', + title: panel.spec.title, options: {}, fieldConfig: { defaults: {}, diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index bee0227e61e..bc5b020c539 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -248,8 +248,12 @@ function getElements(state: DashboardSceneState) { const elementSpec: LibraryPanelKind = { kind: 'LibraryPanel', spec: { - name: behavior.state.name, - uid: behavior.state.uid, + id: getPanelIdForVizPanel(vizPanel), + title: vizPanel.state.title, + libraryPanel: { + uid: behavior.state.uid, + name: behavior.state.name, + }, }, }; return elementSpec; @@ -432,14 +436,10 @@ function getVizPanelQueryOptions(vizPanel: VizPanel): QueryOptionsSpec { } function createElements(panels: Element[]): Record { - const elements: Record = {}; - - for (const panel of panels) { - const key = panel.kind === 'Panel' ? getVizPanelKeyForPanelId(panel.spec.id) : panel.spec.uid; - elements[key] = panel; - } - - return elements; + return panels.reduce>((elements, panel) => { + elements[getVizPanelKeyForPanelId(panel.spec.id)] = panel; + return elements; + }, {}); } function repeaterToLayoutItems(repeater: DashboardGridItem, isSnapshot = false): GridLayoutItemKind[] { diff --git a/public/app/features/dashboard-scene/v2schema/test-helpers.ts b/public/app/features/dashboard-scene/v2schema/test-helpers.ts index f37f8802405..9c37f1df5d5 100644 --- a/public/app/features/dashboard-scene/v2schema/test-helpers.ts +++ b/public/app/features/dashboard-scene/v2schema/test-helpers.ts @@ -94,9 +94,10 @@ export function validateVizPanel(vizPanel: VizPanel, dash: DashboardV2Spec) { expect(vizPanelLinks.state.rawLinks).toEqual(panel.spec.links); expect(queryRunner.state.dataLayerFilter?.panelId).toBe(panel.spec.id); } else if (panel.kind === 'LibraryPanel') { - expect(getLibraryPanelBehavior(vizPanel)?.state.name).toBe(panel.spec.name); - expect(getLibraryPanelBehavior(vizPanel)?.state.uid).toBe(panel.spec.uid); - + expect(getLibraryPanelBehavior(vizPanel)?.state.name).toBe(panel.spec.libraryPanel.name); + expect(getLibraryPanelBehavior(vizPanel)?.state.uid).toBe(panel.spec.libraryPanel.uid); + expect(getPanelIdForVizPanel(vizPanel)).toBe(panel.spec.id); + expect(vizPanel.state.title).toBe(panel.spec.title); expect(vizPanel.state.pluginId).toBe(LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID); } else { throw new Error('vizPanel is not a valid element kind'); diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index 8100e0aa5b4..1209f884b47 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -314,6 +314,7 @@ describe('ResponseTransformers', () => { { id: 2, type: 'table', + title: 'Just a shared table', libraryPanel: { uid: 'library-panel-table', name: 'Table Panel as Library Panel', @@ -458,18 +459,22 @@ describe('ResponseTransformers', () => { expect(spec.layout.spec.items[1].spec).toEqual({ element: { kind: 'ElementReference', - name: 'library-panel-table', + name: '2', }, x: 0, y: 8, width: 12, height: 8, }); - expect(spec.elements['library-panel-table']).toEqual({ + expect(spec.elements['2']).toEqual({ kind: 'LibraryPanel', spec: { - uid: 'library-panel-table', - name: 'Table Panel as Library Panel', + libraryPanel: { + uid: 'library-panel-table', + name: 'Table Panel as Library Panel', + }, + id: 2, + title: 'Just a shared table', }, }); @@ -633,7 +638,10 @@ describe('ResponseTransformers', () => { expect(panelV2.kind).toBe('Panel'); validatePanel(dashboard.panels![0], panelV2, dashboardV2.spec.layout, panelKey); // library panel - expect(dashboard.panels![1].libraryPanel).toEqual(dashboardV2.spec.elements['library-panel-1'].spec); + expect(dashboard.panels![1].libraryPanel).toEqual({ + uid: 'uid-for-library-panel', + name: 'Library Panel', + }); }); describe('getPanelQueries', () => { diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 071004812a5..695157f5c6c 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -278,23 +278,23 @@ function getElementsFromPanels(panels: Panel[]): [DashboardV2Spec['elements'], D // iterate over panels for (const p of panels) { - let elementName; + const elementName = p.id!.toString(); // LibraryPanelKind if (p.libraryPanel) { - elementName = p.libraryPanel.uid; - elements[elementName] = { kind: 'LibraryPanel', spec: { - uid: p.libraryPanel.uid, - name: p.libraryPanel.name, + libraryPanel: { + uid: p.libraryPanel.uid, + name: p.libraryPanel.name, + }, + id: p.id!, + title: p.title ?? '', }, }; // PanelKind } else { - elementName = p.id!.toString(); - // FIXME: for now we should skip row panels if (p.type === 'row') { continue; @@ -826,12 +826,12 @@ function getPanelsV1( } else if (p.kind === 'LibraryPanel') { const panel = p.spec; return { - id: 0, //TODO: LibraryPanelSpec.id will be available after https://github.com/grafana/grafana/pull/99281/ is merged - title: panel.name, + id: panel.id, + title: panel.title, gridPos, libraryPanel: { - uid: panel.uid, - name: panel.name, + uid: panel.libraryPanel.uid, + name: panel.libraryPanel.name, }, }; } else { From b0347792cc428eca141ea7cd150e57ce3829392f Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:04:41 +0100 Subject: [PATCH 013/894] Zazana: Fix verb to relation mapping (#99409) --- pkg/services/authz/zanzana/common/tuple.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go index 54d99827355..30633ff74b9 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -114,8 +114,8 @@ var VerbMapping = map[string]string{ utils.VerbPatch: RelationUpdate, utils.VerbDelete: RelationDelete, utils.VerbDeleteCollection: RelationDelete, - utils.VerbGetPermissions: RelationGet, - utils.VerbSetPermissions: RelationDelete, + utils.VerbGetPermissions: RelationGetPermissions, + utils.VerbSetPermissions: RelationSetPermissions, } // RelationToVerbMapping is mapping a zanzana relation to k8s verb. From b79f1b2a2956362d0602f35f9c042bf26f0c1da2 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:04:51 +0100 Subject: [PATCH 014/894] AuzerAD: Handle empty `client_authentication` case (#99437) AuzerAD: Require client secret when client_authentication is set to empty string --- pkg/login/social/connectors/azuread_oauth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/login/social/connectors/azuread_oauth.go b/pkg/login/social/connectors/azuread_oauth.go index 66bc53db788..0f4a72ed5a7 100644 --- a/pkg/login/social/connectors/azuread_oauth.go +++ b/pkg/login/social/connectors/azuread_oauth.go @@ -373,7 +373,7 @@ func validateClientAuthentication(info *social.OAuthInfo, requester identity.Req } return nil - case social.ClientSecretPost: + case social.ClientSecretPost, "": if info.ClientSecret == "" { return ssosettings.ErrInvalidOAuthConfig("Client secret is required for Client secret authentication.") } From c62bc501d38da7119433774c09bd661129cc0956 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:33:13 +0000 Subject: [PATCH 015/894] Update dependency @types/node to v22.10.9 (#99413) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-icons/package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/mssql/package.json | 2 +- .../app/plugins/datasource/mysql/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 48 +++++++++---------- 21 files changed, 44 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index 5a117590e23..0ccf9b743f3 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "@types/lodash": "4.17.14", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4", "@types/pluralize": "^0.0.33", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 06400ab720c..cecf1602206 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -65,7 +65,7 @@ "@rollup/plugin-node-resolve": "16.0.0", "@types/history": "4.7.11", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/papaparse": "5.3.15", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 0b2c08dc592..0e70a96c6bd 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@rollup/plugin-node-resolve": "16.0.0", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/semver": "7.5.8", "esbuild": "0.24.2", "rimraf": "6.0.1", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 9ff5b7b4f8b..1f3ca5b4913 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -68,7 +68,7 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index d7b9d631c27..64befb9b038 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -45,7 +45,7 @@ "@svgr/plugin-prettier": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "@types/babel__core": "^7", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "esbuild": "0.24.2", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 684b9aed7fc..933fa923886 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -36,7 +36,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/systemjs": "6.15.1", "jest": "^29.6.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index b2d54ad027b..c7f072a4662 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -92,7 +92,7 @@ "@types/jest": "29.5.14", "@types/jquery": "3.5.32", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index cab5a2ac907..a6da2fa240c 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -41,7 +41,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-virtualized-auto-sizer": "1.0.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 65f5cd996f3..7b1872de24c 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -145,7 +145,7 @@ "@types/is-hotkey": "0.1.10", "@types/jest": "29.5.14", "@types/mock-raf": "1.0.6", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-color": "3.0.13", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 019a11edb0a..e0c782fb730 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -33,7 +33,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 2fbb13c34ce..5785f1bf348 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -35,7 +35,7 @@ "@types/debounce-promise": "3.1.9", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index 8cea9b0a65b..7b2e560795a 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 9d271b159df..daee4d49f97 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -27,7 +27,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 23dc2a0f2e4..9d4990a6b85 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -30,7 +30,7 @@ "@types/d3-random": "^3.0.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/uuid": "10.0.0", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index e1b2b9f8e89..4f66d778407 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -31,7 +31,7 @@ "@types/jest": "29.5.14", "@types/lodash": "4.17.14", "@types/logfmt": "^1.2.3", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-window": "1.8.8", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index c37bed22a65..e353840ecef 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index 757edda8588..8cf4be5928e 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index 4bf0208ecdc..3ea03a1e72a 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -23,7 +23,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index f81c885cc34..c867a299c7b 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -46,7 +46,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 3a9f96042ae..1aa5c446a8a 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -26,7 +26,7 @@ "@testing-library/react": "16.1.0", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "ts-node": "10.9.2", diff --git a/yarn.lock b/yarn.lock index 2cc6e403916..6d65dc10968 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2679,7 +2679,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2721,7 +2721,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2751,7 +2751,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2793,7 +2793,7 @@ __metadata: "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/uuid": "npm:10.0.0" @@ -2834,7 +2834,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" "@types/logfmt": "npm:^1.2.3" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/react-window": "npm:1.8.8" @@ -2874,7 +2874,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2905,7 +2905,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2933,7 +2933,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" lodash: "npm:4.17.21" @@ -2971,7 +2971,7 @@ __metadata: "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3024,7 +3024,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3074,7 +3074,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" lodash: "npm:4.17.21" @@ -3132,7 +3132,7 @@ __metadata: "@types/d3-interpolate": "npm:^3.0.0" "@types/history": "npm:4.7.11" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/papaparse": "npm:5.3.15" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3179,7 +3179,7 @@ __metadata: dependencies: "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:16.0.0" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/semver": "npm:7.5.8" esbuild: "npm:0.24.2" rimraf: "npm:6.0.1" @@ -3314,7 +3314,7 @@ __metadata: "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/tinycolor2": "npm:1.4.6" @@ -3407,7 +3407,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/systemjs": "npm:6.15.1" jest: "npm:^29.6.4" @@ -3527,7 +3527,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/jquery": "npm:3.5.32" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -3658,7 +3658,7 @@ __metadata: "@svgr/plugin-prettier": "npm:^8.1.0" "@svgr/plugin-svgo": "npm:^8.1.0" "@types/babel__core": "npm:^7" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" esbuild: "npm:0.24.2" @@ -3756,7 +3756,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/react-virtualized-auto-sizer": "npm:1.0.4" @@ -3843,7 +3843,7 @@ __metadata: "@types/jquery": "npm:3.5.32" "@types/lodash": "npm:4.17.14" "@types/mock-raf": "npm:1.0.6" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-color": "npm:3.0.13" @@ -9437,12 +9437,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:22.10.7, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4, @types/node@npm:^22.0.0": - version: 22.10.7 - resolution: "@types/node@npm:22.10.7" +"@types/node@npm:*, @types/node@npm:22.10.9, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4, @types/node@npm:^22.0.0": + version: 22.10.9 + resolution: "@types/node@npm:22.10.9" dependencies: undici-types: "npm:~6.20.0" - checksum: 10/64cde1c2f5e5f7d597d3bd462f52c3c2d688a66623eb75d25e1d1d63d384ef553a27100635ad0dbb7d74da517048aa636947863eb624cf85f25d2f22370ce474 + checksum: 10/8a13d4e27e85b2e1878e2158400981feb5d5b9508cc920d475d5c327d93c7442d4baa5d2248d1503ffdd5964d955f54d4cf1009f3a3b812464e58cd6425a81cc languageName: node linkType: hard @@ -17437,7 +17437,7 @@ __metadata: "@types/lodash": "npm:4.17.14" "@types/logfmt": "npm:^1.2.3" "@types/lucene": "npm:^2" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/node-forge": "npm:^1" "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4" "@types/pluralize": "npm:^0.0.33" From e110338dce30fa1bd1db454792ac38bccbbe598e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:51:31 +0200 Subject: [PATCH 016/894] Update dependency react-highlight-words to v0.21.0 (#99442) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 17 ++++++++--------- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 0ccf9b743f3..21488de372d 100644 --- a/package.json +++ b/package.json @@ -367,7 +367,7 @@ "react-draggable": "4.4.6", "react-dropzone": "^14.2.3", "react-grid-layout": "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch", - "react-highlight-words": "0.20.0", + "react-highlight-words": "0.21.0", "react-hook-form": "^7.49.2", "react-i18next": "^15.0.0", "react-inlinesvg": "4.1.5", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index c7f072a4662..80c1a9171e7 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -64,7 +64,7 @@ "monaco-promql": "1.7.4", "pluralize": "8.0.0", "prismjs": "1.29.0", - "react-highlight-words": "0.20.0", + "react-highlight-words": "0.21.0", "react-select": "5.9.0", "react-use": "17.6.0", "react-window": "1.8.11", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 7b1872de24c..1d2eb1a9bbb 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -92,7 +92,7 @@ "react-colorful": "5.6.1", "react-custom-scrollbars-2": "4.5.0", "react-dropzone": "14.3.5", - "react-highlight-words": "0.20.0", + "react-highlight-words": "0.21.0", "react-hook-form": "^7.49.2", "react-i18next": "^15.0.0", "react-inlinesvg": "4.1.5", diff --git a/yarn.lock b/yarn.lock index 6d65dc10968..bf6dc173208 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3572,7 +3572,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-highlight-words: "npm:0.20.0" + react-highlight-words: "npm:0.21.0" react-select: "npm:5.9.0" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" @@ -3895,7 +3895,7 @@ __metadata: react-custom-scrollbars-2: "npm:4.5.0" react-dom: "npm:18.3.1" react-dropzone: "npm:14.3.5" - react-highlight-words: "npm:0.20.0" + react-highlight-words: "npm:0.21.0" react-hook-form: "npm:^7.49.2" react-i18next: "npm:^15.0.0" react-inlinesvg: "npm:4.1.5" @@ -17603,7 +17603,7 @@ __metadata: react-draggable: "npm:4.4.6" react-dropzone: "npm:^14.2.3" react-grid-layout: "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch" - react-highlight-words: "npm:0.20.0" + react-highlight-words: "npm:0.21.0" react-hook-form: "npm:^7.49.2" react-i18next: "npm:^15.0.0" react-inlinesvg: "npm:4.1.5" @@ -25281,16 +25281,15 @@ __metadata: languageName: node linkType: hard -"react-highlight-words@npm:0.20.0": - version: 0.20.0 - resolution: "react-highlight-words@npm:0.20.0" +"react-highlight-words@npm:0.21.0": + version: 0.21.0 + resolution: "react-highlight-words@npm:0.21.0" dependencies: highlight-words-core: "npm:^1.2.0" memoize-one: "npm:^4.0.0" - prop-types: "npm:^15.5.8" peerDependencies: - react: ^0.14.0 || ^15.0.0 || ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 - checksum: 10/5adf2cfb1f325ae51ea4dd2cb7522eb433b25534355868d1a3f4556b2b9f7a774c2a1aaa143abebb63a1b3a5590e70ba3d765942a47ff754a1a513cdc5b2f58b + react: ^0.14.0 || ^15.0.0 || ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + checksum: 10/5054e1d3f536bf672047cc22d954837289061754cd342c5439b153069fca6997890f44af4cf9154149f70844dc9cdbc18d5fd82b7458b430fc42b7f1c11a6bf2 languageName: node linkType: hard From 4e740d84109e7a1f534624847846656c9206be49 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Thu, 23 Jan 2025 12:57:00 +0000 Subject: [PATCH 017/894] GCM: Time-range fix (#98455) * Include timeRange on all query executors * Ensure queries run against query specific time range * Fix lint * Improve safety of annotations queries --- pkg/tsdb/cloud-monitoring/annotation_query.go | 6 ++++++ pkg/tsdb/cloud-monitoring/cloudmonitoring.go | 18 ++++++++++-------- pkg/tsdb/cloud-monitoring/promql_query.go | 2 +- pkg/tsdb/cloud-monitoring/slo_query.go | 2 +- .../cloud-monitoring/time_series_filter.go | 2 +- pkg/tsdb/cloud-monitoring/time_series_query.go | 8 ++++---- pkg/tsdb/cloud-monitoring/types.go | 6 ++++-- pkg/tsdb/cloud-monitoring/utils.go | 10 +++++----- 8 files changed, 32 insertions(+), 22 deletions(-) diff --git a/pkg/tsdb/cloud-monitoring/annotation_query.go b/pkg/tsdb/cloud-monitoring/annotation_query.go index 5905e42317e..ee11abe95bd 100644 --- a/pkg/tsdb/cloud-monitoring/annotation_query.go +++ b/pkg/tsdb/cloud-monitoring/annotation_query.go @@ -3,6 +3,7 @@ package cloudmonitoring import ( "context" "encoding/json" + "errors" "strconv" "strings" "time" @@ -38,6 +39,11 @@ func (s *Service) executeAnnotationQuery(ctx context.Context, req *backend.Query } `json:"timeSeriesList"` }{} + if len(req.Queries) != 1 { + return nil, errors.New("multiple queries received in annotation-request") + } + + // It's okay to use the first query for annotations as there should only be one firstQuery := req.Queries[0] err = json.Unmarshal(firstQuery.JSON, &tslq) if err != nil { diff --git a/pkg/tsdb/cloud-monitoring/cloudmonitoring.go b/pkg/tsdb/cloud-monitoring/cloudmonitoring.go index 319a0c66992..107e51008c4 100644 --- a/pkg/tsdb/cloud-monitoring/cloudmonitoring.go +++ b/pkg/tsdb/cloud-monitoring/cloudmonitoring.go @@ -387,11 +387,11 @@ func queryModel(query backend.DataQuery) (grafanaQuery, error) { func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataRequest) ([]cloudMonitoringQueryExecutor, error) { cloudMonitoringQueryExecutors := make([]cloudMonitoringQueryExecutor, 0, len(req.Queries)) - startTime := req.Queries[0].TimeRange.From - endTime := req.Queries[0].TimeRange.To - durationSeconds := int(endTime.Sub(startTime).Seconds()) - for _, query := range req.Queries { + for index, query := range req.Queries { + startTime := req.Queries[index].TimeRange.From + endTime := req.Queries[index].TimeRange.To + durationSeconds := int(endTime.Sub(startTime).Seconds()) q, err := queryModel(query) if err != nil { return nil, fmt.Errorf("could not unmarshal CloudMonitoringQuery json: %w", err) @@ -401,8 +401,9 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR switch query.QueryType { case string(dataquery.QueryTypeTIMESERIESLIST), string(dataquery.QueryTypeANNOTATION): cmtsf := &cloudMonitoringTimeSeriesList{ - refID: query.RefID, - aliasBy: q.AliasBy, + refID: query.RefID, + aliasBy: q.AliasBy, + timeRange: req.Queries[index].TimeRange, } if q.TimeSeriesList.View == nil || *q.TimeSeriesList.View == "" { fullString := "FULL" @@ -417,7 +418,7 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR aliasBy: q.AliasBy, parameters: q.TimeSeriesQuery, IntervalMS: query.Interval.Milliseconds(), - timeRange: req.Queries[0].TimeRange, + timeRange: req.Queries[index].TimeRange, logger: logger, } case string(dataquery.QueryTypeSLO): @@ -425,6 +426,7 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR refID: query.RefID, aliasBy: q.AliasBy, parameters: q.SloQuery, + timeRange: req.Queries[index].TimeRange, } cmslo.setParams(startTime, endTime, durationSeconds, query.Interval.Milliseconds()) queryInterface = cmslo @@ -433,7 +435,7 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR refID: query.RefID, aliasBy: q.AliasBy, parameters: q.PromQLQuery, - timeRange: req.Queries[0].TimeRange, + timeRange: req.Queries[index].TimeRange, logger: logger, } queryInterface = cmp diff --git a/pkg/tsdb/cloud-monitoring/promql_query.go b/pkg/tsdb/cloud-monitoring/promql_query.go index 63baf4584f2..e71a22ade55 100644 --- a/pkg/tsdb/cloud-monitoring/promql_query.go +++ b/pkg/tsdb/cloud-monitoring/promql_query.go @@ -31,7 +31,7 @@ func (promQLQ *cloudMonitoringProm) run(ctx context.Context, req *backend.QueryD return dr, backend.DataResponse{}, "", nil } - span := traceReq(ctx, req, dsInfo, r, "") + span := traceReq(ctx, req, dsInfo, r, "", promQLQ.timeRange) defer span.End() requestBody := map[string]any{ diff --git a/pkg/tsdb/cloud-monitoring/slo_query.go b/pkg/tsdb/cloud-monitoring/slo_query.go index 7aa6544482d..bcf53610cf7 100644 --- a/pkg/tsdb/cloud-monitoring/slo_query.go +++ b/pkg/tsdb/cloud-monitoring/slo_query.go @@ -12,7 +12,7 @@ import ( func (sloQ *cloudMonitoringSLO) run(ctx context.Context, req *backend.QueryDataRequest, s *Service, dsInfo datasourceInfo, logger log.Logger) (*backend.DataResponse, any, string, error) { - return runTimeSeriesRequest(ctx, req, s, dsInfo, sloQ.parameters.ProjectName, sloQ.params, nil, logger) + return runTimeSeriesRequest(ctx, req, s, dsInfo, sloQ.parameters.ProjectName, sloQ.params, nil, logger, sloQ.timeRange) } func (sloQ *cloudMonitoringSLO) parseResponse(queryRes *backend.DataResponse, diff --git a/pkg/tsdb/cloud-monitoring/time_series_filter.go b/pkg/tsdb/cloud-monitoring/time_series_filter.go index 7df6fd16f4e..f2fe2da5dab 100644 --- a/pkg/tsdb/cloud-monitoring/time_series_filter.go +++ b/pkg/tsdb/cloud-monitoring/time_series_filter.go @@ -17,7 +17,7 @@ import ( func (timeSeriesFilter *cloudMonitoringTimeSeriesList) run(ctx context.Context, req *backend.QueryDataRequest, s *Service, dsInfo datasourceInfo, logger log.Logger) (*backend.DataResponse, any, string, error) { - return runTimeSeriesRequest(ctx, req, s, dsInfo, timeSeriesFilter.parameters.ProjectName, timeSeriesFilter.params, nil, logger) + return runTimeSeriesRequest(ctx, req, s, dsInfo, timeSeriesFilter.parameters.ProjectName, timeSeriesFilter.params, nil, logger, timeSeriesFilter.timeRange) } func parseTimeSeriesResponse(queryRes *backend.DataResponse, diff --git a/pkg/tsdb/cloud-monitoring/time_series_query.go b/pkg/tsdb/cloud-monitoring/time_series_query.go index 84f66a193a4..bacf61e645b 100644 --- a/pkg/tsdb/cloud-monitoring/time_series_query.go +++ b/pkg/tsdb/cloud-monitoring/time_series_query.go @@ -18,7 +18,7 @@ func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) appendGraphPeriod(req *ba if timeSeriesQuery.parameters.GraphPeriod != "disabled" { if timeSeriesQuery.parameters.GraphPeriod == "auto" || timeSeriesQuery.parameters.GraphPeriod == "" { intervalCalculator := gcmTime.NewCalculator(gcmTime.CalculatorOptions{}) - interval := intervalCalculator.Calculate(req.Queries[0].TimeRange, time.Duration(timeSeriesQuery.IntervalMS/1000)*time.Second, req.Queries[0].MaxDataPoints) + interval := intervalCalculator.Calculate(timeSeriesQuery.timeRange, time.Duration(timeSeriesQuery.IntervalMS/1000)*time.Second, req.Queries[0].MaxDataPoints) timeSeriesQuery.parameters.GraphPeriod = interval.Text } return fmt.Sprintf(" | graph_period %s", timeSeriesQuery.parameters.GraphPeriod) @@ -29,14 +29,14 @@ func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) appendGraphPeriod(req *ba func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) run(ctx context.Context, req *backend.QueryDataRequest, s *Service, dsInfo datasourceInfo, logger log.Logger) (*backend.DataResponse, any, string, error) { timeSeriesQuery.parameters.Query += timeSeriesQuery.appendGraphPeriod(req) - from := req.Queries[0].TimeRange.From - to := req.Queries[0].TimeRange.To + from := timeSeriesQuery.timeRange.From + to := timeSeriesQuery.timeRange.To timeFormat := "2006/01/02-15:04:05" timeSeriesQuery.parameters.Query += fmt.Sprintf(" | within d'%s', d'%s'", from.UTC().Format(timeFormat), to.UTC().Format(timeFormat)) requestBody := map[string]any{ "query": timeSeriesQuery.parameters.Query, } - return runTimeSeriesRequest(ctx, req, s, dsInfo, timeSeriesQuery.parameters.ProjectName, nil, requestBody, logger) + return runTimeSeriesRequest(ctx, req, s, dsInfo, timeSeriesQuery.parameters.ProjectName, nil, requestBody, logger, timeSeriesQuery.timeRange) } func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) parseResponse(queryRes *backend.DataResponse, diff --git a/pkg/tsdb/cloud-monitoring/types.go b/pkg/tsdb/cloud-monitoring/types.go index 39610af7500..e16415d3aca 100644 --- a/pkg/tsdb/cloud-monitoring/types.go +++ b/pkg/tsdb/cloud-monitoring/types.go @@ -42,7 +42,8 @@ type ( aliasBy string parameters *dataquery.TimeSeriesList // Processed properties - params url.Values + timeRange backend.TimeRange + params url.Values } // cloudMonitoringSLO is used to build time series with a filter but for the SLO case cloudMonitoringSLO struct { @@ -50,7 +51,8 @@ type ( aliasBy string parameters *dataquery.SLOQuery // Processed properties - params url.Values + timeRange backend.TimeRange + params url.Values } // cloudMonitoringProm is used to build a promQL queries diff --git a/pkg/tsdb/cloud-monitoring/utils.go b/pkg/tsdb/cloud-monitoring/utils.go index 4499ebe5dad..1843fd6db52 100644 --- a/pkg/tsdb/cloud-monitoring/utils.go +++ b/pkg/tsdb/cloud-monitoring/utils.go @@ -124,11 +124,11 @@ func doRequestWithPagination(ctx context.Context, r *http.Request, dsInfo dataso return d, nil } -func traceReq(ctx context.Context, req *backend.QueryDataRequest, dsInfo datasourceInfo, _ *http.Request, target string) trace.Span { +func traceReq(ctx context.Context, req *backend.QueryDataRequest, dsInfo datasourceInfo, _ *http.Request, target string, timeRange backend.TimeRange) trace.Span { _, span := tracing.DefaultTracer().Start(ctx, "cloudMonitoring query", trace.WithAttributes( attribute.String("target", target), - attribute.String("from", req.Queries[0].TimeRange.From.String()), - attribute.String("until", req.Queries[0].TimeRange.To.String()), + attribute.String("from", timeRange.From.String()), + attribute.String("until", timeRange.To.String()), attribute.Int64("datasource_id", dsInfo.id), attribute.Int64("org_id", req.PluginContext.OrgID), )) @@ -137,7 +137,7 @@ func traceReq(ctx context.Context, req *backend.QueryDataRequest, dsInfo datasou } func runTimeSeriesRequest(ctx context.Context, req *backend.QueryDataRequest, - s *Service, dsInfo datasourceInfo, projectName string, params url.Values, body map[string]any, logger log.Logger) (*backend.DataResponse, cloudMonitoringResponse, string, error) { + s *Service, dsInfo datasourceInfo, projectName string, params url.Values, body map[string]any, logger log.Logger, timeRange backend.TimeRange) (*backend.DataResponse, cloudMonitoringResponse, string, error) { dr := &backend.DataResponse{} projectName, err := s.ensureProject(ctx, dsInfo, projectName) if err != nil { @@ -154,7 +154,7 @@ func runTimeSeriesRequest(ctx context.Context, req *backend.QueryDataRequest, return dr, cloudMonitoringResponse{}, "", nil } - span := traceReq(ctx, req, dsInfo, r, params.Encode()) + span := traceReq(ctx, req, dsInfo, r, params.Encode(), timeRange) defer span.End() d, err := doRequestWithPagination(ctx, r, dsInfo, params, body, logger) From 2716db4270b9c7f590aed8a25dbb65956bd8a42d Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:13:29 +0100 Subject: [PATCH 018/894] Alerting: Use stack_id instead of id in cloud failures panel in the Insights page (#99424) * Use stack_id instead of id in cloud failures panel in the insights page * update all the wrong id to stack_id --- .../alerting/unified/insights/mimir/AlertsByState.tsx | 2 +- .../alerting/unified/insights/mimir/InvalidConfig.tsx | 2 +- .../alerting/unified/insights/mimir/Notifications.tsx | 4 ++-- .../app/features/alerting/unified/insights/mimir/Silences.tsx | 2 +- .../RuleGroupEvaluationDurationIntervalRatioScene.tsx | 2 +- .../mimir/perGroup/RuleGroupEvaluationDurationScene.tsx | 2 +- .../insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx | 4 ++-- .../insights/mimir/perGroup/RuleGroupIntervalScene.tsx | 2 +- .../unified/insights/mimir/perGroup/RulesPerGroupScene.tsx | 2 +- .../insights/mimir/rules/EvalSuccessVsFailuresScene.tsx | 4 ++-- .../unified/insights/mimir/rules/MissedIterationsScene.tsx | 2 +- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/public/app/features/alerting/unified/insights/mimir/AlertsByState.tsx b/public/app/features/alerting/unified/insights/mimir/AlertsByState.tsx index 5ea2438a856..f977a642fd4 100644 --- a/public/app/features/alerting/unified/insights/mimir/AlertsByState.tsx +++ b/public/app/features/alerting/unified/insights/mimir/AlertsByState.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../InsightsMenuButton'; export function getAlertsByStateScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `sum by (state) (grafanacloud_instance_alertmanager_alerts{id="${INSTANCE_ID}"})` + ? `sum by (state) (grafanacloud_instance_alertmanager_alerts{stack_id="${INSTANCE_ID}"})` : `sum by (state) (grafanacloud_instance_alertmanager_alerts)`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/InvalidConfig.tsx b/public/app/features/alerting/unified/insights/mimir/InvalidConfig.tsx index 8c9a43227cf..d5834b1abb9 100644 --- a/public/app/features/alerting/unified/insights/mimir/InvalidConfig.tsx +++ b/public/app/features/alerting/unified/insights/mimir/InvalidConfig.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../InsightsMenuButton'; export function getInvalidConfigScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `sum by (cluster)(grafanacloud_instance_alertmanager_invalid_config{id="${INSTANCE_ID}"})` + ? `sum by (cluster)(grafanacloud_instance_alertmanager_invalid_config{stack_id="${INSTANCE_ID}"})` : `sum by (cluster)(grafanacloud_instance_alertmanager_invalid_config)`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/Notifications.tsx b/public/app/features/alerting/unified/insights/mimir/Notifications.tsx index 59c0ecdc4f4..0f2d254a2ec 100644 --- a/public/app/features/alerting/unified/insights/mimir/Notifications.tsx +++ b/public/app/features/alerting/unified/insights/mimir/Notifications.tsx @@ -6,11 +6,11 @@ import { InsightsMenuButton } from '../InsightsMenuButton'; export function getNotificationsScene(datasource: DataSourceRef, panelTitle: string) { const exprA = INSTANCE_ID - ? `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_per_second{id="${INSTANCE_ID}"}) - sum by (cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second{id="${INSTANCE_ID}"})` + ? `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_per_second{stack_id="${INSTANCE_ID}"}) - sum by (cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second{stack_id="${INSTANCE_ID}"})` : `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_per_second) - sum by (cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second)`; const exprB = INSTANCE_ID - ? `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second{id="${INSTANCE_ID}"})` + ? `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second{stack_id="${INSTANCE_ID}"})` : `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second)`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/Silences.tsx b/public/app/features/alerting/unified/insights/mimir/Silences.tsx index 8cbddfd0205..670038808a3 100644 --- a/public/app/features/alerting/unified/insights/mimir/Silences.tsx +++ b/public/app/features/alerting/unified/insights/mimir/Silences.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../InsightsMenuButton'; export function getSilencesScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `sum by (state) (grafanacloud_instance_alertmanager_silences{id="${INSTANCE_ID}"})` + ? `sum by (state) (grafanacloud_instance_alertmanager_silences{stack_id="${INSTANCE_ID}"})` : `sum by (state) (grafanacloud_instance_alertmanager_silences)`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationIntervalRatioScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationIntervalRatioScene.tsx index 50f4084ac7c..f05e8fb818d 100644 --- a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationIntervalRatioScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationIntervalRatioScene.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getRuleGroupEvaluationDurationIntervalRatioScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group", id="${INSTANCE_ID}"} / grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group", id="${INSTANCE_ID}"}` + ? `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group", stack_id="${INSTANCE_ID}"} / grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group", stack_id="${INSTANCE_ID}"}` : `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group"} / grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group"}`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationScene.tsx index 12ff616336a..5ae445093cd 100644 --- a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationScene.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getRuleGroupEvaluationDurationScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group", id="${INSTANCE_ID}"}` + ? `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group", stack_id="${INSTANCE_ID}"}` : `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group"}`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx index 4eb70e39d0b..d29901ae7c3 100644 --- a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx @@ -6,11 +6,11 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getRuleGroupEvaluationsScene(datasource: DataSourceRef, panelTitle: string) { const exprA = INSTANCE_ID - ? `grafanacloud_instance_rule_evaluations_total:rate5m{rule_group="$rule_group", id="${INSTANCE_ID}"} - grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group", id="${INSTANCE_ID}"}` + ? `grafanacloud_instance_rule_evaluations_total:rate5m{rule_group="$rule_group", stack_id="${INSTANCE_ID}"} - grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group", stack_id="${INSTANCE_ID}"}` : `grafanacloud_instance_rule_evaluations_total:rate5m{rule_group="$rule_group"} - grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group"}`; const exprB = INSTANCE_ID - ? `grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group", id="${INSTANCE_ID}"}` + ? `grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group", stack_id="${INSTANCE_ID}"}` : `grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group"}`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupIntervalScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupIntervalScene.tsx index f54fc582bc4..521580c096c 100644 --- a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupIntervalScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupIntervalScene.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getRuleGroupIntervalScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group", id="${INSTANCE_ID}"}` + ? `grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group", stack_id="${INSTANCE_ID}"}` : `grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group"}`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RulesPerGroupScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RulesPerGroupScene.tsx index c2c9ac1b44d..3000ce2c31d 100644 --- a/public/app/features/alerting/unified/insights/mimir/perGroup/RulesPerGroupScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RulesPerGroupScene.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getRulesPerGroupScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `sum(grafanacloud_instance_rule_group_rules{rule_group="$rule_group", id="${INSTANCE_ID}"})` + ? `sum(grafanacloud_instance_rule_group_rules{rule_group="$rule_group", stack_id="${INSTANCE_ID}"})` : `sum(grafanacloud_instance_rule_group_rules{rule_group="$rule_group"})`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/rules/EvalSuccessVsFailuresScene.tsx b/public/app/features/alerting/unified/insights/mimir/rules/EvalSuccessVsFailuresScene.tsx index e82fadeb898..bd457de9e64 100644 --- a/public/app/features/alerting/unified/insights/mimir/rules/EvalSuccessVsFailuresScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/rules/EvalSuccessVsFailuresScene.tsx @@ -6,11 +6,11 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getEvalSuccessVsFailuresScene(datasource: DataSourceRef, panelTitle: string) { const exprA = INSTANCE_ID - ? `sum(grafanacloud_instance_rule_evaluations_total:rate5m{id="${INSTANCE_ID}"}) - sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m{id="${INSTANCE_ID}"})` + ? `sum(grafanacloud_instance_rule_evaluations_total:rate5m{stack_id="${INSTANCE_ID}"}) - sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m{stack_id="${INSTANCE_ID}"})` : `sum(grafanacloud_instance_rule_evaluations_total:rate5m) - sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m)`; const exprB = INSTANCE_ID - ? `sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m{id="${INSTANCE_ID}"})` + ? `sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m{stack_id="${INSTANCE_ID}"})` : `sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m)`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/rules/MissedIterationsScene.tsx b/public/app/features/alerting/unified/insights/mimir/rules/MissedIterationsScene.tsx index c32f399146c..9331c90c53a 100644 --- a/public/app/features/alerting/unified/insights/mimir/rules/MissedIterationsScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/rules/MissedIterationsScene.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getMissedIterationsScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `sum(grafanacloud_instance_rule_group_iterations_missed_total:rate5m{id="${INSTANCE_ID}"})` + ? `sum(grafanacloud_instance_rule_group_iterations_missed_total:rate5m{stack_id="${INSTANCE_ID}"})` : `sum(grafanacloud_instance_rule_group_iterations_missed_total:rate5m)`; const query = new SceneQueryRunner({ From 89dd54a47493df60debddc665ffcd91de3a3c04d Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Thu, 23 Jan 2025 06:36:07 -0700 Subject: [PATCH 019/894] Folder: delete from folder table after children (#99399) Co-authored-by: maicon --- pkg/services/folder/folderimpl/folder.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index cdcb9c2ee2b..8d86292c758 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -992,6 +992,12 @@ func (s *Service) DeleteLegacy(ctx context.Context, cmd *folder.DeleteFolderComm } } + err = s.store.Delete(ctx, []string{cmd.UID}, cmd.OrgID) + if err != nil { + s.log.InfoContext(ctx, "failed deleting folder", "org_id", cmd.OrgID, "uid", cmd.UID, "err", err) + return err + } + if err = s.legacyDelete(ctx, cmd, folders); err != nil { return err } @@ -1240,11 +1246,11 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold for _, f := range descendants { descendantUIDs = append(descendantUIDs, f.UID) } - s.log.InfoContext(ctx, "deleting folder and its descendants", "org_id", cmd.OrgID, "uid", cmd.UID) - toDelete := append(descendantUIDs, cmd.UID) - err = s.store.Delete(ctx, toDelete, cmd.OrgID) + s.log.InfoContext(ctx, "deleting folder descendants", "org_id", cmd.OrgID, "uid", cmd.UID) + + err = s.store.Delete(ctx, descendantUIDs, cmd.OrgID) if err != nil { - s.log.InfoContext(ctx, "failed deleting folder", "org_id", cmd.OrgID, "uid", cmd.UID, "err", err) + s.log.InfoContext(ctx, "failed deleting descendants", "org_id", cmd.OrgID, "parent_uid", cmd.UID, "err", err) return descendantUIDs, err } return descendantUIDs, nil From d39e57e836d77722d26bc909154fcad4044c72a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:42:29 +0000 Subject: [PATCH 020/894] Update dependency react-i18next to v15.4.0 (#99444) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index bf6dc173208..96111fd4d26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25312,8 +25312,8 @@ __metadata: linkType: hard "react-i18next@npm:^15.0.0": - version: 15.2.0 - resolution: "react-i18next@npm:15.2.0" + version: 15.4.0 + resolution: "react-i18next@npm:15.4.0" dependencies: "@babel/runtime": "npm:^7.25.0" html-parse-stringify: "npm:^3.0.1" @@ -25325,7 +25325,7 @@ __metadata: optional: true react-native: optional: true - checksum: 10/9b2937f7beab763c494d55a801f21bfdbfe98e9509994c350d24fa404ded573f41e8607eeba290c686d5877d34f0ddefe48e9d6876720d5ed0e1243bcdd5dda6 + checksum: 10/4b3666d819f01cf96a256af4419b26938d314e33c6388eafccc29f67ad02994e5d53e7bf82eac656cade7f7bcd04f4a237f0b293165d7eda91d62e3fde605a38 languageName: node linkType: hard From a037c6f34472ec8e16ab1020121a1be30b0e4186 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 23 Jan 2025 17:25:03 +0300 Subject: [PATCH 021/894] K8s/Folders: Remove kubernetesFolders flag and full path metadata (#99256) * remove full path * remove more * remove KubernetesFolders tests * remove feature toggles * remove feature toggles * skip permissions test * skip permissions test --------- Co-authored-by: Jack Baldry --- .../feature-toggles/index.md | 212 ++++--- .../src/types/featureToggles.gen.ts | 2 - pkg/api/folder.go | 570 +----------------- pkg/api/folder_test.go | 348 +---------- pkg/apimachinery/utils/meta.go | 41 -- pkg/registry/apis/folders/conversions.go | 38 -- pkg/registry/apis/folders/conversions_test.go | 18 - pkg/registry/apis/folders/register.go | 2 - pkg/services/featuremgmt/registry.go | 12 - pkg/services/featuremgmt/toggles_gen.csv | 2 - pkg/services/featuremgmt/toggles_gen.go | 8 - pkg/services/featuremgmt/toggles_gen.json | 152 ++--- pkg/services/folder/folderimpl/folder.go | 14 +- .../folderimpl/folder_unifiedstorage_test.go | 1 - pkg/tests/apis/folder/folders_test.go | 40 +- 15 files changed, 209 insertions(+), 1251 deletions(-) delete mode 100644 pkg/registry/apis/folders/conversions_test.go 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 2bd6d4eef14..435c59f79ef 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -129,113 +129,111 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- [Experimental](https://grafana.com/docs/release-life-cycle/#experimental) features are early in their development lifecycle and so are not yet supported in Grafana Cloud. Experimental features might be changed or removed without prior notice. -| Feature toggle name | Description | -| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | -| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | -| `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 | -| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | -| `alertingBacktesting` | Rule backtesting API for alerting | -| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | -| `lokiShardSplitting` | Use stream shards to split queries into smaller subqueries | -| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | -| `individualCookiePreferences` | Support overriding cookie preferences per user | -| `influxqlStreamingParser` | Enable streaming JSON parser for InfluxDB datasource InfluxQL query language | -| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | -| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | -| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | -| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | -| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | -| `extraThemes` | Enables extra themes | -| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | -| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | -| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | -| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | -| `mlExpressions` | Enable support for Machine Learning in server-side expressions | -| `metricsSummary` | Enables metrics summary queries in the Tempo data source | -| `datasourceAPIServers` | Expose some datasources as apiservers. | -| `provisioning` | Next generation provisioning... and git | -| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | -| `aiGeneratedDashboardChanges` | Enable AI powered features for dashboards to auto-summary changes when saving | -| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. | -| `libraryPanelRBAC` | Enables RBAC support for library panels | -| `wargamesTesting` | Placeholder feature flag for internal testing | -| `externalCorePlugins` | Allow core plugins to be loaded as external | -| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | -| `enableNativeHTTPHistogram` | Enables native HTTP Histograms | -| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) | -| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | -| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | -| `kubernetesCliDashboards` | Use the k8s client to retrieve dashboards internally | -| `kubernetesRestore` | Allow restoring objects in k8s | -| `kubernetesFolders` | Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s | -| `kubernetesFoldersServiceV2` | Use the Folders Service V2, and route Folder Service requests to k8s | -| `grafanaAPIServerTestingWithExperimentalAPIs` | Facilitate integration testing of experimental APIs | -| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) | -| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query | -| `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | -| `queryServiceFromUI` | Routes requests to the new query service | -| `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | -| `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | -| `prometheusCodeModeMetricNamesSearch` | Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names | -| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | -| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | -| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. | -| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe | -| `dashboardNewLayouts` | Enables experimental new dashboard layouts | -| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes | -| `tableSharedCrosshair` | Enables shared crosshair in table panel | -| `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | -| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | -| `onPremToCloudMigrationsAuthApiMig` | Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction. | -| `scopeApi` | In-development feature flag for the scope api using the app platform. | -| `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | -| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | -| `kubernetesAggregator` | Enable grafana's embedded kube-aggregator | -| `expressionParser` | Enable new expression parser | -| `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. | -| `queryLibrary` | Enables Query Library feature in Explore | -| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | -| `alertingListViewV2` | Enables the new alert list view design | -| `dashboardRestore` | Enables deleted dashboard restore feature | -| `alertingCentralAlertHistory` | Enables the new central alert history. | -| `sqlQuerybuilderFunctionParameters` | Enables SQL query builder function parameters | -| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | -| `dataplaneAggregator` | Enable grafana dataplane aggregator | -| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | -| `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources | -| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards | -| `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics | -| `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range | -| `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | -| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | -| `rolePickerDrawer` | Enables the new role picker drawer design | -| `pluginsSriChecks` | Enables SRI checks for plugin assets | -| `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage | -| `timeRangeProvider` | Enables time pickers sync | -| `prometheusUsesCombobox` | Use new combobox component for Prometheus query editor | -| `playlistsReconciler` | Enables experimental reconciler for playlists | -| `prometheusSpecialCharsInLabelValues` | Adds support for quotes and special characters in label values for Prometheus queries | -| `enableExtensionsAdminPage` | Enables the extension admin page regardless of development mode | -| `enableSCIM` | Enables SCIM support for user and group management | -| `crashDetection` | Enables browser crash detection reporting to Faro. | -| `jaegerBackendMigration` | Enables querying the Jaeger data source without the proxy | -| `useV2DashboardsAPI` | Use the v2 kubernetes API in the frontend for dashboards | -| `unifiedHistory` | Displays the navigation history so the user can navigate back to previous pages | -| `investigationsBackend` | Enable the investigations backend API | -| `k8SFolderCounts` | Enable folder's api server counts | -| `k8SFolderMove` | Enable folder's api server move | -| `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams | -| `queryLibraryDashboards` | Enables Query Library feature in Dashboards | -| `grafanaAdvisor` | Enables Advisor app | -| `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing | -| `datasourceConnectionsTab` | Shows defined connections for a data source in the plugins detail page | +| Feature toggle name | Description | +| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | +| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | +| `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 | +| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | +| `alertingBacktesting` | Rule backtesting API for alerting | +| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | +| `lokiShardSplitting` | Use stream shards to split queries into smaller subqueries | +| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | +| `individualCookiePreferences` | Support overriding cookie preferences per user | +| `influxqlStreamingParser` | Enable streaming JSON parser for InfluxDB datasource InfluxQL query language | +| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | +| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | +| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | +| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | +| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | +| `extraThemes` | Enables extra themes | +| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | +| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | +| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | +| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | +| `mlExpressions` | Enable support for Machine Learning in server-side expressions | +| `metricsSummary` | Enables metrics summary queries in the Tempo data source | +| `datasourceAPIServers` | Expose some datasources as apiservers. | +| `provisioning` | Next generation provisioning... and git | +| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | +| `aiGeneratedDashboardChanges` | Enable AI powered features for dashboards to auto-summary changes when saving | +| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. | +| `libraryPanelRBAC` | Enables RBAC support for library panels | +| `wargamesTesting` | Placeholder feature flag for internal testing | +| `externalCorePlugins` | Allow core plugins to be loaded as external | +| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | +| `enableNativeHTTPHistogram` | Enables native HTTP Histograms | +| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) | +| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | +| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | +| `kubernetesCliDashboards` | Use the k8s client to retrieve dashboards internally | +| `kubernetesRestore` | Allow restoring objects in k8s | +| `kubernetesFoldersServiceV2` | Use the Folders Service V2, and route Folder Service requests to k8s | +| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) | +| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query | +| `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | +| `queryServiceFromUI` | Routes requests to the new query service | +| `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | +| `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | +| `prometheusCodeModeMetricNamesSearch` | Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names | +| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | +| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | +| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. | +| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe | +| `dashboardNewLayouts` | Enables experimental new dashboard layouts | +| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes | +| `tableSharedCrosshair` | Enables shared crosshair in table panel | +| `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | +| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | +| `onPremToCloudMigrationsAuthApiMig` | Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction. | +| `scopeApi` | In-development feature flag for the scope api using the app platform. | +| `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | +| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | +| `kubernetesAggregator` | Enable grafana's embedded kube-aggregator | +| `expressionParser` | Enable new expression parser | +| `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. | +| `queryLibrary` | Enables Query Library feature in Explore | +| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | +| `alertingListViewV2` | Enables the new alert list view design | +| `dashboardRestore` | Enables deleted dashboard restore feature | +| `alertingCentralAlertHistory` | Enables the new central alert history. | +| `sqlQuerybuilderFunctionParameters` | Enables SQL query builder function parameters | +| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | +| `dataplaneAggregator` | Enable grafana dataplane aggregator | +| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | +| `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources | +| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards | +| `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics | +| `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range | +| `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | +| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | +| `rolePickerDrawer` | Enables the new role picker drawer design | +| `pluginsSriChecks` | Enables SRI checks for plugin assets | +| `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage | +| `timeRangeProvider` | Enables time pickers sync | +| `prometheusUsesCombobox` | Use new combobox component for Prometheus query editor | +| `playlistsReconciler` | Enables experimental reconciler for playlists | +| `prometheusSpecialCharsInLabelValues` | Adds support for quotes and special characters in label values for Prometheus queries | +| `enableExtensionsAdminPage` | Enables the extension admin page regardless of development mode | +| `enableSCIM` | Enables SCIM support for user and group management | +| `crashDetection` | Enables browser crash detection reporting to Faro. | +| `jaegerBackendMigration` | Enables querying the Jaeger data source without the proxy | +| `useV2DashboardsAPI` | Use the v2 kubernetes API in the frontend for dashboards | +| `unifiedHistory` | Displays the navigation history so the user can navigate back to previous pages | +| `investigationsBackend` | Enable the investigations backend API | +| `k8SFolderCounts` | Enable folder's api server counts | +| `k8SFolderMove` | Enable folder's api server move | +| `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams | +| `queryLibraryDashboards` | Enables Query Library feature in Dashboards | +| `grafanaAdvisor` | Enables Advisor app | +| `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing | +| `datasourceConnectionsTab` | Shows defined connections for a data source in the plugins detail page | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e26b79a8056..97e532863af 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -112,9 +112,7 @@ export interface FeatureToggles { kubernetesDashboards?: boolean; kubernetesCliDashboards?: boolean; kubernetesRestore?: boolean; - kubernetesFolders?: boolean; kubernetesFoldersServiceV2?: boolean; - grafanaAPIServerTestingWithExperimentalAPIs?: boolean; datasourceQueryTypes?: boolean; queryService?: boolean; queryServiceRewrite?: boolean; diff --git a/pkg/api/folder.go b/pkg/api/folder.go index c6ca4e308fb..af38eb9de2c 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -3,16 +3,8 @@ package api import ( "context" "errors" - "fmt" "net/http" "strconv" - "strings" - - k8sErrors "k8s.io/apimachinery/pkg/api/errors" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/api/apierrors" @@ -20,13 +12,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" - folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/infra/slugify" - internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/services/accesscontrol" - grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" @@ -36,9 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/search" - "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" - "github.com/grafana/grafana/pkg/util/errhttp" "github.com/grafana/grafana/pkg/web" ) @@ -57,38 +42,16 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions)) }) }) - if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) && !hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) { - // Use k8s client to implement legacy API - handler := newFolderK8sHandler(hs) - folderRoute.Post("/", handler.createFolder) - folderRoute.Get("/", handler.getFolders) - folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) { - folderUidRoute.Put("/", handler.updateFolder) - folderUidRoute.Delete("/", handler.deleteFolder) - folderUidRoute.Get("/", handler.getFolder) - if hs.Features.IsEnabledGlobally(featuremgmt.FlagK8SFolderCounts) { - folderUidRoute.Get("/counts", handler.countFolderContent) - } else { - folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts)) - } - if hs.Features.IsEnabledGlobally(featuremgmt.FlagK8SFolderMove) { - folderUidRoute.Post("/move", handler.moveFolder) - } else { - folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder)) - } - folderUidRoute.Get("parents", handler.getFolderParents) - }) - } else { - folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder)) - folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders)) - folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) { - folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder)) - folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder)) - folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID)) - folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts)) - folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder)) - }) - } + + folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder)) + folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders)) + folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) { + folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder)) + folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder)) + folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID)) + folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts)) + folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder)) + }) }) } @@ -683,516 +646,3 @@ type GetFolderDescendantCountsResponse struct { // in: body Body folder.DescendantCounts `json:"body"` } - -type folderK8sHandler struct { - namespacer request.NamespaceMapper - gvr schema.GroupVersionResource - clientConfigProvider grafanaapiserver.DirectRestConfigProvider - // #TODO check if it makes more sense to move this to FolderAPIBuilder - accesscontrolService accesscontrol.Service - userService user.Service -} - -//----------------------------------------------------------------------------------------- -// Folder k8s wrapper functions -//----------------------------------------------------------------------------------------- - -func newFolderK8sHandler(hs *HTTPServer) *folderK8sHandler { - return &folderK8sHandler{ - gvr: folderalpha1.FolderResourceInfo.GroupVersionResource(), - namespacer: request.GetNamespaceMapper(hs.Cfg), - clientConfigProvider: hs.clientConfigProvider, - accesscontrolService: hs.accesscontrolService, - userService: hs.userService, - } -} - -func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return // error is already sent - } - cmd := &folder.CreateFolderCommand{} - if err := web.Bind(c.Req, cmd); err != nil { - c.JsonApiErr(http.StatusBadRequest, "bad request data", err) - return - } - obj, err := internalfolders.LegacyCreateCommandToUnstructured(cmd) - if err != nil { - fk8s.writeError(c, err) - return - } - out, err := client.Create(c.Req.Context(), obj, v1.CreateOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - fk8s.accesscontrolService.ClearUserPermissionCache(c.SignedInUser) - folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID()) - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, folderDTO) -} - -func (fk8s *folderK8sHandler) getFolders(c *contextmodel.ReqContext) { - // NOTE: the current implementation is temporary and it will be - // replaced by a proper indexing service/search API - // Also, the current implementation does not support pagination - - parentUid := strings.ToUpper(c.Query("parentUid")) - - client, ok := fk8s.getClient(c) - if !ok { - return // error is already sent - } - - // check that parent exists - if parentUid != "" { - _, err := client.Get(c.Req.Context(), c.Query("parentUid"), v1.GetOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - } - - out, err := client.List(c.Req.Context(), v1.ListOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - hits := make([]dtos.FolderSearchHit, 0) - for _, item := range out.Items { - // convert item to legacy folder format - f, _ := internalfolders.UnstructuredToLegacyFolder(item, c.SignedInUser.GetOrgID()) - if f == nil { - fk8s.writeError(c, fmt.Errorf("unable covert unstructured item to legacy folder")) - return - } - - // it we are at root level, skip subfolder - if parentUid == "" && f.ParentUID != "" { - continue // query filter - } - // if we are at a nested folder, then skip folders that don't belong to parentUid - if parentUid != "" && strings.ToUpper(f.ParentUID) != parentUid { - continue - } - - hits = append(hits, dtos.FolderSearchHit{ - ID: f.ID, // nolint:staticcheck - UID: f.UID, - Title: f.Title, - ParentUID: f.ParentUID, - }) - } - - c.JSON(http.StatusOK, hits) -} - -func (fk8s *folderK8sHandler) countFolderContent(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return - } - - uid := web.Params(c.Req)[":uid"] - - counts, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}, "counts") - if err != nil { - fk8s.writeError(c, err) - return - } - - out, err := toFolderLegacyCounts(counts) - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, out) -} - -func (fk8s *folderK8sHandler) getFolderParents(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return - } - - uid := web.Params(c.Req)[":uid"] - - out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}, "parents") - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, out) -} - -func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return // error is already sent - } - uid := web.Params(c.Req)[":uid"] - - var out *unstructured.Unstructured - var err error - - if uid == accesscontrol.GeneralFolderUID { - out = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "title": folder.RootFolder.Title, - "description": folder.RootFolder.Description, - }, - }, - } - out.SetName(folder.RootFolder.UID) - } else { - out, err = client.Get(c.Req.Context(), uid, v1.GetOptions{}) - } - - if err != nil { - fk8s.writeError(c, err) - return - } - - folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID()) - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, folderDTO) -} - -func (fk8s *folderK8sHandler) deleteFolder(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return // error is already sent - } - uid := web.Params(c.Req)[":uid"] - err := client.Delete(c.Req.Context(), uid, v1.DeleteOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - c.JSON(http.StatusOK, "") -} - -func (fk8s *folderK8sHandler) updateFolder(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return // error is already sent - } - - var ctx = c.Req.Context() - - cmd := &folder.UpdateFolderCommand{} - if err := web.Bind(c.Req, cmd); err != nil { - c.JsonApiErr(http.StatusBadRequest, "bad request data", err) - return - } - cmd.UID = web.Params(c.Req)[":uid"] - - obj, err := client.Get(ctx, cmd.UID, v1.GetOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - updated, err := internalfolders.LegacyUpdateCommandToUnstructured(obj, cmd) - if err != nil { - return - } - - out, err := client.Update(ctx, updated, v1.UpdateOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID()) - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, folderDTO) -} - -func (fk8s *folderK8sHandler) moveFolder(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return - } - - ctx := c.Req.Context() - - cmd := folder.MoveFolderCommand{} - if err := web.Bind(c.Req, &cmd); err != nil { - c.JsonApiErr(http.StatusBadRequest, "bad request data", err) - return - } - cmd.UID = web.Params(c.Req)[":uid"] - - obj, err := client.Get(ctx, cmd.UID, v1.GetOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - obj, err = internalfolders.LegacyMoveCommandToUnstructured(obj, cmd) - if err != nil { - fk8s.writeError(c, err) - return - } - - out, err := client.Update(c.Req.Context(), obj, v1.UpdateOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID()) - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, folderDTO) -} - -//----------------------------------------------------------------------------------------- -// Utility functions -//----------------------------------------------------------------------------------------- - -func (fk8s *folderK8sHandler) getClient(c *contextmodel.ReqContext) (dynamic.ResourceInterface, bool) { - dyn, err := dynamic.NewForConfig(fk8s.clientConfigProvider.GetDirectRestConfig(c)) - if err != nil { - c.JsonApiErr(500, "client", err) - return nil, false - } - return dyn.Resource(fk8s.gvr).Namespace(fk8s.namespacer(c.OrgID)), true -} - -func (fk8s *folderK8sHandler) writeError(c *contextmodel.ReqContext, err error) { - //nolint:errorlint - statusError, ok := err.(*k8sErrors.StatusError) - if ok { - message := statusError.Status().Message - // #TODO: Is there a better way to set the correct meesage? Instead of "access denied to folder", currently we are - // returning something like `folders.folder.grafana.app is forbidden: User "" cannot create resource "folders" in - // API group "folder.grafana.app" in the namespace "default": folder`` - if statusError.Status().Code == http.StatusForbidden { - message = dashboards.ErrFolderAccessDenied.Error() - } - c.JsonApiErr(int(statusError.Status().Code), message, err) - return - } - errhttp.Write(c.Req.Context(), err, c.Resp) -} - -func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item unstructured.Unstructured, orgID int64) (dtos.Folder, error) { - f, createdBy := internalfolders.UnstructuredToLegacyFolder(item, orgID) - - dontCheckCanView := false - checkCanView := true - // no need to check view permission for the starting folder since it's already checked by the callers - folderDTO, err := fk8s.toDTO(c, f, createdBy, dontCheckCanView) - if err != nil { - return dtos.Folder{}, err - } - - if len(f.Fullpath) == 0 || len(f.FullpathUIDs) == 0 { - return folderDTO, nil - } - - parentsFullPath, err := internalfolders.GetParentTitles(f.Fullpath) - if err != nil { - return dtos.Folder{}, err - } - parentsFullPathUIDs := strings.Split(f.FullpathUIDs, "/") - - // The first part of the path is the newly created folder which we don't need to include - // in the parents field - if len(parentsFullPath) < 2 || len(parentsFullPathUIDs) < 2 { - return folderDTO, nil - } - - parents := []dtos.Folder{} - for i, v := range parentsFullPath[1:] { - slug := slugify.Slugify(v) - uid := parentsFullPathUIDs[1:][i] - url := dashboards.GetFolderURL(uid, slug) - - ff := folder.Folder{ - UID: uid, - Title: v, - URL: url, - } - parentDTO, err := fk8s.toDTO(c, &ff, "", checkCanView) - if err != nil { - // #TODO should we log this error? - return dtos.Folder{}, err - } - - parents = append(parents, parentDTO) - } - - folderDTO.Parents = parents - - return folderDTO, nil -} - -func toUID(rawIdentifier string) string { - // #TODO Is there a preexisting function we can use instead, something along the lines of UserIdentifier? - parts := strings.Split(rawIdentifier, ":") - if len(parts) < 2 { - return "" - } - return parts[1] -} - -func (fk8s *folderK8sHandler) toDTO(c *contextmodel.ReqContext, fold *folder.Folder, createdBy string, checkCanView bool) (dtos.Folder, error) { - // #TODO revisit how/where we get orgID - ctx := c.Req.Context() - - g, err := guardian.NewByFolder(c.Req.Context(), fold, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return dtos.Folder{}, err - } - - canEdit, _ := g.CanEdit() - canSave, _ := g.CanSave() - canAdmin, _ := g.CanAdmin() - canDelete, _ := g.CanDelete() - - // Finding creator and last updater of the folder - updater, creator := anonString, anonString - // #TODO refactor the various conversions of the folder so that we either set created by in folder.Folder or - // we convert from unstructured to folder DTO without an intermediate conversion to folder.Folder - if len(createdBy) > 0 { - creator = fk8s.getIdentityName(ctx, toUID(createdBy)) - } - if len(createdBy) > 0 { - updater = fk8s.getIdentityName(ctx, toUID(createdBy)) - } - - acMetadata, _ := fk8s.getFolderACMetadata(c, fold) - - if checkCanView { - canView, _ := g.CanView() - if !canView { - return dtos.Folder{ - UID: REDACTED, - Title: REDACTED, - }, nil - } - } - metrics.MFolderIDsAPICount.WithLabelValues(metrics.NewToFolderDTO).Inc() - - return dtos.Folder{ - ID: fold.ID, // nolint:staticcheck - UID: fold.UID, - Title: fold.Title, - URL: fold.URL, - HasACL: fold.HasACL, - CanSave: canSave, - CanEdit: canEdit, - CanAdmin: canAdmin, - CanDelete: canDelete, - CreatedBy: creator, - Created: fold.Created, - UpdatedBy: updater, - Updated: fold.Updated, - // #TODO version doesn't seem to be used--confirm or set it properly - Version: fold.Version, - AccessControl: acMetadata, - ParentUID: fold.ParentUID, - }, nil -} - -func (fk8s *folderK8sHandler) getIdentityName(ctx context.Context, uid string) string { - ctx, span := tracer.Start(ctx, "api.getUserLogin") - defer span.End() - - ident, err := fk8s.userService.GetByUID(ctx, &user.GetUserByUIDQuery{ - UID: uid, - }) - if err != nil { - return anonString - } - - if ident.IsServiceAccount { - return ident.Name - } - return ident.Login -} - -func (fk8s *folderK8sHandler) getFolderACMetadata(c *contextmodel.ReqContext, f *folder.Folder) (accesscontrol.Metadata, error) { - if !c.QueryBool("accesscontrol") { - return nil, nil - } - - folderIDs, err := getParents(f) - if err != nil { - return nil, err - } - - allMetadata := getMultiAccessControlMetadata(c, dashboards.ScopeFoldersPrefix, folderIDs) - metadata := map[string]bool{} - // Flatten metadata - if any parent has a permission, the child folder inherits it - for _, md := range allMetadata { - for action := range md { - metadata[action] = true - } - } - return metadata, nil -} - -func getParents(f *folder.Folder) (map[string]bool, error) { - folderIDs := map[string]bool{f.UID: true} - if (f.UID == accesscontrol.GeneralFolderUID) || (f.UID == folder.SharedWithMeFolderUID) { - return folderIDs, nil - } - - parentsFullPathUIDs := strings.Split(f.FullpathUIDs, "/") - // The first part of the path is the newly created folder which we don't need to check here - if len(parentsFullPathUIDs) < 2 { - return folderIDs, nil - } - - for _, uid := range parentsFullPathUIDs[1:] { - folderIDs[uid] = true - } - - return folderIDs, nil -} - -func toFolderLegacyCounts(u *unstructured.Unstructured) (*folder.DescendantCounts, error) { - ds, err := folderalpha1.UnstructuredToDescendantCounts(u) - if err != nil { - return nil, err - } - - var out = make(folder.DescendantCounts) - for _, v := range ds.Counts { - // if stats come from unified storage, we will use them - if v.Group != "sql-fallback" { - out[v.Resource] = v.Count - continue - } - // if stats are from single tenant DB and they are not in unified storage, we will use them - if _, ok := out[v.Resource]; !ok { - out[v.Resource] = v.Count - } - } - return &out, nil -} diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index b676bb15696..a464727a31b 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -8,18 +8,15 @@ import ( "net/http/httptest" "strings" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" clientrest "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/api/dtos" folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" - conversions "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" @@ -532,349 +529,6 @@ func (m mockClientConfigProvider) GetDirectRestConfig(c *contextmodel.ReqContext func (m mockClientConfigProvider) DirectlyServeHTTP(w http.ResponseWriter, r *http.Request) {} -func TestUpdateFolderLegacyAndUnifiedStorage(t *testing.T) { - testuser := &user.User{ID: 99, UID: "fdxsqt7t5ryf4a", Login: "testuser"} - testSignedInUser := &user.SignedInUser{UserID: 99, UserUID: "fdxsqt7t5ryf4a", Login: "testuser"} - - legacyFolder := folder.Folder{ - UID: "ady4yobv315a8e", - Title: "Example folder 226", - URL: "/dashboards/f/ady4yobv315a8e/example-folder-226", - CreatedBy: 99, - CreatedByUID: "fdxsqt7t5ryf4a", - Created: time.Date(2024, time.November, 29, 0, 42, 34, 0, time.UTC), - UpdatedBy: 99, - UpdatedByUID: "fdxsqt7t5ryf4a", - Updated: time.Date(2024, time.November, 29, 0, 42, 34, 0, time.UTC), - Version: 3, - } - - namespacer := func(_ int64) string { return "1" } - unifiedStorageFolder, err := conversions.LegacyFolderToUnstructured(&legacyFolder, namespacer) - require.NoError(t, err) - - expectedFolder := dtos.Folder{ - UID: legacyFolder.UID, - OrgID: 0, - Title: legacyFolder.Title, - URL: legacyFolder.URL, - HasACL: false, - CanSave: false, - CanEdit: true, - CanAdmin: false, - CanDelete: false, - CreatedBy: "testuser", - Created: legacyFolder.Created, - UpdatedBy: "testuser", - Updated: legacyFolder.Updated, - Version: legacyFolder.Version, - } - - mux := http.NewServeMux() - - mux.HandleFunc("GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/ady4yobv315a8e", func(w http.ResponseWriter, req *http.Request) { - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(200) - err := json.NewEncoder(w).Encode(unifiedStorageFolder) - require.NoError(t, err) - }) - mux.HandleFunc("PUT /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/ady4yobv315a8e", func(w http.ResponseWriter, req *http.Request) { - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(200) - err := json.NewEncoder(w).Encode(unifiedStorageFolder) - require.NoError(t, err) - }) - - folderApiServerMock := httptest.NewServer(mux) - defer folderApiServerMock.Close() - - t.Run("happy path", func(t *testing.T) { - type testCase struct { - description string - folderUID string - legacyFolder folder.Folder - expectedFolder dtos.Folder - expectedFolderServiceError error - unifiedStorageEnabled bool - unifiedStorageMode grafanarest.DualWriterMode - expectedCode int - } - - tcs := []testCase{ - { - description: "Happy Path - Legacy", - expectedCode: http.StatusOK, - legacyFolder: legacyFolder, - folderUID: legacyFolder.UID, - expectedFolder: expectedFolder, - unifiedStorageEnabled: false, - }, - { - description: "Happy Path - Unified storage, mode 1", - expectedCode: http.StatusOK, - legacyFolder: legacyFolder, - folderUID: legacyFolder.UID, - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode1, - }, - { - description: "Happy Path - Unified storage, mode 2", - expectedCode: http.StatusOK, - legacyFolder: legacyFolder, - folderUID: legacyFolder.UID, - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode2, - }, - { - description: "Happy Path - Unified storage, mode 3", - expectedCode: http.StatusOK, - legacyFolder: legacyFolder, - folderUID: legacyFolder.UID, - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode3, - }, - { - description: "Happy Path - Unified storage, mode 4", - expectedCode: http.StatusOK, - legacyFolder: legacyFolder, - folderUID: legacyFolder.UID, - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode4, - }, - { - description: "Folder Not Found - Legacy", - expectedCode: http.StatusNotFound, - legacyFolder: legacyFolder, - folderUID: "notfound", - expectedFolder: expectedFolder, - unifiedStorageEnabled: false, - expectedFolderServiceError: dashboards.ErrFolderNotFound, - }, - { - description: "Folder Not Found - Unified storage, mode 1", - expectedCode: http.StatusNotFound, - legacyFolder: legacyFolder, - folderUID: "notfound", - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode1, - }, - { - description: "Folder Not Found - Unified storage, mode 2", - expectedCode: http.StatusNotFound, - legacyFolder: legacyFolder, - folderUID: "notfound", - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode2, - }, - { - description: "Folder Not Found - Unified storage, mode 3", - expectedCode: http.StatusNotFound, - legacyFolder: legacyFolder, - folderUID: "notfound", - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode3, - }, - { - description: "Folder Not Found - Unified storage, mode 4", - expectedCode: http.StatusNotFound, - legacyFolder: legacyFolder, - folderUID: "notfound", - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode4, - }, - } - - for _, tc := range tcs { - t.Run(tc.description, func(t *testing.T) { - setUpRBACGuardian(t) - - cfg := setting.NewCfg() - cfg.UnifiedStorage = map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { - DualWriterMode: tc.unifiedStorageMode, - }, - } - - featuresArr := []any{featuremgmt.FlagNestedFolders} - if tc.unifiedStorageEnabled { - featuresArr = append(featuresArr, featuremgmt.FlagKubernetesFolders) - } - - server := SetupAPITestServer(t, func(hs *HTTPServer) { - hs.Cfg = cfg - hs.folderService = &foldertest.FakeService{ - ExpectedFolder: &tc.legacyFolder, - ExpectedError: tc.expectedFolderServiceError, - } - hs.QuotaService = quotatest.New(false, nil) - hs.SearchService = &mockSearchService{ - ExpectedResult: model.HitList{}, - } - hs.userService = &usertest.FakeUserService{ - ExpectedUser: testuser, - ExpectedSignedInUser: testSignedInUser, - } - hs.Features = featuremgmt.WithFeatures( - featuresArr..., - ) - hs.clientConfigProvider = mockClientConfigProvider{ - host: folderApiServerMock.URL, - } - }) - - req := server.NewRequest(http.MethodPut, fmt.Sprintf("/api/folders/%s", tc.folderUID), strings.NewReader(`{"title":"new title"}`)) - req.Header.Set("Content-Type", "application/json") - webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{ - 1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{ - {Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}, - {Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("ady4yobv315a8e")}, - }), - }}) - - res, err := server.Send(req) - require.NoError(t, err) - - require.Equal(t, tc.expectedCode, res.StatusCode) - defer func() { require.NoError(t, res.Body.Close()) }() - - if tc.expectedCode == http.StatusOK { - body := dtos.Folder{} - require.NoError(t, json.NewDecoder(res.Body).Decode(&body)) - - //nolint:staticcheck - body.ID = 0 - body.Version = 0 - tc.expectedFolder.Version = 0 - require.Equal(t, tc.expectedFolder, body) - } - }) - } - }) -} - -func TestToFolderCounts(t *testing.T) { - var tests = []struct { - name string - input *unstructured.Unstructured - expected *folder.DescendantCounts - expectError bool - }{ - { - name: "with only counts from unified storage", - input: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "folder.grafana.app/v0alpha1", - "counts": []interface{}{ - map[string]interface{}{ - "group": "alpha", - "resource": "folders", - "count": int64(1), - }, - map[string]interface{}{ - "group": "alpha", - "resource": "dashboards", - "count": int64(3), - }, - map[string]interface{}{ - "group": "alpha", - "resource": "alertRules", - "count": int64(0), - }, - }, - }, - }, - expected: &folder.DescendantCounts{ - "folders": 1, - "dashboards": 3, - "alertRules": 0, - }, - }, - { - name: "with counts from both storages", - input: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "folder.grafana.app/v0alpha1", - "counts": []interface{}{ - map[string]interface{}{ - "group": "alpha", - "resource": "folders", - "count": int64(1), - }, - map[string]interface{}{ - "group": "alpha", - "resource": "dashboards", - "count": int64(3), - }, - map[string]interface{}{ - "group": "sql-fallback", - "resource": "folders", - "count": int64(0), - }, - }, - }, - }, - expected: &folder.DescendantCounts{ - "folders": 1, - "dashboards": 3, - }, - }, - { - name: "it uses the values from sql-fallaback if not found in unified storage", - input: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "folder.grafana.app/v0alpha1", - "counts": []interface{}{ - map[string]interface{}{ - "group": "alpha", - "resource": "dashboards", - "count": int64(3), - }, - map[string]interface{}{ - "group": "sql-fallback", - "resource": "folders", - "count": int64(2), - }, - }, - }, - }, - expected: &folder.DescendantCounts{ - "folders": 2, - "dashboards": 3, - }, - }, - { - name: "malformed input", - input: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "folder.grafana.app/v0alpha1", - "counts": map[string]interface{}{}, - }, - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual, err := toFolderLegacyCounts(tc.input) - if tc.expectError { - require.Error(t, err) - return - } - require.NoError(t, err) - require.Equal(t, tc.expected, actual) - }) - } -} - // for now, test only the general folder func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) { testuser := &user.User{ID: 99, UID: "fdxsqt7t5ryf4a", Login: "testuser"} @@ -971,7 +625,7 @@ func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) { featuresArr := []any{featuremgmt.FlagNestedFolders} if tc.unifiedStorageEnabled { - featuresArr = append(featuresArr, featuremgmt.FlagKubernetesFolders) + featuresArr = append(featuresArr, featuremgmt.FlagKubernetesFoldersServiceV2) } server := SetupAPITestServer(t, func(hs *HTTPServer) { diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index fb6be0230f1..4d39d4fcd57 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -57,15 +57,6 @@ const oldAnnoKeyOriginPath = "grafana.app/originPath" const oldAnnoKeyOriginHash = "grafana.app/originHash" const oldAnnoKeyOriginTimestamp = "grafana.app/originTimestamp" -// annoKeyFullPath encodes the full path in folder resources -// revisit keeping these folder-specific annotations once we have complete support for mode 1 -// Deprecated: this goes away when folders have a better solution -const annoKeyFullPath = "grafana.app/fullPath" - -// annoKeyFullPathUIDs encodes the full path in folder resources -// Deprecated: this goes away when folders have a better solution -const annoKeyFullPathUIDs = "grafana.app/fullPathUIDs" - // ResourceRepositoryInfo is encoded into kubernetes metadata annotations. // This value identifies indicates the state of the resource in its provisioning source when // the spec was last saved. Currently this is derived from the dashboards provisioning table. @@ -140,18 +131,6 @@ type GrafanaMetaAccessor interface { // NOTE the type must match the existing value, or an error will be thrown SetStatus(any) error - // Deprecated: this is a temporary hack for folders, it will be removed without notice soon - GetFullPath() string - - // Deprecated: this is a temporary hack for folders, it will be removed without notice soon - SetFullPath(path string) - - // Deprecated: this is a temporary hack for folders, it will be removed without notice soon - GetFullPathUIDs() string - - // Deprecated: this is a temporary hack for folders, it will be removed without notice soon - SetFullPathUIDs(path string) - // Find a title in the object // This will reflect the object and try to get: // * spec.title @@ -706,26 +685,6 @@ func (m *grafanaMetaAccessor) SetStatus(s any) (err error) { return } -func (m *grafanaMetaAccessor) GetFullPath() string { - // nolint:staticcheck - return m.get(annoKeyFullPath) -} - -func (m *grafanaMetaAccessor) SetFullPath(path string) { - // nolint:staticcheck - m.SetAnnotation(annoKeyFullPath, path) -} - -func (m *grafanaMetaAccessor) GetFullPathUIDs() string { - // nolint:staticcheck - return m.get(annoKeyFullPathUIDs) -} - -func (m *grafanaMetaAccessor) SetFullPathUIDs(path string) { - // nolint:staticcheck - m.SetAnnotation(annoKeyFullPathUIDs, path) -} - func (m *grafanaMetaAccessor) FindTitle(defaultTitle string) string { // look for Spec.Title or Spec.Name spec := m.r.FieldByName("Spec") diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index 474911d1ac1..470b22ff0f6 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -2,7 +2,6 @@ package folders import ( "fmt" - "regexp" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -114,16 +113,6 @@ func UnstructuredToLegacyFolder(item unstructured.Unstructured, orgID int64) (*f // meta.GetUpdatedTimestamp() but it currently gets overwritten in prepareObjectForStorage(). Updated: createdTime, OrgID: orgID, - - // This will need to be restructured so the full path is looked up when saving - // it can't be saved in the resource metadata because then everything must cascade - // nolint:staticcheck - Fullpath: meta.GetFullPath(), - - // This will need to be restructured so the full path is looked up when saving - // it can't be saved in the resource metadata because then everything must cascade - // nolint:staticcheck - FullpathUIDs: meta.GetFullPathUIDs(), } // CreatedBy needs to be returned separately because it's the user UID (string) but // folder.Folder expects user ID (int64). @@ -172,14 +161,6 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) if v.ParentUID != "" { meta.SetFolder(v.ParentUID) } - if v.Fullpath != "" { - // nolint:staticcheck - meta.SetFullPath(v.Fullpath) - } - if v.FullpathUIDs != "" { - // nolint:staticcheck - meta.SetFullPathUIDs(v.FullpathUIDs) - } f.UID = gapiutil.CalculateClusterWideUID(f) return f, nil } @@ -203,22 +184,3 @@ func getCreated(meta utils.GrafanaMetaAccessor) (*time.Time, error) { created := meta.GetCreationTimestamp().Time return &created, nil } - -func GetParentTitles(fullPath string) ([]string, error) { - // Find all forward slashes which aren't escaped - r, err := regexp.Compile(`[^\\](/)`) - if err != nil { - return nil, err - } - indices := r.FindAllStringIndex(fullPath, -1) - - var start int - titles := []string{} - for _, i := range indices { - titles = append(titles, fullPath[start:i[0]+1]) - start = i[0] + 2 - } - - titles = append(titles, fullPath[start:]) - return titles, nil -} diff --git a/pkg/registry/apis/folders/conversions_test.go b/pkg/registry/apis/folders/conversions_test.go deleted file mode 100644 index 70310d50968..00000000000 --- a/pkg/registry/apis/folders/conversions_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package folders - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestGetParentTitles(t *testing.T) { - path := "get\\/folder-folder-0/get\\/folder-folder-1/another" - - titles, err := GetParentTitles(path) - require.Nil(t, err) - require.Equal(t, 3, len(titles)) - require.Equal(t, "get\\/folder-folder-0", titles[0]) - require.Equal(t, "get\\/folder-folder-1", titles[1]) - require.Equal(t, "another", titles[2]) -} diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index ffc15901864..e7e163ff156 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -65,9 +65,7 @@ func RegisterAPIService(cfg *setting.Cfg, unified resource.ResourceClient, ) *FolderAPIBuilder { if !featuremgmt.AnyEnabled(features, - featuremgmt.FlagKubernetesFolders, featuremgmt.FlagKubernetesFoldersServiceV2, - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagProvisioning) { return nil // skip registration unless opting into Kubernetes folders or unless we want to customize registration when testing diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 9e36b24d863..3ac4af60495 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -708,24 +708,12 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaAppPlatformSquad, }, - { - Name: "kubernetesFolders", - Description: "Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - }, { Name: "kubernetesFoldersServiceV2", Description: "Use the Folders Service V2, and route Folder Service requests to k8s", Stage: FeatureStageExperimental, Owner: grafanaSearchAndStorageSquad, }, - { - Name: "grafanaAPIServerTestingWithExperimentalAPIs", - Description: "Facilitate integration testing of experimental APIs", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - }, { Name: "datasourceQueryTypes", Description: "Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 7f960ff666c..013ef1b7373 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -93,9 +93,7 @@ kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true, kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true kubernetesCliDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,false kubernetesRestore,experimental,@grafana/grafana-app-platform-squad,false,false,false -kubernetesFolders,experimental,@grafana/search-and-storage,false,false,false kubernetesFoldersServiceV2,experimental,@grafana/search-and-storage,false,false,false -grafanaAPIServerTestingWithExperimentalAPIs,experimental,@grafana/search-and-storage,false,false,false datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false queryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b9447726172..c9bfd15c6b7 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -383,18 +383,10 @@ const ( // Allow restoring objects in k8s FlagKubernetesRestore = "kubernetesRestore" - // FlagKubernetesFolders - // Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s - FlagKubernetesFolders = "kubernetesFolders" - // FlagKubernetesFoldersServiceV2 // Use the Folders Service V2, and route Folder Service requests to k8s FlagKubernetesFoldersServiceV2 = "kubernetesFoldersServiceV2" - // FlagGrafanaAPIServerTestingWithExperimentalAPIs - // Facilitate integration testing of experimental APIs - FlagGrafanaAPIServerTestingWithExperimentalAPIs = "grafanaAPIServerTestingWithExperimentalAPIs" - // FlagDatasourceQueryTypes // Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) FlagDatasourceQueryTypes = "datasourceQueryTypes" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 52db6dd9222..4170f739681 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -7,7 +7,7 @@ "metadata": { "name": "ABTestFeatureToggleA", "resourceVersion": "1736782112674", - "creationTimestamp": "2025-01-13T15:28:32Z" + "creationTimestamp": "2025-01-13T21:13:13Z" }, "spec": { "description": "Test feature toggle to see how cohorts could be set up AB testing", @@ -21,7 +21,7 @@ "metadata": { "name": "ABTestFeatureToggleB", "resourceVersion": "1736782112674", - "creationTimestamp": "2025-01-13T15:28:32Z" + "creationTimestamp": "2025-01-13T21:13:13Z" }, "spec": { "description": "Test feature toggle to see how cohorts could be set up AB testing", @@ -277,7 +277,7 @@ "metadata": { "name": "alertingNotificationsStepMode", "resourceVersion": "1737362059637", - "creationTimestamp": "2024-11-06T09:35:49Z", + "creationTimestamp": "2024-11-22T11:07:45Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-20 08:34:19.63725 +0000 UTC" } @@ -294,7 +294,7 @@ "metadata": { "name": "alertingPrometheusRulesPrimary", "resourceVersion": "1727332930692", - "creationTimestamp": "2024-09-09T13:56:47Z", + "creationTimestamp": "2024-09-27T12:27:16Z", "annotations": { "grafana.app/updatedTimestamp": "2024-09-26 06:42:10.692959 +0000 UTC" } @@ -371,7 +371,7 @@ "metadata": { "name": "alertingUIOptimizeReducer", "resourceVersion": "1731923458730", - "creationTimestamp": "2024-11-18T09:06:02Z", + "creationTimestamp": "2024-11-18T10:59:00Z", "annotations": { "grafana.app/updatedTimestamp": "2024-11-18 09:50:58.730825 +0000 UTC" } @@ -458,7 +458,7 @@ "name": "appPlatformAccessTokens", "resourceVersion": "1725549369316", "creationTimestamp": "2024-09-05T16:18:44Z", - "deletionTimestamp": "2024-10-14T13:14:46Z" + "deletionTimestamp": "2024-10-14T10:47:18Z" }, "spec": { "description": "Enables the use of access tokens for the App Platform", @@ -472,7 +472,7 @@ "metadata": { "name": "appPlatformGrpcClientAuth", "resourceVersion": "1728662061076", - "creationTimestamp": "2024-10-11T15:54:21Z" + "creationTimestamp": "2024-10-14T10:47:18Z" }, "spec": { "description": "Enables the gRPC client to authenticate with the App Platform by using ID \u0026 access tokens", @@ -608,7 +608,7 @@ "name": "autoMigrateXYChartPanel", "resourceVersion": "1722537244598", "creationTimestamp": "2024-03-22T15:44:37Z", - "deletionTimestamp": "2024-11-14T01:17:06Z", + "deletionTimestamp": "2024-11-14T16:36:18Z", "annotations": { "grafana.app/updatedTimestamp": "2024-08-01 18:34:04.598082 +0000 UTC" } @@ -684,7 +684,7 @@ "metadata": { "name": "azureMonitorDisableLogLimit", "resourceVersion": "1727698096407", - "creationTimestamp": "2024-09-30T11:51:51Z", + "creationTimestamp": "2024-10-24T13:32:09Z", "deletionTimestamp": "2024-10-22T09:44:12Z", "annotations": { "grafana.app/updatedTimestamp": "2024-09-30 12:08:16.407109 +0000 UTC" @@ -701,7 +701,7 @@ "metadata": { "name": "azureMonitorEnableUserAuth", "resourceVersion": "1732189410576", - "creationTimestamp": "2024-11-21T11:42:29Z", + "creationTimestamp": "2024-11-27T14:01:54Z", "annotations": { "grafana.app/updatedTimestamp": "2024-11-21 11:43:30.576196 +0000 UTC" } @@ -902,7 +902,7 @@ "name": "cloudwatchMetricInsightsCrossAccount", "resourceVersion": "1729265619643", "creationTimestamp": "2024-07-02T10:34:12Z", - "deletionTimestamp": "2025-01-10T15:06:19Z", + "deletionTimestamp": "2025-01-10T22:23:23Z", "annotations": { "grafana.app/updatedTimestamp": "2024-10-18 15:33:39.643165 +0000 UTC" } @@ -950,7 +950,7 @@ "metadata": { "name": "crashDetection", "resourceVersion": "1730381712885", - "creationTimestamp": "2024-10-31T13:35:12Z" + "creationTimestamp": "2024-11-12T15:07:27Z" }, "spec": { "description": "Enables browser crash detection reporting to Faro.", @@ -963,7 +963,7 @@ "metadata": { "name": "dashboardNewLayouts", "resourceVersion": "1729671312626", - "creationTimestamp": "2024-10-16T08:44:05Z", + "creationTimestamp": "2024-10-23T08:55:45Z", "annotations": { "grafana.app/updatedTimestamp": "2024-10-23 08:15:12.626632 +0000 UTC" } @@ -997,7 +997,7 @@ "name": "dashboardRestoreUI", "resourceVersion": "1720021873452", "creationTimestamp": "2024-06-25T14:43:13Z", - "deletionTimestamp": "2024-10-08T14:24:51Z", + "deletionTimestamp": "2024-10-11T08:29:58Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -1064,8 +1064,8 @@ "metadata": { "name": "dashboardSchemaV2", "resourceVersion": "1730192092473", - "creationTimestamp": "2024-10-29T08:54:52Z", - "deletionTimestamp": "2024-12-19T12:03:44Z" + "creationTimestamp": "2024-10-29T10:35:18Z", + "deletionTimestamp": "2024-12-19T12:28:20Z" }, "spec": { "description": "Enables the new dashboard schema version 2, implementing changes necessary for dynamic dashboards and dashboards as code.", @@ -1159,7 +1159,7 @@ "metadata": { "name": "datasourceConnectionsTab", "resourceVersion": "1737049826022", - "creationTimestamp": "2025-01-16T17:36:09Z", + "creationTimestamp": "2025-01-21T17:39:48Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-16 17:50:26.022636488 +0000 UTC" } @@ -1331,7 +1331,7 @@ "metadata": { "name": "elasticsearchCrossClusterSearch", "resourceVersion": "1733848475752", - "creationTimestamp": "2024-12-09T13:53:38Z", + "creationTimestamp": "2024-12-12T22:20:04Z", "annotations": { "grafana.app/updatedTimestamp": "2024-12-10 16:34:35.752111 +0000 UTC" } @@ -1346,7 +1346,7 @@ "metadata": { "name": "elasticsearchImprovedParsing", "resourceVersion": "1736808262603", - "creationTimestamp": "2025-01-13T20:32:35Z", + "creationTimestamp": "2025-01-15T17:05:54Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-13 22:44:22.603729 +0000 UTC" } @@ -1374,7 +1374,7 @@ "metadata": { "name": "enableExtensionsAdminPage", "resourceVersion": "1730819353237", - "creationTimestamp": "2024-11-05T09:18:42Z", + "creationTimestamp": "2024-11-05T15:55:10Z", "annotations": { "grafana.app/updatedTimestamp": "2024-11-05 15:09:13.237578 +0000 UTC" } @@ -1404,7 +1404,7 @@ "metadata": { "name": "enableSCIM", "resourceVersion": "1730980484343", - "creationTimestamp": "2024-11-07T11:54:44Z" + "creationTimestamp": "2024-11-07T14:38:46Z" }, "spec": { "description": "Enables SCIM support for user and group management", @@ -1416,7 +1416,7 @@ "metadata": { "name": "enableScopesInMetricsExplore", "resourceVersion": "1729765731452", - "creationTimestamp": "2024-10-24T10:28:51Z" + "creationTimestamp": "2024-11-06T13:11:33Z" }, "spec": { "description": "Enables the scopes usage in Metrics Explore", @@ -1501,7 +1501,7 @@ "metadata": { "name": "exploreMetricsRelatedLogs", "resourceVersion": "1730125602673", - "creationTimestamp": "2024-10-28T14:26:42Z" + "creationTimestamp": "2024-11-05T16:28:43Z" }, "spec": { "description": "Display Related Logs in Explore Metrics", @@ -1635,7 +1635,7 @@ "metadata": { "name": "feedbackButton", "resourceVersion": "1733158016122", - "creationTimestamp": "2024-12-02T16:46:56Z" + "creationTimestamp": "2024-12-02T17:08:15Z" }, "spec": { "description": "Enables a button to send feedback from the Grafana UI", @@ -1734,7 +1734,8 @@ "metadata": { "name": "grafanaAPIServerTestingWithExperimentalAPIs", "resourceVersion": "1727945615419", - "creationTimestamp": "2024-10-03T08:53:35Z" + "creationTimestamp": "2024-10-03T10:11:40Z", + "deletionTimestamp": "2025-01-22T20:53:53Z" }, "spec": { "description": "Facilitate integration testing of experimental APIs", @@ -1763,7 +1764,7 @@ "metadata": { "name": "grafanaAdvisor", "resourceVersion": "1737365459765", - "creationTimestamp": "2025-01-20T09:30:59Z" + "creationTimestamp": "2025-01-20T10:08:00Z" }, "spec": { "description": "Enables Advisor app", @@ -1897,7 +1898,7 @@ "metadata": { "name": "improvedExternalSessionHandlingSAML", "resourceVersion": "1737370880023", - "creationTimestamp": "2025-01-09T16:33:07Z", + "creationTimestamp": "2025-01-09T17:02:49Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-20 11:01:20.02358 +0000 UTC" } @@ -1973,7 +1974,7 @@ "metadata": { "name": "investigationsBackend", "resourceVersion": "1734447689720", - "creationTimestamp": "2024-12-17T15:01:29Z" + "creationTimestamp": "2024-12-18T08:31:03Z" }, "spec": { "description": "Enable the investigations backend API", @@ -2000,7 +2001,7 @@ "metadata": { "name": "jaegerBackendMigration", "resourceVersion": "1731599633815", - "creationTimestamp": "2024-11-14T15:53:53Z" + "creationTimestamp": "2024-11-15T14:40:20Z" }, "spec": { "description": "Enables querying the Jaeger data source without the proxy", @@ -2026,7 +2027,7 @@ "metadata": { "name": "k8SFolderCounts", "resourceVersion": "1735294794086", - "creationTimestamp": "2024-12-27T10:19:54Z" + "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { "description": "Enable folder's api server counts", @@ -2039,7 +2040,7 @@ "metadata": { "name": "k8SFolderMove", "resourceVersion": "1735294794086", - "creationTimestamp": "2024-12-27T10:19:54Z" + "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { "description": "Enable folder's api server move", @@ -2081,7 +2082,7 @@ "metadata": { "name": "kubernetesCliDashboards", "resourceVersion": "1733520389522", - "creationTimestamp": "2024-12-06T21:26:29Z" + "creationTimestamp": "2024-12-13T22:55:43Z" }, "spec": { "description": "Use the k8s client to retrieve dashboards internally", @@ -2121,6 +2122,7 @@ "name": "kubernetesFolders", "resourceVersion": "1725863636605", "creationTimestamp": "2024-09-10T09:22:08Z", + "deletionTimestamp": "2025-01-22T20:49:15Z", "annotations": { "grafana.app/updatedTimestamp": "2024-09-09 06:33:56.605329 +0000 UTC" } @@ -2135,7 +2137,7 @@ "metadata": { "name": "kubernetesFoldersServiceV2", "resourceVersion": "1735336477446", - "creationTimestamp": "2024-12-27T21:54:37Z" + "creationTimestamp": "2025-01-13T21:15:35Z" }, "spec": { "description": "Use the Folders Service V2, and route Folder Service requests to k8s", @@ -2165,7 +2167,7 @@ "metadata": { "name": "kubernetesRestore", "resourceVersion": "1735880498698", - "creationTimestamp": "2025-01-03T05:01:38Z" + "creationTimestamp": "2025-01-03T14:48:47Z" }, "spec": { "description": "Allow restoring objects in k8s", @@ -2219,7 +2221,7 @@ "metadata": { "name": "logQLScope", "resourceVersion": "1730842404843", - "creationTimestamp": "2024-11-05T21:33:24Z" + "creationTimestamp": "2024-11-11T11:53:24Z" }, "spec": { "description": "In-development feature that will allow injection of labels into loki queries.", @@ -2338,7 +2340,7 @@ "metadata": { "name": "lokiLabelNamesQueryApi", "resourceVersion": "1734096677730", - "creationTimestamp": "2024-12-13T13:31:17Z" + "creationTimestamp": "2024-12-13T14:31:41Z" }, "spec": { "description": "Defaults to using the Loki `/labels` API instead of `/series`", @@ -2364,7 +2366,7 @@ "name": "lokiMetricDataplane", "resourceVersion": "1720021873452", "creationTimestamp": "2023-04-13T13:07:08Z", - "deletionTimestamp": "2024-08-21T13:49:48Z", + "deletionTimestamp": "2024-11-26T16:32:17Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -2469,7 +2471,7 @@ "metadata": { "name": "lokiShardSplitting", "resourceVersion": "1729678036788", - "creationTimestamp": "2024-10-23T10:06:42Z", + "creationTimestamp": "2024-10-23T11:21:03Z", "annotations": { "grafana.app/updatedTimestamp": "2024-10-23 10:07:16.788828 +0000 UTC" } @@ -2660,7 +2662,7 @@ "name": "notificationBanner", "resourceVersion": "1727777007488", "creationTimestamp": "2024-05-13T09:32:34Z", - "deletionTimestamp": "2025-01-10T06:02:47Z", + "deletionTimestamp": "2025-01-10T10:18:43Z", "annotations": { "grafana.app/updatedTimestamp": "2024-10-01 10:03:27.48823 +0000 UTC" } @@ -2706,8 +2708,8 @@ "metadata": { "name": "onPremToCloudMigrationsAlerts", "resourceVersion": "1728048163201", - "creationTimestamp": "2024-10-04T13:22:43Z", - "deletionTimestamp": "2024-12-02T13:37:41Z" + "creationTimestamp": "2024-10-07T10:53:24Z", + "deletionTimestamp": "2024-12-17T11:56:18Z" }, "spec": { "description": "Enables the migration of alerts and its child resources to your Grafana Cloud stack. Requires `onPremToCloudMigrations` to be enabled in conjunction.", @@ -2719,7 +2721,7 @@ "metadata": { "name": "onPremToCloudMigrationsAuthApiMig", "resourceVersion": "1732033809064", - "creationTimestamp": "2024-11-19T16:30:09Z" + "creationTimestamp": "2024-11-21T18:46:06Z" }, "spec": { "description": "Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction.", @@ -2795,7 +2797,7 @@ "name": "panelTitleSearchInV1", "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-13T12:04:24Z", - "deletionTimestamp": "2025-01-20T20:07:05Z" + "deletionTimestamp": "2025-01-21T09:59:32Z" }, "spec": { "description": "Enable searching for dashboards using panel title in search v1", @@ -2809,7 +2811,7 @@ "name": "passScopeToDashboardApi", "resourceVersion": "1718290335877", "creationTimestamp": "2024-06-20T15:49:19Z", - "deletionTimestamp": "2024-10-14T10:53:41Z" + "deletionTimestamp": "2024-10-25T12:56:54Z" }, "spec": { "description": "Enables the passing of scopes to dashboards fetching in Grafana", @@ -2823,7 +2825,7 @@ "metadata": { "name": "passwordlessMagicLinkAuthentication", "resourceVersion": "1730232874003", - "creationTimestamp": "2024-10-29T20:14:34Z" + "creationTimestamp": "2024-11-14T13:50:55Z" }, "spec": { "description": "Enable passwordless login via magic link authentication", @@ -2877,7 +2879,7 @@ "metadata": { "name": "playlistsReconciler", "resourceVersion": "1734463170112", - "creationTimestamp": "2024-11-01T12:08:30Z", + "creationTimestamp": "2024-12-20T03:09:31Z", "deletionTimestamp": "2024-12-19T19:17:00Z", "annotations": { "grafana.app/updatedTimestamp": "2024-12-17 19:19:30.112629 +0000 UTC" @@ -2966,7 +2968,7 @@ "metadata": { "name": "pluginsSriChecks", "resourceVersion": "1727785264632", - "creationTimestamp": "2024-10-01T12:21:04Z" + "creationTimestamp": "2024-10-04T12:55:09Z" }, "spec": { "description": "Enables SRI checks for plugin assets", @@ -2978,7 +2980,7 @@ "metadata": { "name": "preinstallAutoUpdate", "resourceVersion": "1731581146864", - "creationTimestamp": "2024-11-06T14:45:43Z", + "creationTimestamp": "2024-11-07T12:14:25Z", "annotations": { "grafana.app/updatedTimestamp": "2024-11-14 10:45:46.864585 +0000 UTC" } @@ -3058,7 +3060,7 @@ "name": "prometheusConfigOverhaulAuth", "resourceVersion": "1720021873452", "creationTimestamp": "2023-07-26T16:09:53Z", - "deletionTimestamp": "2025-01-02T20:43:41Z", + "deletionTimestamp": "2025-01-02T21:19:11Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -3107,7 +3109,7 @@ "name": "prometheusMetricEncyclopedia", "resourceVersion": "1720021873452", "creationTimestamp": "2023-03-07T18:41:05Z", - "deletionTimestamp": "2024-12-30T14:42:45Z", + "deletionTimestamp": "2024-12-30T21:16:04Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -3156,7 +3158,7 @@ "metadata": { "name": "prometheusSpecialCharsInLabelValues", "resourceVersion": "1735845919509", - "creationTimestamp": "2024-12-12T23:52:48Z", + "creationTimestamp": "2024-12-18T21:31:08Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-02 19:25:19.509884 +0000 UTC" } @@ -3172,7 +3174,7 @@ "metadata": { "name": "prometheusUsesCombobox", "resourceVersion": "1735845919509", - "creationTimestamp": "2024-09-12T11:19:18Z", + "creationTimestamp": "2024-10-23T11:18:33Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-02 19:25:19.509884 +0000 UTC" } @@ -3187,7 +3189,7 @@ "metadata": { "name": "provisioning", "resourceVersion": "1732265054297", - "creationTimestamp": "2024-11-22T08:44:14Z" + "creationTimestamp": "2024-11-22T09:03:50Z" }, "spec": { "description": "Next generation provisioning... and git", @@ -3201,7 +3203,7 @@ "name": "publicDashboards", "resourceVersion": "1720021873452", "creationTimestamp": "2022-04-07T18:30:19Z", - "deletionTimestamp": "2024-11-15T16:38:53Z", + "deletionTimestamp": "2024-11-20T14:36:19Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -3265,7 +3267,7 @@ "metadata": { "name": "queryLibraryDashboards", "resourceVersion": "1736850377404", - "creationTimestamp": "2025-01-14T10:24:54Z", + "creationTimestamp": "2025-01-14T11:01:15Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-14 10:26:17.404592 +0000 UTC" } @@ -3394,7 +3396,7 @@ "metadata": { "name": "reloadDashboardsOnParamsChange", "resourceVersion": "1728903221522", - "creationTimestamp": "2024-10-14T10:53:41Z" + "creationTimestamp": "2024-10-25T12:56:54Z" }, "spec": { "description": "Enables reload of dashboards on scopes, time range and variables changes", @@ -3434,7 +3436,7 @@ "metadata": { "name": "reportingUseRawTimeRange", "resourceVersion": "1735810729877", - "creationTimestamp": "2024-11-13T15:48:28Z", + "creationTimestamp": "2024-11-14T20:08:03Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-02 09:38:49.877519888 +0000 UTC" } @@ -3450,7 +3452,7 @@ "metadata": { "name": "rolePickerDrawer", "resourceVersion": "1727337187819", - "creationTimestamp": "2024-09-26T07:53:07Z" + "creationTimestamp": "2024-09-26T12:51:38Z" }, "spec": { "description": "Enables the new role picker drawer design", @@ -3476,7 +3478,7 @@ "metadata": { "name": "scopeApi", "resourceVersion": "1732690644377", - "creationTimestamp": "2024-11-27T06:57:24Z" + "creationTimestamp": "2024-11-27T07:58:25Z" }, "spec": { "description": "In-development feature flag for the scope api using the app platform.", @@ -3517,7 +3519,7 @@ "name": "singleTopNav", "resourceVersion": "1732104041490", "creationTimestamp": "2024-08-29T08:48:32Z", - "deletionTimestamp": "2024-12-13T11:25:25Z", + "deletionTimestamp": "2024-12-17T13:32:38Z", "annotations": { "grafana.app/updatedTimestamp": "2024-11-20 12:00:41.490792 +0000 UTC" } @@ -3560,7 +3562,7 @@ "metadata": { "name": "sqlQuerybuilderFunctionParameters", "resourceVersion": "1718487716739", - "creationTimestamp": "2024-06-15T21:41:56Z" + "creationTimestamp": "2024-11-04T16:13:35Z" }, "spec": { "description": "Enables SQL query builder function parameters", @@ -3676,7 +3678,7 @@ "metadata": { "name": "teamHttpHeadersMimir", "resourceVersion": "1736763800062", - "creationTimestamp": "2025-01-13T10:23:20Z" + "creationTimestamp": "2025-01-13T10:42:47Z" }, "spec": { "description": "Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams", @@ -3688,7 +3690,7 @@ "metadata": { "name": "timeRangeProvider", "resourceVersion": "1728565214224", - "creationTimestamp": "2024-10-10T13:00:14Z" + "creationTimestamp": "2024-10-22T10:52:33Z" }, "spec": { "description": "Enables time pickers sync", @@ -3717,7 +3719,7 @@ "name": "topnav", "resourceVersion": "1720021873452", "creationTimestamp": "2022-06-20T14:25:43Z", - "deletionTimestamp": "2024-10-15T15:00:51Z", + "deletionTimestamp": "2024-10-17T09:18:30Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -3785,7 +3787,7 @@ "metadata": { "name": "unifiedHistory", "resourceVersion": "1734085219453", - "creationTimestamp": "2024-12-13T10:20:19Z" + "creationTimestamp": "2024-12-13T10:41:18Z" }, "spec": { "description": "Displays the navigation history so the user can navigate back to previous pages", @@ -3845,7 +3847,7 @@ "metadata": { "name": "unifiedStorageBigObjectsSupport", "resourceVersion": "1728994158474", - "creationTimestamp": "2024-10-15T12:09:18Z" + "creationTimestamp": "2024-10-17T10:18:29Z" }, "spec": { "description": "Enables to save big objects in blob storage", @@ -3857,7 +3859,7 @@ "metadata": { "name": "unifiedStorageSearch", "resourceVersion": "1726771421439", - "creationTimestamp": "2024-09-19T18:43:41Z" + "creationTimestamp": "2024-09-30T19:46:14Z" }, "spec": { "description": "Enable unified storage search", @@ -3871,7 +3873,7 @@ "metadata": { "name": "unifiedStorageSearch", "resourceVersion": "1726771421439", - "creationTimestamp": "2024-09-19T18:43:41Z", + "creationTimestamp": "2024-09-30T19:46:14Z", "deletionTimestamp": "2024-10-11T14:56:04Z" }, "spec": { @@ -3886,7 +3888,7 @@ "metadata": { "name": "unifiedStorageSearchPermissionFiltering", "resourceVersion": "1737489629408", - "creationTimestamp": "2025-01-21T20:00:29Z" + "creationTimestamp": "2025-01-22T11:38:37Z" }, "spec": { "description": "Enable permission filtering on unified storage search", @@ -3900,7 +3902,7 @@ "metadata": { "name": "unifiedStorageSearchSprinkles", "resourceVersion": "1734563607668", - "creationTimestamp": "2024-12-18T23:13:27Z" + "creationTimestamp": "2024-12-18T17:00:54Z" }, "spec": { "description": "Enable sprinkles on unified storage search", @@ -3914,7 +3916,7 @@ "metadata": { "name": "unifiedStorageSearchUI", "resourceVersion": "1734563607668", - "creationTimestamp": "2024-12-10T21:28:55Z", + "creationTimestamp": "2024-12-19T18:21:48Z", "annotations": { "grafana.app/updatedTimestamp": "2024-12-18 23:13:27.66802 +0000 UTC" } @@ -3960,7 +3962,7 @@ "metadata": { "name": "useV2DashboardsAPI", "resourceVersion": "1732535420861", - "creationTimestamp": "2024-11-25T11:50:20Z" + "creationTimestamp": "2024-12-17T21:17:09Z" }, "spec": { "description": "Use the v2 kubernetes API in the frontend for dashboards", @@ -3973,7 +3975,7 @@ "metadata": { "name": "userStorageAPI", "resourceVersion": "1736438999910", - "creationTimestamp": "2024-10-29T12:18:41Z", + "creationTimestamp": "2024-11-12T11:56:41Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-09 16:09:59.910083 +0000 UTC" } @@ -4004,7 +4006,7 @@ "name": "vizAndWidgetSplit", "resourceVersion": "1718727528075", "creationTimestamp": "2023-06-27T10:22:13Z", - "deletionTimestamp": "2024-10-30T14:21:33Z" + "deletionTimestamp": "2024-10-30T16:12:03Z" }, "spec": { "description": "Split panels between visualizations and widgets", @@ -4043,7 +4045,7 @@ "metadata": { "name": "zipkinBackendMigration", "resourceVersion": "1733846643829", - "creationTimestamp": "2024-11-06T15:39:38Z", + "creationTimestamp": "2024-11-07T09:35:53Z", "annotations": { "grafana.app/updatedTimestamp": "2024-12-10 16:04:03.82919 +0000 UTC" } diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 8d86292c758..85664cda390 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -11,12 +11,13 @@ import ( "sync" "time" - "github.com/grafana/dskit/concurrency" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "golang.org/x/exp/slices" + "github.com/grafana/dskit/concurrency" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/bus" @@ -315,10 +316,6 @@ func (s *Service) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*fol f.FullpathUIDs = f.UID // set full path to the folder UID } - if s.features.IsEnabled(ctx, featuremgmt.FlagKubernetesFolders) { - f, err = s.setFullpath(ctx, f, q.SignedInUser, true) - } - return f, err } @@ -784,13 +781,6 @@ func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderComm f.ParentUID = nestedFolder.ParentUID } - if s.features.IsEnabled(ctx, featuremgmt.FlagKubernetesFolders) { - f, err = s.setFullpath(ctx, f, user, true) - if err != nil { - return nil, err - } - } - return f, nil } diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 5ec969918c4..98b713cf0fa 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -200,7 +200,6 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { } featuresArr := []any{ - featuremgmt.FlagKubernetesFolders, featuremgmt.FlagKubernetesFoldersServiceV2} features := featuremgmt.WithFeatures(featuresArr...) diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index 2fe65677c64..4b30d9428cf 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -49,10 +49,8 @@ func TestIntegrationFoldersApp(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ AppModeProduction: true, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, }, - // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). - // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). }) t.Run("Check discovery client", func(t *testing.T) { @@ -125,10 +123,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, }, - // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). - // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). })) }) @@ -143,10 +139,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, }, - // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). - // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). })) }) @@ -161,9 +155,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, }, })) }) @@ -179,9 +172,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, }, })) }) @@ -197,9 +189,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, }, })) }) @@ -215,9 +206,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, }, })) }) @@ -421,7 +411,7 @@ func doNestedCreateTest(t *testing.T, helper *apis.K8sTestHelper) { // creating a folder with a known parent should succeed require.Equal(t, parentUID, childCreate.Result.ParentUID) require.Equal(t, parentUID, parent.UID) - require.Equal(t, "Test\\/parent", parent.Title) + require.Equal(t, "Test/parent", parent.Title) require.Equal(t, parentCreate.Result.URL, parent.URL) } @@ -502,6 +492,7 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + t.Skip("not working yet") folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}" folderWithParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}" @@ -585,9 +576,8 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagKubernetesFoldersServiceV2, }, }) @@ -627,6 +617,7 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + t.Skip("not yet working") type testCase struct { description string @@ -687,9 +678,8 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagKubernetesFoldersServiceV2, }, }) @@ -865,9 +855,8 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagKubernetesFoldersServiceV2, }, }) @@ -1036,9 +1025,8 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagKubernetesFoldersServiceV2, }, }) From f6202f59d48eb0bb012b7009b5e0e95cd0b1db14 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:38:27 +0000 Subject: [PATCH 022/894] Update dependency rollup to v4.31.0 (#99449) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 158 +++++++++++++++++++++++++++--------------------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/yarn.lock b/yarn.lock index 96111fd4d26..1f0d402feb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6551,135 +6551,135 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.28.1" +"@rollup/rollup-android-arm-eabi@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.31.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-android-arm64@npm:4.28.1" +"@rollup/rollup-android-arm64@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-android-arm64@npm:4.31.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-darwin-arm64@npm:4.28.1" +"@rollup/rollup-darwin-arm64@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.31.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-darwin-x64@npm:4.28.1" +"@rollup/rollup-darwin-x64@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.31.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.28.1" +"@rollup/rollup-freebsd-arm64@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.31.0" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-freebsd-x64@npm:4.28.1" +"@rollup/rollup-freebsd-x64@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.31.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.28.1" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.31.0" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.28.1" +"@rollup/rollup-linux-arm-musleabihf@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.31.0" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.28.1" +"@rollup/rollup-linux-arm64-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.31.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.28.1" +"@rollup/rollup-linux-arm64-musl@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.31.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.28.1" +"@rollup/rollup-linux-loongarch64-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.31.0" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.28.1" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.31.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.28.1" +"@rollup/rollup-linux-riscv64-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.31.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.28.1" +"@rollup/rollup-linux-s390x-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.31.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.28.1" +"@rollup/rollup-linux-x64-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.31.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.28.1" +"@rollup/rollup-linux-x64-musl@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.31.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.28.1" +"@rollup/rollup-win32-arm64-msvc@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.31.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.28.1" +"@rollup/rollup-win32-ia32-msvc@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.31.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.28.1" +"@rollup/rollup-win32-x64-msvc@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.31.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -26662,28 +26662,28 @@ __metadata: linkType: hard "rollup@npm:^4.22.4": - version: 4.28.1 - resolution: "rollup@npm:4.28.1" + version: 4.31.0 + resolution: "rollup@npm:4.31.0" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.28.1" - "@rollup/rollup-android-arm64": "npm:4.28.1" - "@rollup/rollup-darwin-arm64": "npm:4.28.1" - "@rollup/rollup-darwin-x64": "npm:4.28.1" - "@rollup/rollup-freebsd-arm64": "npm:4.28.1" - "@rollup/rollup-freebsd-x64": "npm:4.28.1" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.28.1" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.28.1" - "@rollup/rollup-linux-arm64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-arm64-musl": "npm:4.28.1" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.28.1" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-s390x-gnu": "npm:4.28.1" - "@rollup/rollup-linux-x64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-x64-musl": "npm:4.28.1" - "@rollup/rollup-win32-arm64-msvc": "npm:4.28.1" - "@rollup/rollup-win32-ia32-msvc": "npm:4.28.1" - "@rollup/rollup-win32-x64-msvc": "npm:4.28.1" + "@rollup/rollup-android-arm-eabi": "npm:4.31.0" + "@rollup/rollup-android-arm64": "npm:4.31.0" + "@rollup/rollup-darwin-arm64": "npm:4.31.0" + "@rollup/rollup-darwin-x64": "npm:4.31.0" + "@rollup/rollup-freebsd-arm64": "npm:4.31.0" + "@rollup/rollup-freebsd-x64": "npm:4.31.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.31.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.31.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.31.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.31.0" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.31.0" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.31.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.31.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.31.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.31.0" + "@rollup/rollup-linux-x64-musl": "npm:4.31.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.31.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.31.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.31.0" "@types/estree": "npm:1.0.6" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -26729,7 +26729,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/4337898d07e646835b52494b43b4ccd6929da87af2b0febc05ab217fd2425cfda05af5efaea6037c1641c90d803eb5b3e491eefdd47b28fda85af4f46a0dad34 + checksum: 10/4f5fac0a0df7878ca810512c283df0e81b21d42fed262943b412c488a30beceb0149a4be36dbf2750b6c5cbfa4d4cf5097a134266f1425a9e213c2a2a09853fc languageName: node linkType: hard From 5ca24fde024af57c04682f4d1c0bccd05af2b783 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:48:06 +0100 Subject: [PATCH 023/894] AutoSizeInput: Improve performance when typing (#99443) * AutoSizeInput: Fix performance issue * Add comments * Fix a little oopsie --- packages/grafana-ui/src/components/Input/Input.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx index 1240569cb57..09b113cf76b 100644 --- a/packages/grafana-ui/src/components/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -64,12 +64,17 @@ export const Input = forwardRef((props, ref) => { const theme = useTheme2(); - const styles = getInputStyles({ theme, invalid: !!invalid, width: finalWidth }); + // Don't pass the width prop, as this causes an unnecessary amount of Emotion calls when auto sizing + const styles = getInputStyles({ theme, invalid: !!invalid }); const suffix = suffixProp || (loading && ); return ( -
+
{!!addonBefore &&
{addonBefore}
}
{prefix && ( @@ -125,7 +130,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }: css({ label: 'input-wrapper', display: 'flex', - width: width ? theme.spacing(width) : '100%', + width: width ? theme.spacing(width) : '100%', // Not used in Input, as this causes performance issues with auto sizing height: theme.spacing(theme.components.height.md), borderRadius: theme.shape.radius.default, '&:hover': { From 05015a57b3de236b672221b6ddb09063117b9c7a Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 23 Jan 2025 15:52:03 +0100 Subject: [PATCH 024/894] Chore: Upgrade authlib (#99447) --- go.mod | 2 +- go.sum | 4 ++-- pkg/apimachinery/go.mod | 2 +- pkg/apimachinery/go.sum | 4 ++-- pkg/storage/unified/apistore/go.mod | 2 +- pkg/storage/unified/apistore/go.sum | 4 ++-- pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/go.sum | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index e4140a19173..d57b3cb3d61 100644 --- a/go.mod +++ b/go.mod @@ -70,7 +70,7 @@ 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-20250117230852-a5e8136407d4 // @grafana/alerting-backend - github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @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 diff --git a/go.sum b/go.sum index 2198c1a9589..166125e556b 100644 --- a/go.sum +++ b/go.sum @@ -1500,8 +1500,8 @@ 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-20250117230852-a5e8136407d4 h1:616HUg7WVyLJLtVvk2pZ845M4Gk0fswgTwEZSuAZbJU= github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c h1:duHQ8Bih3nt9p68aQdp+22a6mFBLpK4IOURhEq+Cvk8= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 12e69bb5cd9..3c8273750a3 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery go 1.23.1 require ( - github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team github.com/stretchr/testify v1.10.0 k8s.io/apimachinery v0.32.0 diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 1dc1eff7033..5957167466c 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -32,8 +32,8 @@ 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-20250120145936-5f0e28e7a87c h1:duHQ8Bih3nt9p68aQdp+22a6mFBLpK4IOURhEq+Cvk8= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 1936abf4697..bb4398f1a66 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -171,7 +171,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4 // indirect - github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c // indirect + github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // 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.29.0 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index b82db37d6e4..478fa431e69 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -549,8 +549,8 @@ 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-20250117230852-a5e8136407d4 h1:616HUg7WVyLJLtVvk2pZ845M4Gk0fswgTwEZSuAZbJU= github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c h1:duHQ8Bih3nt9p68aQdp+22a6mFBLpK4IOURhEq+Cvk8= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 554dbe9c4ca..8f0adf19785 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -11,7 +11,7 @@ replace ( require ( github.com/fullstorydev/grpchan v1.1.1 github.com/google/uuid v1.6.0 - github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c + github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 65f2d1de918..ef047967788 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -405,8 +405,8 @@ 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-20250117230852-a5e8136407d4 h1:616HUg7WVyLJLtVvk2pZ845M4Gk0fswgTwEZSuAZbJU= github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c h1:duHQ8Bih3nt9p68aQdp+22a6mFBLpK4IOURhEq+Cvk8= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= From 7d2eb83cbdbe362f0d473c3019cd75c9ecc6a338 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:11:05 +0000 Subject: [PATCH 025/894] Update dependency @playwright/test to v1.50.0 (#99452) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 21488de372d..d43c76f4903 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@grafana/plugin-e2e": "^1.11.0", "@grafana/tsconfig": "^2.0.0", "@manypkg/get-packages": "^2.2.0", - "@playwright/test": "1.49.1", + "@playwright/test": "1.50.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", "@react-types/button": "3.10.2", "@react-types/menu": "3.9.14", diff --git a/yarn.lock b/yarn.lock index 1f0d402feb9..765a1f74a0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5929,14 +5929,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:1.49.1": - version: 1.49.1 - resolution: "@playwright/test@npm:1.49.1" +"@playwright/test@npm:1.50.0": + version: 1.50.0 + resolution: "@playwright/test@npm:1.50.0" dependencies: - playwright: "npm:1.49.1" + playwright: "npm:1.50.0" bin: playwright: cli.js - checksum: 10/bb0d5eda58ee0b5bbca732d2aa57782fadf420d101e08e16d5760179459c667907bd8d224ee3d6f43f3088378e377ef63d32ed605fec37605debf217c3efe8da + checksum: 10/1fec2ed986205b57b03f24392bb01c6454c1f0a5c14204ce921afd51c3f5d61f20eddb3a18d36a02b19b3e3d731c7ff6bb7ba3c622aabc8fa3802021aef7d21b languageName: node linkType: hard @@ -17388,7 +17388,7 @@ __metadata: "@opentelemetry/api": "npm:1.9.0" "@opentelemetry/exporter-collector": "npm:0.25.0" "@opentelemetry/semantic-conventions": "npm:1.28.0" - "@playwright/test": "npm:1.49.1" + "@playwright/test": "npm:1.50.0" "@pmmmwh/react-refresh-webpack-plugin": "npm:0.5.15" "@popperjs/core": "npm:2.11.8" "@react-aria/dialog": "npm:3.5.21" @@ -23732,27 +23732,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.49.1": - version: 1.49.1 - resolution: "playwright-core@npm:1.49.1" +"playwright-core@npm:1.50.0": + version: 1.50.0 + resolution: "playwright-core@npm:1.50.0" bin: playwright-core: cli.js - checksum: 10/baa39a53024ec7744708410f2b952ac3aa2e1a6d311dabfa303523712848eba142fce5c20f1b2ed2a66fbd9a415d22ea8642b0f70423360aaebd4b41c47d364e + checksum: 10/0d27e52164bcc37ed5aeaa0c7efa6a0b3616cfbb01e206c26572bff8b8e5f0923a993369c826056cd7bee4b975508a1ec257b533098ee9db7bc5b75832110e4d languageName: node linkType: hard -"playwright@npm:1.49.1": - version: 1.49.1 - resolution: "playwright@npm:1.49.1" +"playwright@npm:1.50.0": + version: 1.50.0 + resolution: "playwright@npm:1.50.0" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.49.1" + playwright-core: "npm:1.50.0" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10/49fb063f4a107b8090f66d2d351ebd51fbb66843a8f95a161fa0c0e0b5156515961e75cc10f4249f61b9d2af51f762dda505c62b096d8f61cd47d1ff73ab39d2 + checksum: 10/53521f05c48ab51a37d6fa280a7c1e6486e2879f9997e877227517945faf195ce16829cf144709bba292c3023bcd07cf44a4dd965458c9adc30ea6fbe1f0f74a languageName: node linkType: hard From b066a6313173405eb865648804518092051043b7 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 23 Jan 2025 16:19:50 +0100 Subject: [PATCH 026/894] Grafana Advisor: Datasource checks (#99313) --- apps/advisor/kinds/check.cue | 6 + apps/advisor/pkg/apis/advisor_manifest.go | 10 +- apps/advisor/pkg/app/app.go | 46 +++++++ .../pkg/app/checkregistry/checkregistry.go | 42 ++++++ .../pkg/app/checks/datasourcecheck/check.go | 96 ++++++++++++++ .../app/checks/datasourcecheck/check_test.go | 114 ++++++++++++++++ apps/advisor/pkg/app/checks/ifaces.go | 13 ++ apps/advisor/pkg/app/utils.go | 95 ++++++++++++++ apps/advisor/pkg/app/utils_test.go | 124 ++++++++++++++++++ pkg/registry/apps/advisor/register.go | 6 +- pkg/registry/apps/wireset.go | 3 + 11 files changed, 553 insertions(+), 2 deletions(-) create mode 100644 apps/advisor/pkg/app/checkregistry/checkregistry.go create mode 100644 apps/advisor/pkg/app/checks/datasourcecheck/check.go create mode 100644 apps/advisor/pkg/app/checks/datasourcecheck/check_test.go create mode 100644 apps/advisor/pkg/app/checks/ifaces.go create mode 100644 apps/advisor/pkg/app/utils.go create mode 100644 apps/advisor/pkg/app/utils_test.go diff --git a/apps/advisor/kinds/check.cue b/apps/advisor/kinds/check.cue index 4ab33fb05df..cf1cfd58d39 100644 --- a/apps/advisor/kinds/check.cue +++ b/apps/advisor/kinds/check.cue @@ -10,6 +10,12 @@ check: { frontend: false backend: true } + validation: { + operations: [ + "CREATE", + "UPDATE", + ] + } schema: { spec: { // Generic data input that a check can receive diff --git a/apps/advisor/pkg/apis/advisor_manifest.go b/apps/advisor/pkg/apis/advisor_manifest.go index 2e8bb06fb65..f62ea3addb8 100644 --- a/apps/advisor/pkg/apis/advisor_manifest.go +++ b/apps/advisor/pkg/apis/advisor_manifest.go @@ -27,7 +27,15 @@ var appManifestData = app.ManifestData{ Conversion: false, Versions: []app.ManifestKindVersion{ { - Name: "v0alpha1", + Name: "v0alpha1", + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, Schema: &versionSchemaCheckv0alpha1, }, }, diff --git a/apps/advisor/pkg/app/app.go b/apps/advisor/pkg/app/app.go index e4ccafbcaaf..42848758d5a 100644 --- a/apps/advisor/pkg/app/app.go +++ b/apps/advisor/pkg/app/app.go @@ -2,16 +2,44 @@ package app import ( "context" + "fmt" "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/k8s" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" ) +const ( + typeLabel = "advisor.grafana.app/type" + statusAnnotation = "advisor.grafana.app/status" +) + func New(cfg app.Config) (app.App, error) { + // Read config + checkRegistry, ok := cfg.SpecificConfig.(checkregistry.CheckService) + if !ok { + return nil, fmt.Errorf("invalid config type") + } + + // Prepare storage client + clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{}) + client, err := clientGenerator.ClientFor(advisorv0alpha1.CheckKind()) + if err != nil { + return nil, err + } + + // Initialize checks + checkMap := map[string]checks.Check{} + for _, c := range checkRegistry.Checks() { + checkMap[c.Type()] = c + } + simpleConfig := simple.AppConfig{ Name: "advisor", KubeConfig: cfg.KubeConfig, @@ -23,6 +51,24 @@ func New(cfg app.Config) (app.App, error) { ManagedKinds: []simple.AppManagedKind{ { Kind: advisorv0alpha1.CheckKind(), + Validator: &simple.Validator{ + ValidateFunc: func(ctx context.Context, req *app.AdmissionRequest) error { + if req.Object != nil { + _, err := getCheck(req.Object, checkMap) + return err + } + return nil + }, + }, + Watcher: &simple.Watcher{ + AddFunc: func(ctx context.Context, obj resource.Object) error { + check, err := getCheck(obj, checkMap) + if err != nil { + return err + } + return processCheck(ctx, client, obj, check) + }, + }, }, }, } diff --git a/apps/advisor/pkg/app/checkregistry/checkregistry.go b/apps/advisor/pkg/app/checkregistry/checkregistry.go new file mode 100644 index 00000000000..0c375f035c5 --- /dev/null +++ b/apps/advisor/pkg/app/checkregistry/checkregistry.go @@ -0,0 +1,42 @@ +package checkregistry + +import ( + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks/datasourcecheck" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry/apis/datasource" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" +) + +type CheckService interface { + Checks() []checks.Check +} + +type Service struct { + datasourceSvc datasources.DataSourceService + pluginStore pluginstore.Store + pluginContextProvider datasource.PluginContextWrapper + pluginClient plugins.Client +} + +func ProvideService(datasourceSvc datasources.DataSourceService, pluginStore pluginstore.Store, + pluginContextProvider datasource.PluginContextWrapper, pluginClient plugins.Client) *Service { + return &Service{ + datasourceSvc: datasourceSvc, + pluginStore: pluginStore, + pluginContextProvider: pluginContextProvider, + pluginClient: pluginClient, + } +} + +func (s *Service) Checks() []checks.Check { + return []checks.Check{ + datasourcecheck.New( + s.datasourceSvc, + s.pluginStore, + s.pluginContextProvider, + s.pluginClient, + ), + } +} diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go new file mode 100644 index 00000000000..7aede0c65ec --- /dev/null +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -0,0 +1,96 @@ +package datasourcecheck + +import ( + "context" + "fmt" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry/apis/datasource" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" + "github.com/grafana/grafana/pkg/util" + "k8s.io/klog/v2" +) + +func New( + datasourceSvc datasources.DataSourceService, + pluginStore pluginstore.Store, + pluginContextProvider datasource.PluginContextWrapper, + pluginClient plugins.Client, +) checks.Check { + return &check{ + DatasourceSvc: datasourceSvc, + PluginStore: pluginStore, + PluginContextProvider: pluginContextProvider, + PluginClient: pluginClient, + } +} + +type check struct { + DatasourceSvc datasources.DataSourceService + PluginStore pluginstore.Store + PluginContextProvider datasource.PluginContextWrapper + PluginClient plugins.Client +} + +func (c *check) Type() string { + return "datasource" +} + +func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.CheckV0alpha1StatusReport, error) { + // Optionally read the check input encoded in the object + // fmt.Println(obj.Data) + + dss, err := c.DatasourceSvc.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{}) + if err != nil { + return nil, err + } + + dsErrs := []advisor.CheckV0alpha1StatusReportErrors{} + for _, ds := range dss { + // Data source UID validation + err := util.ValidateUID(ds.UID) + if err != nil { + dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{ + Severity: advisor.CheckStatusSeverityLow, + Reason: fmt.Sprintf("Invalid UID: %s", ds.UID), + Action: "Change UID", + }) + } + + // Health check execution + pCtx, err := c.PluginContextProvider.PluginContextForDataSource(ctx, &backend.DataSourceInstanceSettings{ + Type: ds.Type, + UID: ds.UID, + APIVersion: ds.APIVersion, + }) + if err != nil { + klog.ErrorS(err, "Error creating plugin context", "datasource", ds.Name) + continue + } + req := &backend.CheckHealthRequest{ + PluginContext: pCtx, + Headers: map[string]string{}, + } + resp, err := c.PluginClient.CheckHealth(ctx, req) + if err != nil { + fmt.Println("Error checking health", err) + continue + } + if resp.Status != backend.HealthStatusOk { + dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{ + Severity: advisor.CheckStatusSeverityHigh, + Reason: fmt.Sprintf("Health check failed: %s", ds.Name), + Action: "Check datasource", + }) + } + } + + return &advisor.CheckV0alpha1StatusReport{ + Count: int64(len(dss)), + Errors: dsErrs, + }, nil +} diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go new file mode 100644 index 00000000000..ee489483bd2 --- /dev/null +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go @@ -0,0 +1,114 @@ +package datasourcecheck + +import ( + "context" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry/apis/datasource" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/stretchr/testify/assert" +) + +func TestCheck_Run(t *testing.T) { + t.Run("should return no errors when all datasources are healthy", func(t *testing.T) { + datasources := []*datasources.DataSource{ + {UID: "valid-uid-1", Type: "prometheus", Name: "Prometheus"}, + {UID: "valid-uid-2", Type: "mysql", Name: "MySQL"}, + } + + mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} + mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} + + check := &check{ + DatasourceSvc: mockDatasourceSvc, + PluginContextProvider: mockPluginContextProvider, + PluginClient: mockPluginClient, + } + + report, err := check.Run(context.Background(), &advisor.CheckSpec{}) + + assert.NoError(t, err) + assert.Equal(t, int64(2), report.Count) + assert.Empty(t, report.Errors) + }) + + t.Run("should return errors when datasource UID is invalid", func(t *testing.T) { + datasources := []*datasources.DataSource{ + {UID: "invalid uid", Type: "prometheus", Name: "Prometheus"}, + } + + mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} + mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} + + check := &check{ + DatasourceSvc: mockDatasourceSvc, + PluginContextProvider: mockPluginContextProvider, + PluginClient: mockPluginClient, + } + + report, err := check.Run(context.Background(), &advisor.CheckSpec{}) + + assert.NoError(t, err) + assert.Equal(t, int64(1), report.Count) + assert.Len(t, report.Errors, 1) + assert.Equal(t, "Invalid UID: invalid uid", report.Errors[0].Reason) + }) + + t.Run("should return errors when datasource health check fails", func(t *testing.T) { + datasources := []*datasources.DataSource{ + {UID: "valid-uid-1", Type: "prometheus", Name: "Prometheus"}, + } + + mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} + mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusError}} + + check := &check{ + DatasourceSvc: mockDatasourceSvc, + PluginContextProvider: mockPluginContextProvider, + PluginClient: mockPluginClient, + } + + report, err := check.Run(context.Background(), &advisor.CheckSpec{}) + + assert.NoError(t, err) + assert.Equal(t, int64(1), report.Count) + assert.Len(t, report.Errors, 1) + assert.Equal(t, "Health check failed: Prometheus", report.Errors[0].Reason) + }) +} + +type MockDatasourceSvc struct { + datasources.DataSourceService + + dss []*datasources.DataSource +} + +func (m *MockDatasourceSvc) GetAllDataSources(ctx context.Context, query *datasources.GetAllDataSourcesQuery) ([]*datasources.DataSource, error) { + return m.dss, nil +} + +type MockPluginContextProvider struct { + datasource.PluginContextWrapper + + pCtx backend.PluginContext +} + +func (m *MockPluginContextProvider) PluginContextForDataSource(ctx context.Context, datasourceSettings *backend.DataSourceInstanceSettings) (backend.PluginContext, error) { + return m.pCtx, nil +} + +type MockPluginClient struct { + plugins.Client + + res *backend.CheckHealthResult +} + +func (m *MockPluginClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + return m.res, nil +} diff --git a/apps/advisor/pkg/app/checks/ifaces.go b/apps/advisor/pkg/app/checks/ifaces.go new file mode 100644 index 00000000000..008f5656298 --- /dev/null +++ b/apps/advisor/pkg/app/checks/ifaces.go @@ -0,0 +1,13 @@ +package checks + +import ( + "context" + + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" +) + +// Check defines the methods that a check must implement to be executed. +type Check interface { + Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec) (*advisorv0alpha1.CheckV0alpha1StatusReport, error) + Type() string +} diff --git a/apps/advisor/pkg/app/utils.go b/apps/advisor/pkg/app/utils.go new file mode 100644 index 00000000000..4eae090198b --- /dev/null +++ b/apps/advisor/pkg/app/utils.go @@ -0,0 +1,95 @@ +package app + +import ( + "context" + "errors" + "fmt" + + claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/resource" + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/user" +) + +func getCheck(obj resource.Object, checks map[string]checks.Check) (checks.Check, error) { + labels := obj.GetLabels() + objTypeLabel, ok := labels[typeLabel] + if !ok { + return nil, errors.New("missing check type as label") + } + c, ok := checks[objTypeLabel] + if !ok { + supportedTypes := "" + for k := range checks { + supportedTypes += k + ", " + } + return nil, fmt.Errorf("unknown check type %s. Supported types are: %s", objTypeLabel, supportedTypes) + } + + return c, nil +} + +func getStatusAnnotation(obj resource.Object) string { + return obj.GetAnnotations()[statusAnnotation] +} + +func setStatusAnnotation(ctx context.Context, client resource.Client, obj resource.Object, status string) error { + annotations := obj.GetAnnotations() + annotations[statusAnnotation] = status + return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ + Operations: []resource.PatchOperation{{ + Operation: resource.PatchOpAdd, + Path: "/metadata/annotations", + Value: annotations, + }}, + }, resource.PatchOptions{}, obj) +} + +func processCheck(ctx context.Context, client resource.Client, obj resource.Object, check checks.Check) error { + status := getStatusAnnotation(obj) + if status != "" { + // Check already processed + return nil + } + c, ok := obj.(*advisorv0alpha1.Check) + if !ok { + return fmt.Errorf("invalid object type") + } + // Populate ctx with the user that created the check + meta, err := utils.MetaAccessor(obj) + if err != nil { + return err + } + createdBy := meta.GetCreatedBy() + typ, uid, err := claims.ParseTypeID(createdBy) + if err != nil { + return err + } + ctx = identity.WithRequester(ctx, &user.SignedInUser{ + UserUID: uid, + FallbackType: typ, + }) + // Run the checks + report, err := check.Run(ctx, &c.Spec) + if err != nil { + setErr := setStatusAnnotation(ctx, client, obj, "error") + if setErr != nil { + return setErr + } + return err + } + err = setStatusAnnotation(ctx, client, obj, "processed") + if err != nil { + return err + } + return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ + Operations: []resource.PatchOperation{{ + Operation: resource.PatchOpAdd, + Path: "/status/report", + Value: *report, + }}, + }, resource.PatchOptions{}, obj) +} diff --git a/apps/advisor/pkg/app/utils_test.go b/apps/advisor/pkg/app/utils_test.go new file mode 100644 index 00000000000..6626f645eb7 --- /dev/null +++ b/apps/advisor/pkg/app/utils_test.go @@ -0,0 +1,124 @@ +package app + +import ( + "context" + "errors" + "testing" + + "github.com/grafana/grafana-app-sdk/resource" + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/stretchr/testify/assert" +) + +func TestGetCheck(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetLabels(map[string]string{typeLabel: "testType"}) + + checkMap := map[string]checks.Check{ + "testType": &mockCheck{}, + } + + check, err := getCheck(obj, checkMap) + assert.NoError(t, err) + assert.NotNil(t, check) +} + +func TestGetCheck_MissingLabel(t *testing.T) { + obj := &advisorv0alpha1.Check{} + checkMap := map[string]checks.Check{} + + _, err := getCheck(obj, checkMap) + assert.Error(t, err) + assert.Equal(t, "missing check type as label", err.Error()) +} + +func TestGetCheck_UnknownType(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetLabels(map[string]string{typeLabel: "unknownType"}) + + checkMap := map[string]checks.Check{ + "testType": &mockCheck{}, + } + + _, err := getCheck(obj, checkMap) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown check type unknownType") +} + +func TestSetStatusAnnotation(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{}) + client := &mockClient{} + ctx := context.TODO() + + err := setStatusAnnotation(ctx, client, obj, "processed") + assert.NoError(t, err) + assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) +} + +func TestProcessCheck(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{}) + meta, err := utils.MetaAccessor(obj) + if err != nil { + t.Fatal(err) + } + meta.SetCreatedBy("user:1") + client := &mockClient{} + ctx := context.TODO() + check := &mockCheck{} + + err = processCheck(ctx, client, obj, check) + assert.NoError(t, err) + assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) +} + +func TestProcessCheck_AlreadyProcessed(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{statusAnnotation: "processed"}) + client := &mockClient{} + ctx := context.TODO() + check := &mockCheck{} + + err := processCheck(ctx, client, obj, check) + assert.NoError(t, err) +} + +func TestProcessCheck_RunError(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{}) + meta, err := utils.MetaAccessor(obj) + if err != nil { + t.Fatal(err) + } + meta.SetCreatedBy("user:1") + client := &mockClient{} + ctx := context.TODO() + + check := &mockCheck{ + err: errors.New("run error"), + } + + err = processCheck(ctx, client, obj, check) + assert.Error(t, err) + assert.Equal(t, "error", obj.GetAnnotations()[statusAnnotation]) +} + +type mockClient struct { + resource.Client +} + +func (m *mockClient) PatchInto(ctx context.Context, id resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions, obj resource.Object) error { + return nil +} + +type mockCheck struct { + checks.Check + err error +} + +func (m *mockCheck) Run(ctx context.Context, spec *advisorv0alpha1.CheckSpec) (*advisorv0alpha1.CheckV0alpha1StatusReport, error) { + return &advisorv0alpha1.CheckV0alpha1StatusReport{}, m.err +} diff --git a/pkg/registry/apps/advisor/register.go b/pkg/registry/apps/advisor/register.go index 1e9c8b30f53..a680dcc0880 100644 --- a/pkg/registry/apps/advisor/register.go +++ b/pkg/registry/apps/advisor/register.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/apps/advisor/pkg/apis" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" advisorapp "github.com/grafana/grafana/apps/advisor/pkg/app" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" "github.com/grafana/grafana/pkg/services/apiserver/builder/runner" ) @@ -13,11 +14,14 @@ type AdvisorAppProvider struct { app.Provider } -func RegisterApp() *AdvisorAppProvider { +func RegisterApp( + checkRegistry checkregistry.CheckService, +) *AdvisorAppProvider { provider := &AdvisorAppProvider{} appCfg := &runner.AppBuilderConfig{ OpenAPIDefGetter: advisorv0alpha1.GetOpenAPIDefinitions, ManagedKinds: advisorapp.GetKinds(), + CustomConfig: any(checkRegistry), } provider.Provider = simple.NewAppProvider(apis.LocalManifest(), appCfg, advisorapp.New) return provider diff --git a/pkg/registry/apps/wireset.go b/pkg/registry/apps/wireset.go index b4da6459d3f..62397bebe51 100644 --- a/pkg/registry/apps/wireset.go +++ b/pkg/registry/apps/wireset.go @@ -3,6 +3,7 @@ package appregistry import ( "github.com/google/wire" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" "github.com/grafana/grafana/pkg/registry/apps/advisor" "github.com/grafana/grafana/pkg/registry/apps/investigation" "github.com/grafana/grafana/pkg/registry/apps/playlist" @@ -13,4 +14,6 @@ var WireSet = wire.NewSet( playlist.RegisterApp, investigation.RegisterApp, advisor.RegisterApp, + checkregistry.ProvideService, + wire.Bind(new(checkregistry.CheckService), new(*checkregistry.Service)), ) From a540c2fe7c80de387bf6f89f4d10b44dde3dee58 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 23 Jan 2025 15:31:34 +0000 Subject: [PATCH 027/894] MultiCombobox: Refactor open state (#99453) * Make downshift useCombobox own the isOpen state again * oopsie, removed my console logs --- .../src/components/Combobox/MultiCombobox.tsx | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index a56cf853f1d..8099fb595d6 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -74,11 +74,6 @@ export const MultiCombobox = (props: MultiComboboxPro return newItems; }, [baseItems, inputValue, enableAllOption, allOptionItem]); - const [isOpen, setIsOpen] = useState(false); - - const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, isOpen); - - const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled); const { measureRef, counterMeasureRef, suffixMeasureRef, shownItems } = useMeasureMulti( selectedItems, @@ -108,17 +103,41 @@ export const MultiCombobox = (props: MultiComboboxPro break; } }, + stateReducer: (state, actionAndChanges) => { + const { changes } = actionAndChanges; + return { + ...changes, + + /** + * TODO: Fix Hack! + * This prevents the menu from closing when the user unselects an item in the dropdown at the expense + * of breaking keyboard navigation. + * + * Downshift isn't really designed to keep selected items in the dropdown menu, so when you unselect an item + * in a multiselect, the stateReducer tries to move focus onto another item which causes the menu to be closed. + * This only seems to happen when you deselect the last item in the selectedItems list. + * + * Check out: + * - FunctionRemoveSelectedItem in the useMultipleSelection reducer https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/reducer.js#L75 + * - The activeIndex useEffect in useMultipleSelection https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/index.js#L68-L72 + * + * Forcing the activeIndex to -999 both prevents the useEffect that changes the focus from triggering (value never changes) + * and prevents the if statement in useMultipleSelection from focusing anything. + */ + activeIndex: -999, + }; + }, }); const { //getToggleButtonProps, //getLabelProps, + isOpen, + highlightedIndex, getMenuProps, getInputProps, - highlightedIndex, getItemProps, } = useCombobox({ - isOpen, items, itemToString, inputValue, @@ -135,7 +154,6 @@ export const MultiCombobox = (props: MultiComboboxPro }; case useCombobox.stateChangeTypes.InputBlur: setInputValue(''); - setIsOpen(false); default: return changes; } @@ -175,14 +193,15 @@ export const MultiCombobox = (props: MultiComboboxPro case useCombobox.stateChangeTypes.InputChange: setInputValue(newInputValue ?? ''); break; - case useCombobox.stateChangeTypes.InputClick: - setIsOpen(true); default: break; } }, }); + const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, isOpen); + const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled); + const virtualizerOptions = { count: items.length, getScrollElement: () => scrollRef.current, @@ -241,7 +260,6 @@ export const MultiCombobox = (props: MultiComboboxPro disabled, preventKeyAction: isOpen, placeholder: selectedItems.length > 0 ? undefined : placeholder, - onFocus: () => !disabled && setIsOpen(true), }) )} /> From 3993d691f43244c0532a854fee3133ed8072c02e Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 23 Jan 2025 17:00:02 +0100 Subject: [PATCH 028/894] Advisor: Implement authorizer (#99440) --- apps/advisor/pkg/app/authorizer.go | 31 ++++++++++ apps/advisor/pkg/app/authorizer_test.go | 78 +++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- pkg/registry/apps/advisor/register.go | 1 + 5 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 apps/advisor/pkg/app/authorizer.go create mode 100644 apps/advisor/pkg/app/authorizer_test.go diff --git a/apps/advisor/pkg/app/authorizer.go b/apps/advisor/pkg/app/authorizer.go new file mode 100644 index 00000000000..67defbc85d9 --- /dev/null +++ b/apps/advisor/pkg/app/authorizer.go @@ -0,0 +1,31 @@ +package app + +import ( + "context" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc(func( + ctx context.Context, attr authorizer.Attributes, + ) (authorized authorizer.Decision, reason string, err error) { + if !attr.IsResourceRequest() { + return authorizer.DecisionNoOpinion, "", nil + } + + // require a user + u, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "valid user is required", err + } + + // check if is admin + if u.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + + return authorizer.DecisionDeny, "forbidden", nil + }) +} diff --git a/apps/advisor/pkg/app/authorizer_test.go b/apps/advisor/pkg/app/authorizer_test.go new file mode 100644 index 00000000000..84414362fbd --- /dev/null +++ b/apps/advisor/pkg/app/authorizer_test.go @@ -0,0 +1,78 @@ +package app + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/stretchr/testify/assert" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func TestGetAuthorizer(t *testing.T) { + tests := []struct { + name string + ctx context.Context + attr authorizer.Attributes + expectedDecision authorizer.Decision + expectedReason string + expectedErr error + }{ + { + name: "non-resource request", + ctx: context.TODO(), + attr: &mockAttributes{resourceRequest: false}, + expectedDecision: authorizer.DecisionNoOpinion, + expectedReason: "", + expectedErr: nil, + }, + { + name: "user is admin", + ctx: identity.WithRequester(context.TODO(), &mockUser{isGrafanaAdmin: true}), + attr: &mockAttributes{resourceRequest: true}, + expectedDecision: authorizer.DecisionAllow, + expectedReason: "", + expectedErr: nil, + }, + { + name: "user is not admin", + ctx: identity.WithRequester(context.TODO(), &mockUser{isGrafanaAdmin: false}), + attr: &mockAttributes{resourceRequest: true}, + expectedDecision: authorizer.DecisionDeny, + expectedReason: "forbidden", + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + auth := GetAuthorizer() + decision, reason, err := auth.Authorize(tt.ctx, tt.attr) + assert.Equal(t, tt.expectedDecision, decision) + assert.Equal(t, tt.expectedReason, reason) + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +type mockAttributes struct { + authorizer.Attributes + resourceRequest bool +} + +func (m *mockAttributes) IsResourceRequest() bool { + return m.resourceRequest +} + +// Implement other methods of authorizer.Attributes as needed + +type mockUser struct { + identity.Requester + isGrafanaAdmin bool +} + +func (m *mockUser) GetIsGrafanaAdmin() bool { + return m.isGrafanaAdmin +} + +// Implement other methods of identity.Requester as needed diff --git a/go.mod b/go.mod index d57b3cb3d61..d0282c33045 100644 --- a/go.mod +++ b/go.mod @@ -198,7 +198,7 @@ require ( ) require ( - github.com/grafana/grafana/apps/advisor v0.0.0-20250121115006-c1eac9f9973f // @grafana/plugins-platform-backend + github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173 // @grafana/plugins-platform-backend github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d // @grafana/alerting-backend github.com/grafana/grafana/apps/investigation v0.0.0-20250121113133-e747350fee2d // @fcjack @matryer github.com/grafana/grafana/apps/playlist v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-app-platform-squad diff --git a/go.sum b/go.sum index 166125e556b..7285649b56c 100644 --- a/go.sum +++ b/go.sum @@ -1532,8 +1532,8 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 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.262.0 h1:R2DV6lwBQE5zaogxX3PorD9Seo8CXA8YuStf84oqwkk= github.com/grafana/grafana-plugin-sdk-go v0.262.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= -github.com/grafana/grafana/apps/advisor v0.0.0-20250121115006-c1eac9f9973f h1:c8IkbxPvM6+lscVOLgtbt8Gnro4Liltd+E2eqkoAeZA= -github.com/grafana/grafana/apps/advisor v0.0.0-20250121115006-c1eac9f9973f/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= +github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173 h1:uOM89HiWVVOTls0LrD4coHTckb2lA4U0sIJwCYdbhbw= +github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d h1:NRVOtiG1aUwOazBj9KM7X2o2shsM6TchqisezzoH1gw= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d/go.mod h1:AvleS6icyPmcBjihtx5jYEvdzLmHGBp66NuE0AMR57A= github.com/grafana/grafana/apps/investigation v0.0.0-20250121113133-e747350fee2d h1:oNc/aDfDucQxLbRZK25yz3Cwc+dGo1C0Xmm2LaliWUQ= diff --git a/pkg/registry/apps/advisor/register.go b/pkg/registry/apps/advisor/register.go index a680dcc0880..c989d82262e 100644 --- a/pkg/registry/apps/advisor/register.go +++ b/pkg/registry/apps/advisor/register.go @@ -21,6 +21,7 @@ func RegisterApp( appCfg := &runner.AppBuilderConfig{ OpenAPIDefGetter: advisorv0alpha1.GetOpenAPIDefinitions, ManagedKinds: advisorapp.GetKinds(), + Authorizer: advisorapp.GetAuthorizer(), CustomConfig: any(checkRegistry), } provider.Provider = simple.NewAppProvider(apis.LocalManifest(), appCfg, advisorapp.New) From ec9f59fe9e29a7d83917e96c1d920355682e2190 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 23 Jan 2025 17:06:14 +0100 Subject: [PATCH 029/894] Chore: Bump promlib to v.0.0.8 (#99458) * bump promlib * make update-workspace --- go.mod | 2 +- go.sum | 4 ++-- pkg/storage/unified/apistore/go.sum | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index d0282c33045..e330b0e2e5f 100644 --- a/go.mod +++ b/go.mod @@ -209,7 +209,7 @@ require ( // This needs to be here for other projects that import grafana/grafana // For local development grafana/grafana will always use the local files // Check go.work file for details - github.com/grafana/grafana/pkg/promlib v0.0.7 // @grafana/oss-big-tent + github.com/grafana/grafana/pkg/promlib v0.0.8 // @grafana/oss-big-tent github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-search-and-storage github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-search-and-storage diff --git a/go.sum b/go.sum index 7285649b56c..a48895f596d 100644 --- a/go.sum +++ b/go.sum @@ -1546,8 +1546,8 @@ github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d h github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d/go.mod h1:PNmbi49lVrv2b2I8pdu46dTs2728lKEbnVuQD8I5MnM= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d h1:vVaLOibSj5lDwCb+pa0fly9uods8nTq1NJPYrTuPAGE= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d/go.mod h1:srJ6H0DL7c9Cz4CmK0h4AOlC1sm8UDwn9jBZ+RHxBt4= -github.com/grafana/grafana/pkg/promlib v0.0.7 h1:BdpanKOKnID/l1BJZLhE7TRNtmq7aOVdou1LBFWaMmU= -github.com/grafana/grafana/pkg/promlib v0.0.7/go.mod h1:rnwJXCA2xRwb7F27NB35iO/JsLL/H/+eVXECk/hrEhQ= +github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= +github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d h1:cYBjuhb3m5oC6Z00Kw8DdySFaNhwb38SMxx0oXnz5vQ= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d h1:4w4x8g6xCSRj06er81atNSQVL0fTSwLekDYsRxpz+M0= diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 478fa431e69..c6bf0abfdee 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -569,8 +569,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.262.0 h1:R2DV6lwBQE5zaogxX3PorD9Seo8 github.com/grafana/grafana-plugin-sdk-go v0.262.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d h1:aBD5kzsIAh50vjNqUkWK9mNpLGIBYAnKkWtUepGNAiQ= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d/go.mod h1:1sq0guad+G4SUTlBgx7SXfhnzy7D86K/LcVOtiQCiMA= -github.com/grafana/grafana/pkg/promlib v0.0.7 h1:BdpanKOKnID/l1BJZLhE7TRNtmq7aOVdou1LBFWaMmU= -github.com/grafana/grafana/pkg/promlib v0.0.7/go.mod h1:rnwJXCA2xRwb7F27NB35iO/JsLL/H/+eVXECk/hrEhQ= +github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= +github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d h1:cYBjuhb3m5oC6Z00Kw8DdySFaNhwb38SMxx0oXnz5vQ= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= From ef3a53f85cd4af1d941e75e9b9fcbc5e0ac9cbbc Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Thu, 23 Jan 2025 13:06:48 -0300 Subject: [PATCH 030/894] Share: Add tracking to invite user button (#99376) --- public/app/features/users/UsersActionBar.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx index 48ef3a81e83..5a4fa4b2e8b 100644 --- a/public/app/features/users/UsersActionBar.tsx +++ b/public/app/features/users/UsersActionBar.tsx @@ -1,5 +1,6 @@ import { connect, ConnectedProps } from 'react-redux'; +import { reportInteraction } from '@grafana/runtime'; import { RadioButtonGroup, LinkButton, FilterInput, InlineField } from '@grafana/ui'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; @@ -52,6 +53,13 @@ export const UsersActionBarUnconnected = ({ // 2) new basic auth users can be created for this instance (!config.disableLoginForm). const showInviteButton: boolean = canAddToOrg && !(config.disableLoginForm && config.externalUserMngInfo); + const onExternalUserMngClick = () => { + reportInteraction('users_admin_actions_clicked', { + category: 'org_users', + item: 'manage_users_external', + }); + }; + return (
@@ -68,7 +76,12 @@ export const UsersActionBarUnconnected = ({ )} {showInviteButton && Invite} {externalUserMngLinkUrl && ( - + {externalUserMngLinkName} )} From 59b246dbeaa09ae0521510b607622f04cdd67321 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:10:59 +0000 Subject: [PATCH 031/894] Update dependency stylelint to v16.13.2 (#99455) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 102 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 70 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index d43c76f4903..8cbbdc5d5d0 100644 --- a/package.json +++ b/package.json @@ -228,7 +228,7 @@ "sass-loader": "16.0.4", "smtp-tester": "^2.1.0", "style-loader": "4.0.0", - "stylelint": "16.12.0", + "stylelint": "16.13.2", "stylelint-config-sass-guidelines": "12.1.0", "terser-webpack-plugin": "5.3.11", "testing-library-selector": "0.3.1", diff --git a/yarn.lock b/yarn.lock index 765a1f74a0e..9abbcd7ada9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4457,6 +4457,15 @@ __metadata: languageName: node linkType: hard +"@keyv/serialize@npm:^1.0.2": + version: 1.0.2 + resolution: "@keyv/serialize@npm:1.0.2" + dependencies: + buffer: "npm:^6.0.3" + checksum: 10/6a42a5778a6b4542f6903ba7e6a17c5bd116441798d75c95fba9908c76c7606db527fad710b5c54abc6175e49b1bbaaafe3b836ad4b91e1af701394134f1d504 + languageName: node + linkType: hard + "@kusto/language-service-next@npm:11.5.3": version: 11.5.3 resolution: "@kusto/language-service-next@npm:11.5.3" @@ -11980,6 +11989,16 @@ __metadata: languageName: node linkType: hard +"cacheable@npm:^1.8.7": + version: 1.8.7 + resolution: "cacheable@npm:1.8.7" + dependencies: + hookified: "npm:^1.6.0" + keyv: "npm:^5.2.3" + checksum: 10/dce96e947c5b879a58ce024fd2d08a4c44ee328e8406cd6243da2d0e3a17579d63108c80e9272f64190d2d2f00e26a307b3e3ff65b6665abc70956714442897b + languageName: node + linkType: hard + "cachedir@npm:^2.3.0": version: 2.3.0 resolution: "cachedir@npm:2.3.0" @@ -13453,7 +13472,7 @@ __metadata: languageName: node linkType: hard -"css-tree@npm:^3.0.1": +"css-tree@npm:^3.1.0": version: 3.1.0 resolution: "css-tree@npm:3.1.0" dependencies: @@ -16299,6 +16318,15 @@ __metadata: languageName: node linkType: hard +"file-entry-cache@npm:^10.0.5": + version: 10.0.5 + resolution: "file-entry-cache@npm:10.0.5" + dependencies: + flat-cache: "npm:^6.1.5" + checksum: 10/57439b39635e75aa900ccdaad9167b85a500a559dd8f0902509189849784e9255d43cd3d8307f918dbd19f48b02562b5e354bcf7bfa23afe3985485504eb7a5e + languageName: node + linkType: hard + "file-entry-cache@npm:^8.0.0": version: 8.0.0 resolution: "file-entry-cache@npm:8.0.0" @@ -16308,15 +16336,6 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^9.1.0": - version: 9.1.0 - resolution: "file-entry-cache@npm:9.1.0" - dependencies: - flat-cache: "npm:^5.0.0" - checksum: 10/fd67a9552f272ac4a1731c545e1350bd135e208659144cc5311baac6b8bbf55da7c8c3a0bf25c71ed78eff2bdd26d2a3a8f9ba3d8bec968fe8d1eeba6ab14a96 - languageName: node - linkType: hard - "file-saver@npm:2.0.5": version: 2.0.5 resolution: "file-saver@npm:2.0.5" @@ -16485,13 +16504,14 @@ __metadata: languageName: node linkType: hard -"flat-cache@npm:^5.0.0": - version: 5.0.0 - resolution: "flat-cache@npm:5.0.0" +"flat-cache@npm:^6.1.5": + version: 6.1.5 + resolution: "flat-cache@npm:6.1.5" dependencies: - flatted: "npm:^3.3.1" - keyv: "npm:^4.5.4" - checksum: 10/42570762052b17a1dec221d73a1e417d0ba07137de6debaabb51389cac265a12a027a895dc84e1725bc5cdde04fe8b706ad836860b05488e9a04bda9301d2529 + cacheable: "npm:^1.8.7" + flatted: "npm:^3.3.2" + hookified: "npm:^1.6.0" + checksum: 10/c98d7635316ceee08b74c1dd3f05eda6a5f2f073688af927de68193ca992ab09eb9f0ee7d5fac40f53280844f11cf368b90dc43f73fee358165dc84e13828674 languageName: node linkType: hard @@ -16504,10 +16524,10 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.2.9, flatted@npm:^3.3.1": - version: 3.3.1 - resolution: "flatted@npm:3.3.1" - checksum: 10/7b8376061d5be6e0d3658bbab8bde587647f68797cf6bfeae9dea0e5137d9f27547ab92aaff3512dd9d1299086a6d61be98e9d48a56d17531b634f77faadbc49 +"flatted@npm:^3.2.9, flatted@npm:^3.3.2": + version: 3.3.2 + resolution: "flatted@npm:3.3.2" + checksum: 10/ac3c159742e01d0e860a861164bcfd35bb567ccbebb8a0dd041e61cf3c64a435b917dd1e7ed1c380c2ebca85735fb16644485ec33665bc6aafc3b316aa1eed44 languageName: node linkType: hard @@ -17645,7 +17665,7 @@ __metadata: slate-react: "npm:0.22.10" smtp-tester: "npm:^2.1.0" style-loader: "npm:4.0.0" - stylelint: "npm:16.12.0" + stylelint: "npm:16.13.2" stylelint-config-sass-guidelines: "npm:12.1.0" swagger-ui-react: "npm:5.18.2" symbol-observable: "npm:4.0.0" @@ -17947,6 +17967,13 @@ __metadata: languageName: node linkType: hard +"hookified@npm:^1.6.0": + version: 1.7.0 + resolution: "hookified@npm:1.7.0" + checksum: 10/87fb8f2ae170f28b1e0b903f2b0b40fb2a92a0364baab7c54121db1c871a2e81c589d778b48e1a35462181b8840764caf6cac3744330e5687a05a4b4d2ad729d + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -18477,10 +18504,10 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^6.0.2": - version: 6.0.2 - resolution: "ignore@npm:6.0.2" - checksum: 10/af39e49996cd989763920e445eff897d0ae1e36b5f27b0e09e14a4fd2df89b362f92e720ecf06ef729056842366527db8561d310e904718810b92ffbcd23056d +"ignore@npm:^7.0.1": + version: 7.0.3 + resolution: "ignore@npm:7.0.3" + checksum: 10/ce5e812af3acd6607a3fe0a9f9b5f01d53f009a5ace8cbf5b6491d05a481b55d65186e6a7eaa13126e93f15276bcf3d1e8d6ff3ce5549c312f9bb313fff33365 languageName: node linkType: hard @@ -20515,6 +20542,15 @@ __metadata: languageName: node linkType: hard +"keyv@npm:^5.2.3": + version: 5.2.3 + resolution: "keyv@npm:5.2.3" + dependencies: + "@keyv/serialize": "npm:^1.0.2" + checksum: 10/47b4e9deb33e6a80e5ea79f3022ed3a14bc9fe553b7527ffff0a70b10c7a6c1a5d7e49b9bcfdbd8e8b9fb4632d68baa19d09e82628bcf853103e750e56d49a9e + languageName: node + linkType: hard + "kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": version: 6.0.3 resolution: "kind-of@npm:6.0.3" @@ -28494,9 +28530,9 @@ __metadata: languageName: node linkType: hard -"stylelint@npm:16.12.0, stylelint@npm:^16.8.2": - version: 16.12.0 - resolution: "stylelint@npm:16.12.0" +"stylelint@npm:16.13.2, stylelint@npm:^16.8.2": + version: 16.13.2 + resolution: "stylelint@npm:16.13.2" dependencies: "@csstools/css-parser-algorithms": "npm:^3.0.4" "@csstools/css-tokenizer": "npm:^3.0.3" @@ -28507,16 +28543,16 @@ __metadata: colord: "npm:^2.9.3" cosmiconfig: "npm:^9.0.0" css-functions-list: "npm:^3.2.3" - css-tree: "npm:^3.0.1" + css-tree: "npm:^3.1.0" debug: "npm:^4.3.7" - fast-glob: "npm:^3.3.2" + fast-glob: "npm:^3.3.3" fastest-levenshtein: "npm:^1.0.16" - file-entry-cache: "npm:^9.1.0" + file-entry-cache: "npm:^10.0.5" global-modules: "npm:^2.0.0" globby: "npm:^11.1.0" globjoin: "npm:^0.1.4" html-tags: "npm:^3.3.1" - ignore: "npm:^6.0.2" + ignore: "npm:^7.0.1" imurmurhash: "npm:^0.1.4" is-plain-object: "npm:^5.0.0" known-css-properties: "npm:^0.35.0" @@ -28538,7 +28574,7 @@ __metadata: write-file-atomic: "npm:^5.0.1" bin: stylelint: bin/stylelint.mjs - checksum: 10/8ab174441f3909a79b84efe62b99061db42ef232262abf0af50ab2458822c39781886171912362eb018a293b70c33bd20b8ff7194ef64af4aee4541787aee2e9 + checksum: 10/98385b53d3c822b3b764fe8ff2f7212717127ab40ca9fd34a83bc6e27b5240d4ea02f959e01d4eaf91c87480a0c787b07b837883d4b3ec44133cc7ca03c79b47 languageName: node linkType: hard From 192a81d07f077b43d178a01e6dac5eb11d42961c Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Thu, 23 Jan 2025 09:30:14 -0700 Subject: [PATCH 032/894] Folders: Fix guardian to use folder service (#99339) --- pkg/api/dashboard_snapshot_test.go | 3 +- pkg/api/dashboard_test.go | 6 +- pkg/api/folder_bench_test.go | 2 +- .../dashboards/service/dashboard_service.go | 20 ++- .../dashboard_service_integration_test.go | 3 +- pkg/services/folder/folderimpl/folder.go | 14 +- .../guardian/accesscontrol_guardian.go | 141 ++++++++++-------- .../guardian/accesscontrol_guardian_test.go | 3 +- pkg/services/guardian/guardian.go | 28 ++-- pkg/services/guardian/provider.go | 20 +-- pkg/services/libraryelements/guard.go | 13 +- .../libraryelements_get_all_test.go | 4 +- .../libraryelements_patch_test.go | 6 +- .../libraryelements_permissions_test.go | 21 +-- .../libraryelements/libraryelements_test.go | 62 ++++---- .../librarypanels/librarypanels_test.go | 8 +- 16 files changed, 207 insertions(+), 147 deletions(-) diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index 2f27909020e..24dcd3ef4ec 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/dashboards" @@ -40,7 +41,7 @@ func TestHTTPServer_DeleteDashboardSnapshot(t *testing.T) { hs.DashboardService = svc hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) + guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) }) } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 5d130a1ddd3..0efb1a167ee 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -151,7 +151,7 @@ func TestHTTPServer_GetDashboard_AccessControl(t *testing.T) { hs.starService = startest.NewStarServiceFake() hs.dashboardProvisioningService = mockDashboardProvisioningService{} - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) + guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) }) } @@ -279,7 +279,7 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) { license.On("FeatureEnabled", publicdashboardModels.FeaturePublicDashboardsEmailSharing).Return(false) hs.PublicDashboardsApi = api.ProvideApi(pubDashService, nil, hs.AccessControl, featuremgmt.WithFeatures(), middleware, hs.Cfg, license) - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) + guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) }) } deleteDashboard := func(server *webtest.Server, permissions []accesscontrol.Permission) (*http.Response, error) { @@ -330,7 +330,7 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { ExpectedDashboardVersion: &dashver.DashboardVersionDTO{}, } - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) + guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) }) } diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index 0ca06bdd658..8192ff6fef5 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -495,7 +495,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog } hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) + guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) m.Get("/api/folders", hs.GetFolders) m.Get("/api/search", hs.Search) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index f68ed03495d..d5af358d86f 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -56,6 +56,7 @@ var ( provisionerPermissions = []accesscontrol.Permission{ {Action: dashboards.ActionFoldersCreate, Scope: dashboards.ScopeFoldersAll}, {Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}, + {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll}, {Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll}, {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersAll}, {Action: datasources.ActionRead, Scope: datasources.ScopeAll}, @@ -611,13 +612,28 @@ func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashbo if newDashboard { // if it's a new dashboard/folder check the parent folder permissions metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - guard, err := guardian.New(ctx, d.FolderID, d.OrgID, user) + guard, err := guardian.NewByFolder(ctx, &folder.Folder{ + ID: d.FolderID, // nolint:staticcheck + OrgID: d.OrgID, + }, d.OrgID, user) if err != nil { return nil, err } return guard, nil } + + if d.IsFolder { + guard, err := guardian.NewByFolder(ctx, &folder.Folder{ + ID: d.ID, // nolint:staticcheck + UID: d.UID, + OrgID: d.OrgID, + }, d.OrgID, user) + if err != nil { + return nil, err + } + return guard, nil + } + guard, err := guardian.NewByDashboard(ctx, d, d.OrgID, user) if err != nil { return nil, err diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index 860e0be4cf6..95f840294c9 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -896,7 +897,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc ) dashboardService.RegisterDashboardPermissions(dashboardPermissions) require.NoError(t, err) - guardian.InitAccessControlGuardian(cfg, ac, dashboardService) + guardian.InitAccessControlGuardian(cfg, ac, dashboardService, folderService, log.NewNopLogger()) savedFolder := saveTestFolder(t, "Saved folder", testOrgID, sqlStore) savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.UID, sqlStore) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 85664cda390..b9f8c31b9a9 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -392,7 +392,7 @@ func (s *Service) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQu // we only need to check access to the folder // if the parent is accessible then the subfolders are accessible as well (due to inheritance) - g, err := guardian.NewByUID(ctx, q.UID, q.OrgID, q.SignedInUser) + g, err := guardian.NewByFolderUID(ctx, q.UID, q.OrgID, q.SignedInUser) if err != nil { return nil, err } @@ -941,7 +941,7 @@ func (s *Service) DeleteLegacy(ctx context.Context, cmd *folder.DeleteFolderComm return folder.ErrBadRequest.Errorf("invalid orgID") } - guard, err := guardian.NewByUID(ctx, cmd.UID, cmd.OrgID, cmd.SignedInUser) + guard, err := guardian.NewByFolderUID(ctx, cmd.UID, cmd.OrgID, cmd.SignedInUser) if err != nil { return err } @@ -1414,13 +1414,19 @@ func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashbo // if it's a new dashboard/folder check the parent folder permissions metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc() // nolint:staticcheck - guard, err := guardian.New(ctx, d.FolderID, d.OrgID, user) + guard, err := guardian.NewByFolder(ctx, &folder.Folder{ + ID: d.FolderID, // nolint:staticcheck + OrgID: d.OrgID, + }, d.OrgID, user) if err != nil { return nil, err } return guard, nil } - guard, err := guardian.NewByDashboard(ctx, d, d.OrgID, user) + guard, err := guardian.NewByFolder(ctx, &folder.Folder{ + UID: d.UID, + OrgID: d.OrgID, + }, d.OrgID, user) if err != nil { return nil, err } diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go index de32d370ae2..c521fa9221e 100644 --- a/pkg/services/guardian/accesscontrol_guardian.go +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -18,6 +18,7 @@ var _ DashboardGuardian = new(accessControlDashboardGuardian) func NewAccessControlDashboardGuardian( ctx context.Context, cfg *setting.Cfg, dashboardId int64, user identity.Requester, ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, + foldersService folder.Service, logger log.Logger, ) (DashboardGuardian, error) { var dashboard *dashboards.Dashboard if dashboardId != 0 { @@ -37,6 +38,7 @@ func NewAccessControlDashboardGuardian( } if dashboard != nil && dashboard.IsFolder { + logger.Info("using dashboard guardian for folder", "folder", dashboard.UID) return &accessControlFolderGuardian{ accessControlBaseGuardian: accessControlBaseGuardian{ ctx: ctx, @@ -58,68 +60,22 @@ func NewAccessControlDashboardGuardian( user: user, ac: ac, dashboardService: dashboardService, - }, - dashboard: dashboard, - }, nil -} - -// NewAccessControlDashboardGuardianByDashboard creates a dashboard guardian by the provided dashboardUID. -func NewAccessControlDashboardGuardianByUID( - ctx context.Context, cfg *setting.Cfg, dashboardUID string, user identity.Requester, - ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, -) (DashboardGuardian, error) { - var dashboard *dashboards.Dashboard - if dashboardUID != "" { - q := &dashboards.GetDashboardQuery{ - UID: dashboardUID, - OrgID: user.GetOrgID(), - } - - qResult, err := dashboardService.GetDashboard(ctx, q) - if err != nil { - if errors.Is(err, dashboards.ErrDashboardNotFound) { - return nil, ErrGuardianDashboardNotFound.Errorf("failed to get dashboard by UID: %w", err) - } - return nil, ErrGuardianGetDashboardFailure.Errorf("failed to get dashboard by UID: %w", err) - } - dashboard = qResult - } - - if dashboard != nil && dashboard.IsFolder { - return &accessControlFolderGuardian{ - accessControlBaseGuardian: accessControlBaseGuardian{ - ctx: ctx, - cfg: cfg, - log: log.New("folder.permissions"), - user: user, - ac: ac, - dashboardService: dashboardService, - }, - folder: dashboards.FromDashboard(dashboard), - }, nil - } - - return &accessControlDashboardGuardian{ - accessControlBaseGuardian: accessControlBaseGuardian{ - cfg: cfg, - ctx: ctx, - log: log.New("dashboard.permissions"), - user: user, - ac: ac, - dashboardService: dashboardService, + folderService: foldersService, }, dashboard: dashboard, }, nil } // NewAccessControlDashboardGuardianByDashboard creates a dashboard guardian by the provided dashboard. -// This constructor should be preferred over the other two if the dashboard in available +// This constructor should be preferred over the other two if the dashboard is available // since it avoids querying the database for fetching the dashboard. func NewAccessControlDashboardGuardianByDashboard( ctx context.Context, cfg *setting.Cfg, dashboard *dashboards.Dashboard, user identity.Requester, - ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, + ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, folderService folder.Service, + logger log.Logger, ) (DashboardGuardian, error) { if dashboard != nil && dashboard.IsFolder { + logger.Info("using by dashboard guardian for folder", "folder", dashboard.UID) return &accessControlFolderGuardian{ accessControlBaseGuardian: accessControlBaseGuardian{ ctx: ctx, @@ -128,6 +84,7 @@ func NewAccessControlDashboardGuardianByDashboard( user: user, ac: ac, dashboardService: dashboardService, + folderService: folderService, }, folder: dashboards.FromDashboard(dashboard), }, nil @@ -141,16 +98,35 @@ func NewAccessControlDashboardGuardianByDashboard( user: user, ac: ac, dashboardService: dashboardService, + folderService: folderService, }, dashboard: dashboard, }, nil } -// NewAccessControlFolderGuardian creates a folder guardian by the provided folder. -func NewAccessControlFolderGuardian( - ctx context.Context, cfg *setting.Cfg, f *folder.Folder, user identity.Requester, - ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, +// NewAccessControlFolderGuardianByUID creates a folder guardian by the provided folderUID. +func NewAccessControlFolderGuardianByUID( + ctx context.Context, cfg *setting.Cfg, folderUID string, user identity.Requester, + ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, foldersService folder.Service, ) (DashboardGuardian, error) { + var f *folder.Folder + if folderUID != "" { + q := &folder.GetFolderQuery{ + UID: &folderUID, + OrgID: user.GetOrgID(), + SignedInUser: user, + } + + qResult, err := foldersService.Get(ctx, q) + if err != nil { + if errors.Is(err, dashboards.ErrFolderNotFound) { + return nil, ErrGuardianFolderNotFound.Errorf("failed to get folder by UID: %w", err) + } + return nil, ErrGuardianGetFolderFailure.Errorf("failed to get folder by UID: %w", err) + } + f = qResult + } + return &accessControlFolderGuardian{ accessControlBaseGuardian: accessControlBaseGuardian{ ctx: ctx, @@ -159,6 +135,44 @@ func NewAccessControlFolderGuardian( user: user, ac: ac, dashboardService: dashboardService, + folderService: foldersService, + }, + folder: f, + }, nil +} + +// NewAccessControlFolderGuardian creates a folder guardian by the provided folder. +func NewAccessControlFolderGuardian( + ctx context.Context, cfg *setting.Cfg, f *folder.Folder, user identity.Requester, + ac accesscontrol.AccessControl, orgID int64, dashboardService dashboards.DashboardService, + folderService folder.Service, +) (DashboardGuardian, error) { + if f.UID == "" { // nolint:staticcheck + query := &folder.GetFolderQuery{ + ID: &f.ID, // nolint:staticcheck + OrgID: orgID, + SignedInUser: user, + } + + folder, err := folderService.Get(ctx, query) + if err != nil { + if errors.Is(err, dashboards.ErrFolderNotFound) { + return nil, ErrGuardianFolderNotFound.Errorf("failed to get folder: %w", err) + } + return nil, ErrGuardianGetFolderFailure.Errorf("failed to get folder: %w", err) + } + f = folder + } + + return &accessControlFolderGuardian{ + accessControlBaseGuardian: accessControlBaseGuardian{ + ctx: ctx, + cfg: cfg, + log: log.New("folder.permissions"), + user: user, + ac: ac, + dashboardService: dashboardService, + folderService: folderService, }, folder: f, }, nil @@ -171,6 +185,7 @@ type accessControlBaseGuardian struct { user identity.Requester ac accesscontrol.AccessControl dashboardService dashboards.DashboardService + folderService folder.Service } type accessControlDashboardGuardian struct { @@ -353,24 +368,24 @@ func (a *accessControlFolderGuardian) evaluate(evaluator accesscontrol.Evaluator return ok, err } -func (a *accessControlDashboardGuardian) loadParentFolder(folderID int64) (*dashboards.Dashboard, error) { +func (a *accessControlDashboardGuardian) loadParentFolder(folderID int64) (*folder.Folder, error) { if folderID == 0 { - return &dashboards.Dashboard{UID: accesscontrol.GeneralFolderUID}, nil + return &folder.Folder{UID: accesscontrol.GeneralFolderUID, OrgID: a.user.GetOrgID()}, nil } - folderQuery := &dashboards.GetDashboardQuery{ID: folderID, OrgID: a.user.GetOrgID()} - folderQueryResult, err := a.dashboardService.GetDashboard(a.ctx, folderQuery) + folderQuery := &folder.GetFolderQuery{ID: &folderID, OrgID: a.user.GetOrgID(), SignedInUser: a.user} + folderQueryResult, err := a.folderService.Get(a.ctx, folderQuery) if err != nil { return nil, err } return folderQueryResult, nil } -func (a *accessControlFolderGuardian) loadParentFolder(folderID int64) (*dashboards.Dashboard, error) { +func (a *accessControlFolderGuardian) loadParentFolder(folderID int64) (*folder.Folder, error) { if folderID == 0 { - return &dashboards.Dashboard{UID: accesscontrol.GeneralFolderUID}, nil + return &folder.Folder{UID: accesscontrol.GeneralFolderUID, OrgID: a.user.GetOrgID()}, nil } - folderQuery := &dashboards.GetDashboardQuery{ID: folderID, OrgID: a.user.GetOrgID()} - folderQueryResult, err := a.dashboardService.GetDashboard(a.ctx, folderQuery) + folderQuery := &folder.GetFolderQuery{ID: &folderID, OrgID: a.user.GetOrgID(), SignedInUser: a.user} + folderQueryResult, err := a.folderService.Get(a.ctx, folderQuery) if err != nil { return nil, err } diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index 8d24b282395..59ac7d49828 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/dashboards" @@ -976,7 +977,7 @@ func setupAccessControlGuardianTest( userPermissions[orgID][p.Action] = append(userPermissions[orgID][p.Action], p.Scope) } - g, err := NewAccessControlDashboardGuardianByDashboard(context.Background(), cfg, d, &user.SignedInUser{OrgID: orgID, Permissions: userPermissions}, ac, fakeDashboardService) + g, err := NewAccessControlDashboardGuardianByDashboard(context.Background(), cfg, d, &user.SignedInUser{OrgID: orgID, Permissions: userPermissions}, ac, fakeDashboardService, folderSvc, log.NewNopLogger()) require.NoError(t, err) return g } diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index e094674d07e..7100b7ffacc 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -15,6 +15,7 @@ var ( ErrGuardianGetDashboardFailure = errutil.Internal("guardian.getDashboardFailure", errutil.WithPublicMessage("Failed to get dashboard")) ErrGuardianDashboardNotFound = errutil.NotFound("guardian.dashboardNotFound") ErrGuardianFolderNotFound = errutil.NotFound("guardian.folderNotFound") + ErrGuardianGetFolderFailure = errutil.Internal("guardian.getFolderFailure", errutil.WithPublicMessage("Failed to get folder")) ) // DashboardGuardian to be used for guard against operations without access on dashboard and acl @@ -33,18 +34,18 @@ var New = func(ctx context.Context, dashId int64, orgId int64, user identity.Req panic("no guardian factory implementation provided") } -// NewByUID factory for creating a new dashboard guardian instance -// When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned -var NewByUID = func(ctx context.Context, dashUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { - panic("no guardian factory implementation provided") -} - // NewByDashboard factory for creating a new dashboard guardian instance // When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned var NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user identity.Requester) (DashboardGuardian, error) { panic("no guardian factory implementation provided") } +// NewByFolderUID factory for creating a new folder guardian instance +// When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned +var NewByFolderUID = func(ctx context.Context, folderUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { + panic("no guardian factory implementation provided") +} + // NewByFolder factory for creating a new folder guardian instance // When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned var NewByFolder = func(ctx context.Context, f *folder.Folder, orgId int64, user identity.Requester) (DashboardGuardian, error) { @@ -107,14 +108,6 @@ func MockDashboardGuardian(mock *FakeDashboardGuardian) { mock.User = user return mock, nil } - - NewByUID = func(_ context.Context, dashUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { - mock.OrgID = orgId - mock.DashUID = dashUID - mock.User = user - return mock, nil - } - NewByDashboard = func(_ context.Context, dash *dashboards.Dashboard, orgId int64, user identity.Requester) (DashboardGuardian, error) { mock.OrgID = orgId mock.DashUID = dash.UID @@ -123,6 +116,13 @@ func MockDashboardGuardian(mock *FakeDashboardGuardian) { return mock, nil } + NewByFolderUID = func(_ context.Context, folderUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { + mock.OrgID = orgId + mock.DashUID = folderUID + mock.User = user + return mock, nil + } + NewByFolder = func(_ context.Context, f *folder.Folder, orgId int64, user identity.Requester) (DashboardGuardian, error) { mock.OrgID = orgId mock.DashUID = f.UID diff --git a/pkg/services/guardian/provider.go b/pkg/services/guardian/provider.go index ad07caf0695..7e3902f1e5a 100644 --- a/pkg/services/guardian/provider.go +++ b/pkg/services/guardian/provider.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" @@ -16,28 +17,29 @@ type Provider struct{} func ProvideService( cfg *setting.Cfg, ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, teamService team.Service, + folderService folder.Service, ) *Provider { // TODO: Fix this hack, see https://github.com/grafana/grafana-enterprise/issues/2935 - InitAccessControlGuardian(cfg, ac, dashboardService) + InitAccessControlGuardian(cfg, ac, dashboardService, folderService, log.New("guardian")) return &Provider{} } func InitAccessControlGuardian( - cfg *setting.Cfg, ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, + cfg *setting.Cfg, ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, folderService folder.Service, logger log.Logger, ) { New = func(ctx context.Context, dashId int64, orgId int64, user identity.Requester) (DashboardGuardian, error) { - return NewAccessControlDashboardGuardian(ctx, cfg, dashId, user, ac, dashboardService) - } - - NewByUID = func(ctx context.Context, dashUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { - return NewAccessControlDashboardGuardianByUID(ctx, cfg, dashUID, user, ac, dashboardService) + return NewAccessControlDashboardGuardian(ctx, cfg, dashId, user, ac, dashboardService, folderService, logger) } NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user identity.Requester) (DashboardGuardian, error) { - return NewAccessControlDashboardGuardianByDashboard(ctx, cfg, dash, user, ac, dashboardService) + return NewAccessControlDashboardGuardianByDashboard(ctx, cfg, dash, user, ac, dashboardService, folderService, logger) + } + + NewByFolderUID = func(ctx context.Context, folderUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { + return NewAccessControlFolderGuardianByUID(ctx, cfg, folderUID, user, ac, dashboardService, folderService) } NewByFolder = func(ctx context.Context, f *folder.Folder, orgId int64, user identity.Requester) (DashboardGuardian, error) { - return NewAccessControlFolderGuardian(ctx, cfg, f, user, ac, dashboardService) + return NewAccessControlFolderGuardian(ctx, cfg, f, user, ac, orgId, dashboardService, folderService) } } diff --git a/pkg/services/libraryelements/guard.go b/pkg/services/libraryelements/guard.go index aa4f5d484fd..737d961f8c8 100644 --- a/pkg/services/libraryelements/guard.go +++ b/pkg/services/libraryelements/guard.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" @@ -41,7 +42,7 @@ func (l *LibraryElementService) requireEditPermissionsOnFolderUID(ctx context.Co return dashboards.ErrFolderAccessDenied } - g, err := guardian.NewByUID(ctx, folderUID, user.GetOrgID(), user) + g, err := guardian.NewByFolderUID(ctx, folderUID, user.GetOrgID(), user) if err != nil { return err } @@ -67,7 +68,10 @@ func (l *LibraryElementService) requireEditPermissionsOnFolder(ctx context.Conte return dashboards.ErrFolderAccessDenied } - g, err := guardian.New(ctx, folderID, user.GetOrgID(), user) + g, err := guardian.NewByFolder(ctx, &folder.Folder{ + ID: folderID, + OrgID: user.GetOrgID(), + }, user.GetOrgID(), user) if err != nil { return err } @@ -88,7 +92,10 @@ func (l *LibraryElementService) requireViewPermissionsOnFolder(ctx context.Conte return nil } - g, err := guardian.New(ctx, folderID, user.GetOrgID(), user) + g, err := guardian.NewByFolder(ctx, &folder.Folder{ + ID: folderID, + OrgID: user.GetOrgID(), + }, user.GetOrgID(), user) if err != nil { return err } diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index 29cc7819fca..5be5ff4dede 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -539,7 +539,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilterUIDs is set to existing folders, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder") + newFolder := createFolder(t, sc, "NewFolder", nil) // nolint:staticcheck command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) @@ -608,7 +608,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to a nonexistent folders, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder") + newFolder := createFolder(t, sc, "NewFolder", nil) // nolint:staticcheck command := getCreatePanelCommand(newFolder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index e59a19e29b6..8e7dce686ea 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -24,7 +24,7 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel that exists, it should succeed", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder") + newFolder := createFolder(t, sc, "NewFolder", nil) cmd := model.PatchLibraryElementCommand{ FolderID: newFolder.ID, // nolint:staticcheck FolderUID: &newFolder.UID, @@ -91,7 +91,7 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with folder only, it should change folder successfully and return correct result", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder") + newFolder := createFolder(t, sc, "NewFolder", nil) cmd := model.PatchLibraryElementCommand{ FolderID: newFolder.ID, // nolint:staticcheck FolderUID: &newFolder.UID, @@ -335,7 +335,7 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with a folder where a library panel with the same name already exists, it should fail", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder") + newFolder := createFolder(t, sc, "NewFolder", nil) // nolint:staticcheck command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel") sc.ctx.Req.Body = mockRequestBody(command) diff --git a/pkg/services/libraryelements/libraryelements_permissions_test.go b/pkg/services/libraryelements/libraryelements_permissions_test.go index a07092269ad..f9af4ed15ad 100644 --- a/pkg/services/libraryelements/libraryelements_permissions_test.go +++ b/pkg/services/libraryelements/libraryelements_permissions_test.go @@ -38,7 +38,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", nil) // nolint:staticcheck command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) @@ -56,7 +56,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", nil) command := getCreatePanelCommand(0, "", "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) @@ -178,7 +178,7 @@ func TestLibraryElementCreatePermissions(t *testing.T) { for _, testCase := range accessCases { testScenario(t, testCase.desc, func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", nil) sc.reqContext.SignedInUser.Permissions = map[int64]map[string][]string{ 1: testCase.permissions, } @@ -235,14 +235,14 @@ func TestLibraryElementPatchPermissions(t *testing.T) { for _, testCase := range accessCases { testScenario(t, testCase.desc, func(t *testing.T, sc scenarioContext) { - fromFolder := createFolder(t, sc, "FromFolder") + fromFolder := createFolder(t, sc, "FromFolder", nil) // nolint:staticcheck command := getCreatePanelCommand(fromFolder.ID, fromFolder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) - toFolder := createFolder(t, sc, "ToFolder") + toFolder := createFolder(t, sc, "ToFolder", nil) sc.reqContext.SignedInUser.Permissions = map[int64]map[string][]string{ 1: testCase.permissions, @@ -268,6 +268,7 @@ func TestLibraryElementDeletePermissions(t *testing.T) { desc: "can delete library elements when granted write access to the correct folder", permissions: map[string][]string{ dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Folder")}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Folder")}, }, status: http.StatusOK, }, @@ -275,6 +276,7 @@ func TestLibraryElementDeletePermissions(t *testing.T) { desc: "can delete library elements when granted write access to all folders", permissions: map[string][]string{ dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, }, status: http.StatusOK, }, @@ -282,6 +284,7 @@ func TestLibraryElementDeletePermissions(t *testing.T) { desc: "can't delete library elements when granted write access to the wrong folder", permissions: map[string][]string{ dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Other_folder")}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Other_folder")}, }, status: http.StatusForbidden, }, @@ -297,7 +300,7 @@ func TestLibraryElementDeletePermissions(t *testing.T) { for _, testCase := range accessCases { testScenario(t, testCase.desc, func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", sc.service.folderService) // nolint:staticcheck command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) @@ -327,7 +330,7 @@ func TestLibraryElementsWithMissingFolders(t *testing.T) { testScenario(t, "When a user tries to patch a library panel by moving it to a folder that doesn't exist, it should fail", func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", nil) // nolint:staticcheck command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) @@ -368,7 +371,7 @@ func TestLibraryElementsGetPermissions(t *testing.T) { for _, testCase := range getCases { testScenario(t, testCase.desc, func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", nil) // nolint:staticcheck cmd := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel") sc.reqContext.Req.Body = mockRequestBody(cmd) @@ -419,7 +422,7 @@ func TestLibraryElementsGetAllPermissions(t *testing.T) { testScenario(t, testCase.desc, func(t *testing.T, sc scenarioContext) { for i := 1; i <= 2; i++ { - folder := createFolder(t, sc, fmt.Sprintf("Folder%d", i)) + folder := createFolder(t, sc, fmt.Sprintf("Folder%d", i), nil) // nolint:staticcheck cmd := getCreatePanelCommand(folder.ID, folder.UID, fmt.Sprintf("Library Panel %d", i)) sc.reqContext.Req.Body = mockRequestBody(cmd) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 6dcdacb026c..9debfc717d5 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -32,7 +32,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" - "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" @@ -99,7 +98,7 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) { scenarioWithPanel(t, "When an admin tries to delete a folder uid that doesn't exist, it should fail", func(t *testing.T, sc scenarioContext) { err := sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.UID+"xxxx") - require.EqualError(t, err, dashboards.ErrFolderNotFound.Error()) + require.EqualError(t, err, guardian.ErrGuardianFolderNotFound.Errorf("failed to get folder by UID: %w", dashboards.ErrFolderNotFound).Error()) }) scenarioWithPanel(t, "When an admin tries to delete a folder that contains disconnected elements, it should delete all disconnected elements too", @@ -300,17 +299,19 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash require.NoError(t, err) ac := actest.FakeAccessControl{ExpectedEvaluate: true} folderPermissions := acmock.NewMockedPermissionsService() + folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) dashboardPermissions := acmock.NewMockedPermissionsService() dashboardPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - var expectedFolder *folder.Folder - if dash.FolderUID != "" || dash.FolderID != 0 { // nolint:staticcheck - expectedFolder = &folder.Folder{ID: folderID, UID: folderUID} - } + fStore := folderimpl.ProvideStore(sqlStore) + folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, + folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) + _, err = folderSvc.Create(context.Background(), &folder.CreateFolderCommand{UID: folderUID, SignedInUser: &user, Title: folderUID + "-title"}) + require.NoError(t, err) service, err := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, ac, - &foldertest.FakeService{ExpectedFolder: expectedFolder}, + folderSvc, folder.NewFakeStore(), nil, nil, @@ -327,22 +328,24 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash return dashboard } -func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder { +func createFolder(t *testing.T, sc scenarioContext, title string, folderSvc folder.Service) *folder.Folder { t.Helper() - features := featuremgmt.WithFeatures() - cfg := setting.NewCfg() - ac := actest.FakeAccessControl{ExpectedEvaluate: true} - dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore)) - require.NoError(t, err) + if folderSvc == nil { + features := featuremgmt.WithFeatures() + cfg := setting.NewCfg() + ac := actest.FakeAccessControl{ExpectedEvaluate: true} + dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore)) + require.NoError(t, err) - folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) - store := folderimpl.ProvideStore(sc.sqlStore) - s := folderimpl.ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, - features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) - t.Logf("Creating folder with title and UID %q", title) + folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) + store := folderimpl.ProvideStore(sc.sqlStore) + folderSvc = folderimpl.ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, + features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) + t.Logf("Creating folder with title and UID %q", title) + } ctx := identity.WithRequester(context.Background(), &sc.user) - folder, err := s.Create(ctx, &folder.CreateFolderCommand{ + folder, err := folderSvc.Create(ctx, &folder.CreateFolderCommand{ OrgID: sc.user.OrgID, Title: title, UID: title, SignedInUser: &sc.user, }) require.NoError(t, err) @@ -399,15 +402,18 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena folderPermissions := acmock.NewMockedPermissionsService() dashboardPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) + fStore := folderimpl.ProvideStore(sqlStore) + folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, + folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) dashboardService, svcErr := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, ac, - foldertest.NewFakeService(), folder.NewFakeStore(), + folderSvc, fStore, nil, nil, nil, nil, quotaService, nil, ) require.NoError(t, svcErr) dashboardService.RegisterDashboardPermissions(dashboardPermissions) - guardian.InitAccessControlGuardian(cfg, ac, dashboardService) + guardian.InitAccessControlGuardian(cfg, ac, dashboardService, folderSvc, log.NewNopLogger()) testScenario(t, desc, func(t *testing.T, sc scenarioContext) { // nolint:staticcheck @@ -462,23 +468,23 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) dashboardPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) + fStore := folderimpl.ProvideStore(sqlStore) + folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, + folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) dashService, dashSvcErr := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, ac, - foldertest.NewFakeService(), folder.NewFakeStore(), + folderSvc, fStore, nil, nil, nil, nil, quotaService, nil, ) require.NoError(t, dashSvcErr) dashService.RegisterDashboardPermissions(dashboardPermissions) - guardian.InitAccessControlGuardian(cfg, ac, dashService) - fStore := folderimpl.ProvideStore(sqlStore) - folderSrv := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracer), dashboardStore, folderStore, sqlStore, - features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) + guardian.InitAccessControlGuardian(cfg, ac, dashService, folderSvc, log.NewNopLogger()) service := LibraryElementService{ Cfg: cfg, features: featuremgmt.WithFeatures(), SQLStore: sqlStore, - folderService: folderSrv, + folderService: folderSvc, } // deliberate difference between signed in user and user in db to make it crystal clear @@ -510,7 +516,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo }, } - sc.folder = createFolder(t, sc, "ScenarioFolder") + sc.folder = createFolder(t, sc, "ScenarioFolder", folderSvc) fn(t, sc) }) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 80baeead8d2..9c030cc9061 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/kinds/librarypanel" @@ -823,18 +824,19 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo ac := actest.FakeAccessControl{ExpectedEvaluate: true} dashStore := &dashboards.FakeDashboardStore{} - dashStore.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 1}, nil) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) dashPermissionService := acmock.NewMockedPermissionsService() + folderSvc := foldertest.NewFakeService() + folderSvc.ExpectedFolder = &folder.Folder{ID: 1} dashService, err := dashboardservice.ProvideDashboardServiceImpl( cfg, dashStore, folderStore, features, acmock.NewMockedPermissionsService(), ac, - foldertest.NewFakeService(), folder.NewFakeStore(), + folderSvc, folder.NewFakeStore(), nil, nil, nil, nil, quotaService, nil, ) require.NoError(t, err) dashService.RegisterDashboardPermissions(dashPermissionService) - guardian.InitAccessControlGuardian(cfg, ac, dashService) + guardian.InitAccessControlGuardian(cfg, ac, dashService, folderSvc, log.NewNopLogger()) dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) require.NoError(t, err) From a4ef1f76e47abc69fa5ea6264de80e4768850757 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:40:22 -0500 Subject: [PATCH 033/894] Dashboards: Backend schema version migration (#99392) --- pkg/apis/dashboard/migration/migrate.go | 30 ++++ pkg/apis/dashboard/migration/migrate_test.go | 104 ++++++++++++++ .../migration/schemaversion/errors.go | 40 ++++++ .../migration/schemaversion/migrations.go | 31 ++++ .../schemaversion/migrations_test.go | 57 ++++++++ .../dashboard/migration/schemaversion/v40.go | 9 ++ .../migration/schemaversion/v40_test.go | 70 +++++++++ .../testdata/input/39.refresh_true.json | 134 ++++++++++++++++++ .../testdata/output/39.refresh_true.40.json | 134 ++++++++++++++++++ pkg/apis/dashboard/v1alpha1/conversion.go | 23 ++- 10 files changed, 625 insertions(+), 7 deletions(-) create mode 100644 pkg/apis/dashboard/migration/migrate.go create mode 100644 pkg/apis/dashboard/migration/migrate_test.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/errors.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/migrations.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/migrations_test.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/v40.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/v40_test.go create mode 100644 pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json diff --git a/pkg/apis/dashboard/migration/migrate.go b/pkg/apis/dashboard/migration/migrate.go new file mode 100644 index 00000000000..36a4679f191 --- /dev/null +++ b/pkg/apis/dashboard/migration/migrate.go @@ -0,0 +1,30 @@ +package migration + +import "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" + +func Migrate(dash map[string]interface{}, targetVersion int) error { + if dash == nil { + dash = map[string]interface{}{} + } + inputVersion := schemaversion.GetSchemaVersion(dash) + dash["schemaVersion"] = inputVersion + + if inputVersion < schemaversion.MINIUM_VERSION { + return schemaversion.NewMinimumVersionError(inputVersion) + } + + for nextVersion := inputVersion + 1; nextVersion <= targetVersion; nextVersion++ { + if migration, ok := schemaversion.Migrations[nextVersion]; ok { + if err := migration(dash); err != nil { + return schemaversion.NewMigrationError("migration failed", inputVersion, nextVersion) + } + dash["schemaVersion"] = nextVersion + } + } + + if schemaversion.GetSchemaVersion(dash) != targetVersion { + return schemaversion.NewMigrationError("schema version not migrated to target version", inputVersion, targetVersion) + } + + return nil +} diff --git a/pkg/apis/dashboard/migration/migrate_test.go b/pkg/apis/dashboard/migration/migrate_test.go new file mode 100644 index 00000000000..af6a1305ea3 --- /dev/null +++ b/pkg/apis/dashboard/migration/migrate_test.go @@ -0,0 +1,104 @@ +package migration_test + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/apis/dashboard/migration" + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" +) + +const INPUT_DIR = "testdata/input" +const OUTPUT_DIR = "testdata/output" + +func TestMigrate(t *testing.T) { + files, err := os.ReadDir(INPUT_DIR) + require.NoError(t, err) + + t.Run("minimum version check", func(t *testing.T) { + err := migration.Migrate(map[string]interface{}{ + "schemaVersion": schemaversion.MINIUM_VERSION - 1, + }, schemaversion.MINIUM_VERSION) + + var minVersionErr = schemaversion.NewMinimumVersionError(schemaversion.MINIUM_VERSION - 1) + require.ErrorAs(t, err, &minVersionErr) + }) + + for _, f := range files { + if f.IsDir() { + continue + } + + inputDash, inputVersion, name := load(t, filepath.Join(INPUT_DIR, f.Name())) + + t.Run("input check "+f.Name(), func(t *testing.T) { + // use input version as the target version to ensure there are no changes + require.NoError(t, migration.Migrate(inputDash, inputVersion), "input check migration failed") + outBytes, err := json.MarshalIndent(inputDash, "", " ") + require.NoError(t, err, "failed to marshal migrated dashboard") + // We can ignore gosec G304 here since it's a test + // nolint:gosec + expectedDash, err := os.ReadFile(filepath.Join(INPUT_DIR, f.Name())) + require.NoError(t, err, "failed to read expected output file") + require.JSONEq(t, string(expectedDash), string(outBytes), "%s input check did not match", f.Name()) + }) + + for targetVersion := range schemaversion.Migrations { + testName := fmt.Sprintf("%s v%d to v%d", name, inputVersion, targetVersion) + t.Run(testName, func(t *testing.T) { + testMigration(t, f, targetVersion) + }) + } + } +} + +func testMigration(t *testing.T, file fs.DirEntry, targetVersion int) { + t.Helper() + dash, inputVersion, name := load(t, filepath.Join(INPUT_DIR, file.Name())) + require.NoError(t, migration.Migrate(dash, targetVersion), "%d migration failed", targetVersion) + + outPath := filepath.Join(OUTPUT_DIR, fmt.Sprintf("%d.%s.%d.json", inputVersion, name, targetVersion)) + outBytes, err := json.MarshalIndent(dash, "", " ") + require.NoError(t, err, "failed to marshal migrated dashboard") + + if _, err := os.Stat(outPath); os.IsNotExist(err) { + err = os.WriteFile(outPath, outBytes, 0644) + require.NoError(t, err, "failed to write new output file", outPath) + return + } + + // We can ignore gosec G304 here since it's a test + // nolint:gosec + existingBytes, err := os.ReadFile(outPath) + require.NoError(t, err, "failed to read existing output file") + require.JSONEq(t, string(existingBytes), string(outBytes), "%s did not match", outPath) +} + +func parseInputName(t *testing.T, name string) (int, string) { + t.Helper() + parts := strings.SplitN(filepath.Base(name), ".", 3) + if len(parts) < 3 { + t.Fatalf("invalid input filename: %s", name) + } + iv, err := strconv.Atoi(parts[0]) + require.NoError(t, err, "failed to parse input version") + return iv, parts[1] +} + +func load(t *testing.T, path string) (dash map[string]interface{}, inputVersion int, name string) { + // We can ignore gosec G304 here since it's a test + // nolint:gosec + inputBytes, err := os.ReadFile(path) + require.NoError(t, err, "failed to read embedded input file") + require.NoError(t, json.Unmarshal(inputBytes, &dash), "failed to unmarshal dashboard JSON") + inputVersion, name = parseInputName(t, path) + return dash, inputVersion, name +} diff --git a/pkg/apis/dashboard/migration/schemaversion/errors.go b/pkg/apis/dashboard/migration/schemaversion/errors.go new file mode 100644 index 00000000000..110a596a1ad --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/errors.go @@ -0,0 +1,40 @@ +package schemaversion + +import "fmt" + +var _ error = &MinimumVersionError{} +var _ error = &MigrationError{} + +// MinimumVersionError is an error that is returned when the schema version is below the minimum version. +func NewMinimumVersionError(inputVersion int) *MinimumVersionError { + return &MinimumVersionError{inputVersion: inputVersion} +} + +// MinimumVersionError is an error type for minimum version errors. +type MinimumVersionError struct { + inputVersion int +} + +func (e *MinimumVersionError) Error() string { + return fmt.Errorf("input schema version is below minimum version. input: %d minimum: %d", e.inputVersion, MINIUM_VERSION).Error() +} + +// ErrMigrationFailed is an error that is returned when a migration fails. +func NewMigrationError(msg string, currentVersion, targetVersion int) *MigrationError { + return &MigrationError{ + msg: msg, + targetVersion: targetVersion, + currentVersion: currentVersion, + } +} + +// MigrationError is an error type for migration errors. +type MigrationError struct { + msg string + targetVersion int + currentVersion int +} + +func (e *MigrationError) Error() string { + return fmt.Errorf("schema migration from version %d to %d failed: %v", e.currentVersion, e.targetVersion, e.msg).Error() +} diff --git a/pkg/apis/dashboard/migration/schemaversion/migrations.go b/pkg/apis/dashboard/migration/schemaversion/migrations.go new file mode 100644 index 00000000000..1b8925e2885 --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/migrations.go @@ -0,0 +1,31 @@ +package schemaversion + +import "strconv" + +type SchemaVersionMigrationFunc func(map[string]interface{}) error + +const ( + MINIUM_VERSION = 39 + LATEST_VERSION = 40 +) + +var Migrations = map[int]SchemaVersionMigrationFunc{ + 40: V40, +} + +func GetSchemaVersion(dash map[string]interface{}) int { + if v, ok := dash["schemaVersion"]; ok { + switch v := v.(type) { + case int: + return v + case float64: + return int(v) + case string: + if version, err := strconv.Atoi(v); err == nil { + return version + } + return 0 + } + } + return 0 +} diff --git a/pkg/apis/dashboard/migration/schemaversion/migrations_test.go b/pkg/apis/dashboard/migration/schemaversion/migrations_test.go new file mode 100644 index 00000000000..79be968d23b --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/migrations_test.go @@ -0,0 +1,57 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" + "github.com/stretchr/testify/require" +) + +func TestGetSchemaVersion(t *testing.T) { + tests := []struct { + name string + dash map[string]interface{} + expected int + }{ + { + name: "schemaVersion as int", + dash: map[string]interface{}{ + "schemaVersion": 16, + }, + expected: 16, + }, + { + name: "schemaVersion as float64", + dash: map[string]interface{}{ + "schemaVersion": 40.2345, + }, + expected: 40, + }, + { + name: "schemaVersion is not set", + dash: map[string]interface{}{}, + expected: 0, + }, + { + name: "schemaVersion as string int", + dash: map[string]interface{}{ + "schemaVersion": "5", + }, + expected: 5, + }, + { + name: "schemaVersion as invalid string", + dash: map[string]interface{}{ + "schemaVersion": "foo", + }, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := schemaversion.GetSchemaVersion(tt.dash) + require.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/apis/dashboard/migration/schemaversion/v40.go b/pkg/apis/dashboard/migration/schemaversion/v40.go new file mode 100644 index 00000000000..3336ee1c1fd --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/v40.go @@ -0,0 +1,9 @@ +package schemaversion + +func V40(dash map[string]interface{}) error { + dash["schemaVersion"] = int(40) + if _, ok := dash["refresh"].(string); !ok { + dash["refresh"] = "" + } + return nil +} diff --git a/pkg/apis/dashboard/migration/schemaversion/v40_test.go b/pkg/apis/dashboard/migration/schemaversion/v40_test.go new file mode 100644 index 00000000000..19e6c90aa79 --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/v40_test.go @@ -0,0 +1,70 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" + "github.com/stretchr/testify/require" +) + +func TestV40(t *testing.T) { + tests := []migrationTestCase{ + { + name: "refresh not set", + input: map[string]interface{}{ + "title": "Test Dashboard", + }, + expected: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 40, + "refresh": "", + }, + }, + { + name: "boolean refresh value is converted to an empty string", + input: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 39, + "refresh": true, + }, + expected: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 40, + "refresh": "", + }, + }, + { + name: "string refresh value is not converted", + input: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 39, + "refresh": "1m", + }, + expected: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 40, + "refresh": "1m", + }, + }, + } + + runMigrationTests(t, tests, schemaversion.V40) +} + +type migrationTestCase struct { + name string + input map[string]interface{} + expected map[string]interface{} +} + +func runMigrationTests(t *testing.T, testCases []migrationTestCase, migrationFunc schemaversion.SchemaVersionMigrationFunc) { + t.Helper() + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + err := migrationFunc(tt.input) + require.NoError(t, err) + require.Equal(t, tt.expected, tt.input) + }) + } +} diff --git a/pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json b/pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json new file mode 100644 index 00000000000..4ea2be531f9 --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json @@ -0,0 +1,134 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "preload": false, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "", + "refresh": true, + "schemaVersion": 39 + } \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json b/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json new file mode 100644 index 00000000000..a8c67a1e80d --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json @@ -0,0 +1,134 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/v1alpha1/conversion.go b/pkg/apis/dashboard/v1alpha1/conversion.go index cc512e7c38c..8e21a1c62c5 100644 --- a/pkg/apis/dashboard/v1alpha1/conversion.go +++ b/pkg/apis/dashboard/v1alpha1/conversion.go @@ -1,26 +1,35 @@ package v1alpha1 import ( + "errors" + conversion "k8s.io/apimachinery/pkg/conversion" klog "k8s.io/klog/v2" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/apis/dashboard/migration" + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" ) func Convert_v0alpha1_Unstructured_To_v1alpha1_DashboardSpec(in *common.Unstructured, out *DashboardSpec, s conversion.Scope) error { - out.Unstructured = *in - - t, ok := in.Object["title"] - if !ok { - return nil // skip setting the title if it's not in the unstructured object + err := migration.Migrate(in.Object, schemaversion.LATEST_VERSION) + if err != nil { + minErr := &schemaversion.MinimumVersionError{} + if errors.As(err, &minErr) { + in.Object["__migrationError"] = err.Error() + } else { + return err + } } - title, ok := t.(string) + out.Unstructured = *in + + t, ok := in.Object["title"].(string) if !ok { klog.V(5).Infof("unstructured dashboard title field is not a string %v", t) return nil // skip setting the title if it's not a string in the unstructured object } - out.Title = title + out.Title = t return nil } From 75e72366876a7f751a042fb6e298361b692c25b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:41:27 +0000 Subject: [PATCH 034/894] Update dependency type-fest to v4.33.0 (#99463) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9abbcd7ada9..74a61b40636 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29564,9 +29564,9 @@ __metadata: linkType: hard "type-fest@npm:^4.18.2, type-fest@npm:^4.26.1": - version: 4.30.2 - resolution: "type-fest@npm:4.30.2" - checksum: 10/c5168b159c366e4fd5b74c7f7b786bed9248c03f67e6e07d52dd5d51354447468fa7c92b9f2142c7fe9279814031f783959370242c3520de848931b65ddb48bb + version: 4.33.0 + resolution: "type-fest@npm:4.33.0" + checksum: 10/0d179e66fa765bd0a25a785b12dc797f90f2f92bdb8c9c8a789f3fd8e5a4492444e7ef83551b3b8463aeab24fd6195761e26b03174722de636b4b75aa5726fb7 languageName: node linkType: hard From 6b227bb3742ac79b33a4d7eedf4f5bce36c1ca11 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 23 Jan 2025 18:05:23 +0100 Subject: [PATCH 035/894] Prometheus: Handle non-json errors in a better way (#99342) * handle json errors in a better way * update comments * update unit tests * Update pkg/promlib/converter/prom.go Co-authored-by: Arve Knudsen * Update pkg/promlib/querydata/response_test.go Co-authored-by: Arve Knudsen * Update pkg/promlib/querydata/response_test.go Co-authored-by: Arve Knudsen * Update pkg/promlib/querydata/response_test.go Co-authored-by: Arve Knudsen * update import --------- Co-authored-by: Arve Knudsen --- pkg/promlib/converter/prom.go | 5 +- pkg/promlib/querydata/response.go | 66 +++++++++++++++++++------- pkg/promlib/querydata/response_test.go | 51 +++++++++++++++++--- 3 files changed, 97 insertions(+), 25 deletions(-) diff --git a/pkg/promlib/converter/prom.go b/pkg/promlib/converter/prom.go index 2d0a35524ca..09cd20086df 100644 --- a/pkg/promlib/converter/prom.go +++ b/pkg/promlib/converter/prom.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" sdkjsoniter "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" + "github.com/grafana/grafana-plugin-sdk-go/experimental/status" jsoniter "github.com/json-iterator/go" ) @@ -23,7 +24,7 @@ type Options struct { } func rspErr(e error) backend.DataResponse { - return backend.DataResponse{Error: e} + return backend.DataResponse{Error: e, ErrorSource: status.SourceDownstream} } // ReadPrometheusStyleResult will read results from a prometheus or loki server and return data frames @@ -39,7 +40,7 @@ func ReadPrometheusStyleResult(jIter *jsoniter.Iterator, opt Options) backend.Da l1Fields: for l1Field, err := iter.ReadObject(); ; l1Field, err = iter.ReadObject() { if err != nil { - return rspErr(err) + return rspErr(fmt.Errorf("response from prometheus couldn't be parsed. it is non-json: %w", err)) } switch l1Field { case "status": diff --git a/pkg/promlib/querydata/response.go b/pkg/promlib/querydata/response.go index 93cff2080dc..853510916d5 100644 --- a/pkg/promlib/querydata/response.go +++ b/pkg/promlib/querydata/response.go @@ -3,6 +3,7 @@ package querydata import ( "context" "fmt" + "io" "net/http" "sort" "strings" @@ -28,28 +29,59 @@ func (s *QueryData) parseResponse(ctx context.Context, q *models.Query, res *htt ctx, endSpan := utils.StartTrace(ctx, s.tracer, "datasource.prometheus.parseResponse") defer endSpan() - iter := jsoniter.Parse(jsoniter.ConfigDefault, res.Body, 1024) - r := converter.ReadPrometheusStyleResult(iter, converter.Options{Dataplane: true}) - r.Status = backend.Status(res.StatusCode) + statusCode := res.StatusCode - // Add frame to attach metadata - if len(r.Frames) == 0 && !q.ExemplarQuery { - r.Frames = append(r.Frames, data.NewFrame("")) - } + switch { + // Status codes that Prometheus might return + // so we want to parse the response + // https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview + case statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices, + statusCode == http.StatusBadRequest, + statusCode == http.StatusUnprocessableEntity, + statusCode == http.StatusServiceUnavailable: - // The ExecutedQueryString can be viewed in QueryInspector in UI - for i, frame := range r.Frames { - addMetadataToMultiFrame(q, frame) - if i == 0 { - frame.Meta.ExecutedQueryString = executedQueryString(q) + iter := jsoniter.Parse(jsoniter.ConfigDefault, res.Body, 1024) + r := converter.ReadPrometheusStyleResult(iter, converter.Options{Dataplane: true}) + r.Status = backend.Status(res.StatusCode) + + // Add frame to attach metadata + if len(r.Frames) == 0 && !q.ExemplarQuery { + r.Frames = append(r.Frames, data.NewFrame("")) } - } - if r.Error == nil { - r = s.processExemplars(ctx, q, r) - } + // The ExecutedQueryString can be viewed in QueryInspector in UI + for i, frame := range r.Frames { + addMetadataToMultiFrame(q, frame) + if i == 0 { + frame.Meta.ExecutedQueryString = executedQueryString(q) + } + } - return r + if r.Error == nil { + r = s.processExemplars(ctx, q, r) + } + + return r + default: + // Unknown status code. We don't want to parse the response. + const maxBodySize = 1024 + lr := io.LimitReader(res.Body, maxBodySize) + tb, _ := io.ReadAll(lr) + + s.log.FromContext(ctx).Error("Unexpected response received", "status", statusCode, "body", tb) + + errResp := backend.DataResponse{ + Error: fmt.Errorf("unexpected response with status code %d: %s", statusCode, tb), + ErrorSource: backend.ErrorSourceFromHTTPStatus(statusCode), + } + + f := data.NewFrame("") + addMetadataToMultiFrame(q, f) + f.Meta.ExecutedQueryString = executedQueryString(q) + errResp.Frames = append(errResp.Frames, f) + + return errResp + } } func (s *QueryData) processExemplars(ctx context.Context, q *models.Query, dr backend.DataResponse) backend.DataResponse { diff --git a/pkg/promlib/querydata/response_test.go b/pkg/promlib/querydata/response_test.go index 6d12465fee8..44264641324 100644 --- a/pkg/promlib/querydata/response_test.go +++ b/pkg/promlib/querydata/response_test.go @@ -7,7 +7,9 @@ import ( "net/http" "testing" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/promlib/models" "github.com/grafana/grafana/pkg/promlib/querydata/exemplar" @@ -18,7 +20,7 @@ func TestQueryData_parseResponse(t *testing.T) { t.Run("resultType is before result the field must parsed normally", func(t *testing.T) { resBody := `{"data":{"resultType":"vector", "result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Nil(t, result.Error) assert.Len(t, result.Frames, 1) @@ -26,7 +28,7 @@ func TestQueryData_parseResponse(t *testing.T) { t.Run("resultType is after the result field must parsed normally", func(t *testing.T) { resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}],"resultType":"vector"},"status":"success"}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Nil(t, result.Error) assert.Len(t, result.Frames, 1) @@ -34,7 +36,7 @@ func TestQueryData_parseResponse(t *testing.T) { t.Run("no resultType is existed in the data", func(t *testing.T) { resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Error(t, result.Error) assert.Equal(t, result.Error.Error(), "no resultType found") @@ -42,7 +44,7 @@ func TestQueryData_parseResponse(t *testing.T) { t.Run("resultType is set as empty string before result", func(t *testing.T) { resBody := `{"data":{"resultType":"", "result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Error(t, result.Error) assert.Equal(t, result.Error.Error(), "unknown result type: ") @@ -50,7 +52,7 @@ func TestQueryData_parseResponse(t *testing.T) { t.Run("resultType is set as empty string after result", func(t *testing.T) { resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}],"resultType":""},"status":"success"}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Error(t, result.Error) assert.Equal(t, result.Error.Error(), "unknown result type: ") @@ -61,10 +63,47 @@ func TestAddMetadataToMultiFrame(t *testing.T) { t.Run("when you have native histogram result", func(t *testing.T) { qd := QueryData{exemplarSampler: exemplar.NewStandardDeviationSampler} resBody := `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"__name__":"rpc_durations_native_histogram_seconds","instance":"nativehisto:8080","job":"prometheus"},"histograms":[[1729529685,{"count":"7243102","sum":"72460202.93145595","buckets":[[0,"1.8340080864093422","2","10"],[0,"2","2.1810154653305154","68"]]}],[1729529700,{"count":"7243490","sum":"72464056.03309634","buckets":[[0,"1.8340080864093422","2","10"],[0,"2","2.1810154653305154","68"]]}],[1729529715,{"count":"7243880","sum":"72467935.35871512","buckets":[[0,"1.8340080864093422","2","10"],[0,"2","2.1810154653305154","68"]]}]]}]}}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Nil(t, result.Error) assert.Len(t, result.Frames, 1) assert.Equal(t, "yMin", result.Frames[0].Fields[1].Name) }) } + +// Helper function to create mock HTTP response. +func createMockResponse(statusCode int, body string) *http.Response { + return &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(bytes.NewReader([]byte(body))), + } +} + +func TestParseResponse_ErrorCases(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + statusCode int + body string + }{ + {"500 Internal Server Error", http.StatusInternalServerError, `{"error":"internal server error"}`}, + {"404 Not Found", http.StatusNotFound, `{"error":"not found"}`}, + {"401 Unauthorized", http.StatusUnauthorized, `{"error":"unauthorized"}`}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := createMockResponse(tt.statusCode, tt.body) + q := &models.Query{} + qd := QueryData{exemplarSampler: exemplar.NewStandardDeviationSampler} + qd.log = log.New() + resp := qd.parseResponse(ctx, q, res) + + require.Error(t, resp.Error) + assert.Contains(t, resp.Error.Error(), "unexpected response") + assert.Len(t, resp.Frames, 1) + assert.NoError(t, res.Body.Close()) + }) + } +} From d24e7c126ddb107515e9a36ab1285c05cc639fe2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:21:11 +0000 Subject: [PATCH 036/894] Update dependency yaml to v2.7.0 (#99466) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 74a61b40636..c85beb509fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31128,11 +31128,11 @@ __metadata: linkType: hard "yaml@npm:^2.0.0, yaml@npm:^2.3.4": - version: 2.6.1 - resolution: "yaml@npm:2.6.1" + version: 2.7.0 + resolution: "yaml@npm:2.7.0" bin: yaml: bin.mjs - checksum: 10/cf412f03a33886db0a3aac70bb4165588f4c5b3c6f8fc91520b71491e5537800b6c2c73ed52015617f6e191eb4644c73c92973960a1999779c62a200ee4c231d + checksum: 10/c8c314c62fbd49244a6a51b06482f6d495b37ab10fa685fcafa1bbaae7841b7233ee7d12cab087bcca5a0b28adc92868b6e437322276430c28d00f1c1732eeec languageName: node linkType: hard From e9d9b152954deb5571bab368655186facf5a24f6 Mon Sep 17 00:00:00 2001 From: Kristina Date: Thu, 23 Jan 2025 12:21:24 -0600 Subject: [PATCH 037/894] StateTimeline / StatusHistory: Add axis visibility and width controls (#98548) * Allow setting the y axis width * Add to docs * Add to status history as well * Add to status history docs and schema * Change config to come from generic axis builder * keep axis * Change overridden label * Update docs/sources/panels-visualizations/visualizations/status-history/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/panels-visualizations/visualizations/state-timeline/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Remove the category/label override * Move axis to its own section in docs as well * clean * rename to addAxisWidth * Apply suggestions from code review Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Move sections to match UI order * Update docs/sources/shared/visualizations/axis-options-all.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Change other axis options doc to be consistent. * Fix linter * add AxisPlacement * Add new placement option to docs * change some wording * Apply suggestions from code review Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --------- Co-authored-by: Leon Sorokin Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --- .../visualizations/state-timeline/index.md | 4 + .../visualizations/status-history/index.md | 4 + .../visualizations/time-series/index.md | 16 +--- .../shared/visualizations/axis-options-all.md | 21 +++++ .../axis-options-state-status.md | 10 +++ .../x/StateTimelinePanelCfg_types.gen.ts | 2 +- .../x/StatusHistoryPanelCfg_types.gen.ts | 2 +- .../grafana-ui/src/options/builder/axis.tsx | 88 +++++++++++-------- .../core/components/TimelineChart/utils.ts | 14 ++- public/app/plugins/panel/barchart/module.tsx | 2 +- .../state-timeline/StateTimelinePanel.tsx | 20 +++-- .../plugins/panel/state-timeline/module.tsx | 7 +- .../plugins/panel/state-timeline/panelcfg.cue | 1 + .../panel/state-timeline/panelcfg.gen.ts | 2 +- .../status-history/StatusHistoryPanel.tsx | 27 ++++-- .../plugins/panel/status-history/module.tsx | 7 +- .../plugins/panel/status-history/panelcfg.cue | 1 + .../panel/status-history/panelcfg.gen.ts | 2 +- 18 files changed, 149 insertions(+), 81 deletions(-) create mode 100644 docs/sources/shared/visualizations/axis-options-all.md create mode 100644 docs/sources/shared/visualizations/axis-options-state-status.md diff --git a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md index 1ef99435336..a6916eb1f83 100644 --- a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md +++ b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md @@ -149,6 +149,10 @@ The **Page size** option lets you paginate the state timeline visualization to l {{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" leveloffset="+1" >}} +### Axis options + +{{< docs/shared lookup="visualizations/axis-options-state-status.md" source="grafana" version="" leveloffset="+1" >}} + ### Standard options {{< docs/shared lookup="visualizations/standard-options.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 b7825b94b82..3ca5df864e6 100644 --- a/docs/sources/panels-visualizations/visualizations/status-history/index.md +++ b/docs/sources/panels-visualizations/visualizations/status-history/index.md @@ -131,6 +131,10 @@ Controls the opacity of state regions. {{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" >}} +## Axis options + +{{< docs/shared lookup="visualizations/axis-options-state-status.md" source="grafana" version="" leveloffset="+1" >}} + ## Standard options {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/time-series/index.md b/docs/sources/panels-visualizations/visualizations/time-series/index.md index e7f21000554..e54509d5aeb 100644 --- a/docs/sources/panels-visualizations/visualizations/time-series/index.md +++ b/docs/sources/panels-visualizations/visualizations/time-series/index.md @@ -209,21 +209,7 @@ The following example shows three series: Min, Max, and Value. The Min and Max s ### Axis options -Options under the **Axis** section control how the x- and y-axes are rendered. Some options don't take effect until you click outside of the field option box you're editing. You can also press `Enter`. - -| Option | Description | -| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Time zone | Set the desired time zones to display along the x-axis. | -| [Placement](#placement) | Select the placement of the y-axis. | -| Label | Set a y-axis text label. If you have more than one y-axis, then you can assign different labels using an override. | -| Width | Set a fixed width of the axis. By default, Grafana dynamically calculates the width of an axis. By setting the width of the axis, data with different axes types can share the same display proportions. This setting makes it easier for you to compare more than one graph’s worth of data because the axes aren't shifted or stretched within visual proximity to each other. | -| Show grid lines | Set the axis grid line visibility.
| -| Color | Set the color of the axis. | -| Show border | Set the axis border visibility. | -| Scale | Set the y-axis values scale.
| -| Centered zero | Set the y-axis so it's centered on zero. | -| [Soft min](#soft-min-and-soft-max) | Set a soft min to better control the y-axis limits. zero. | -| [Soft max](#soft-min-and-soft-max) | Set a soft max to better control the y-axis limits. zero. | +{{< docs/shared lookup="visualizations/axis-options-all.md" source="grafana" version="" leveloffset="+1" >}} #### Placement diff --git a/docs/sources/shared/visualizations/axis-options-all.md b/docs/sources/shared/visualizations/axis-options-all.md new file mode 100644 index 00000000000..e2c833ab9e2 --- /dev/null +++ b/docs/sources/shared/visualizations/axis-options-all.md @@ -0,0 +1,21 @@ +--- +title: Axis options +comments: | + This file is used in the following visualizations: time series. +--- + +Options under the **Axis** section control how the x- and y-axes are rendered. Some options don't take effect until you click outside of the field option box you're editing. You can also press `Enter`. + +| Option | Description | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Time zone | Set the desired time zones to display along the x-axis. | +| [Placement](#placement) | Select the placement of the y-axis. | +| Label | Set a y-axis text label. If you have more than one y-axis, then you can assign different labels using an override. | +| Width | Set a fixed width for the axis. By default, Grafana dynamically calculates the width of an axis. | +| Show grid lines | Set the axis grid line visibility.
| +| Color | Set the color of the axis. | +| Show border | Set the axis border visibility. | +| Scale | Set the y-axis values scale.
| +| Centered zero | Set the y-axis so it's centered on zero. | +| [Soft min](#soft-min-and-soft-max) | Set a soft min to better control the y-axis limits. zero. | +| [Soft max](#soft-min-and-soft-max) | Set a soft max to better control the y-axis limits. zero. | diff --git a/docs/sources/shared/visualizations/axis-options-state-status.md b/docs/sources/shared/visualizations/axis-options-state-status.md new file mode 100644 index 00000000000..8b1bacfd78c --- /dev/null +++ b/docs/sources/shared/visualizations/axis-options-state-status.md @@ -0,0 +1,10 @@ +--- +title: Axis options +comments: | + This file is used in the following visualizations: state timeline, status history. +--- + +| Option | Description | +| --------- | ------------------------------------------------------------------------------------------------ | +| Placement | Control the visibility of series names along the y-axis or time values along the x-axis. | +| Width | Set a fixed width for the axis. By default, Grafana dynamically calculates the width of an axis. | diff --git a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts index 72c3097c014..a62b8181e42 100644 --- a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts @@ -43,7 +43,7 @@ export const defaultOptions: Partial = { showValue: ui.VisibilityMode.Auto, }; -export interface FieldConfig extends ui.HideableFieldConfig { +export interface FieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { fillOpacity?: number; lineWidth?: number; } diff --git a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts index 8b0b21a3723..1efeeb3dec4 100644 --- a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts @@ -33,7 +33,7 @@ export const defaultOptions: Partial = { showValue: ui.VisibilityMode.Auto, }; -export interface FieldConfig extends ui.HideableFieldConfig { +export interface FieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { fillOpacity?: number; lineWidth?: number; } diff --git a/packages/grafana-ui/src/options/builder/axis.tsx b/packages/grafana-ui/src/options/builder/axis.tsx index 2e08654298c..304dc78268a 100644 --- a/packages/grafana-ui/src/options/builder/axis.tsx +++ b/packages/grafana-ui/src/options/builder/axis.tsx @@ -14,49 +14,32 @@ import { Stack } from '../../components/Layout/Stack/Stack'; import { Select } from '../../components/Select/Select'; import { graphFieldOptions } from '../../components/uPlot/config'; +const category = ['Axis']; + /** * @alpha */ -export function addAxisConfig( - builder: FieldConfigEditorBuilder, - defaultConfig: AxisConfig, - hideScale?: boolean -) { - const category = ['Axis']; - +export function addAxisConfig(builder: FieldConfigEditorBuilder, defaultConfig: AxisConfig) { // options for axis appearance + addAxisPlacement(builder); + + builder.addTextInput({ + path: 'axisLabel', + name: 'Label', + category, + defaultValue: '', + settings: { + placeholder: 'Optional text', + expandTemplateVars: true, + }, + showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden, + // Do not apply default settings to time and string fields which are used as x-axis fields in Time series and Bar chart panels + shouldApply: (f) => f.type !== FieldType.time && f.type !== FieldType.string, + }); + + addAxisWidth(builder); + builder - .addRadio({ - path: 'axisPlacement', - name: 'Placement', - category, - defaultValue: graphFieldOptions.axisPlacement[0].value, - settings: { - options: graphFieldOptions.axisPlacement, - }, - }) - .addTextInput({ - path: 'axisLabel', - name: 'Label', - category, - defaultValue: '', - settings: { - placeholder: 'Optional text', - expandTemplateVars: true, - }, - showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden, - // Do not apply default settings to time and string fields which are used as x-axis fields in Time series and Bar chart panels - shouldApply: (f) => f.type !== FieldType.time && f.type !== FieldType.string, - }) - .addNumberInput({ - path: 'axisWidth', - name: 'Width', - category, - settings: { - placeholder: 'Auto', - }, - showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden, - }) .addRadio({ path: 'axisGridShow', name: 'Show grid lines', @@ -209,3 +192,32 @@ export const ScaleDistributionEditor = ({ value, onChange }: StandardEditorProps ); }; + +/** @internal */ +export function addAxisWidth(builder: FieldConfigEditorBuilder) { + builder.addNumberInput({ + path: 'axisWidth', + name: 'Width', + category, + settings: { + placeholder: 'Auto', + }, + showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden, + }); +} + +/** @internal */ +export function addAxisPlacement( + builder: FieldConfigEditorBuilder, + optionsFilter = (placement: AxisPlacement) => true +) { + builder.addRadio({ + path: 'axisPlacement', + name: 'Placement', + category, + defaultValue: graphFieldOptions.axisPlacement[0].value, + settings: { + options: graphFieldOptions.axisPlacement.filter((placement) => optionsFilter(placement.value!)), + }, + }); +} diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 0057b21331d..5a727990631 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -51,6 +51,7 @@ interface UPlotConfigOptions { mergeValues?: boolean; getValueColor: (frameIdx: number, fieldIdx: number, value: unknown) => string; hoverMulti: boolean; + axisWidth?: number; } /** @@ -161,25 +162,32 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( range: coreConfig.yRange, }); + const xAxisHidden = frame.fields[0].config.custom.axisPlacement === AxisPlacement.Hidden; + builder.addAxis({ + show: !xAxisHidden, scaleKey: xScaleKey, isTime: true, splits: coreConfig.xSplits!, placement: AxisPlacement.Bottom, timeZone: timeZones[0], theme, - grid: { show: true }, }); + const yCustomConfig = frame.fields[1].config.custom; + const yAxisWidth = yCustomConfig.axisWidth; + const yAxisHidden = yCustomConfig.axisPlacement === AxisPlacement.Hidden; + builder.addAxis({ scaleKey: FIXED_UNIT, // y isTime: false, placement: AxisPlacement.Left, splits: coreConfig.ySplits, - values: coreConfig.yValues, + values: yAxisHidden ? (u, splits) => splits.map((v) => null) : coreConfig.yValues, grid: { show: false }, ticks: { show: false }, - gap: 16, + gap: yAxisHidden ? 0 : 16, + size: yAxisHidden ? 0 : yAxisWidth, theme, }); diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index 35d1f095828..5b5ae542531 100644 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -103,7 +103,7 @@ export const plugin = new PanelPlugin(BarChartPanel) shouldApply: () => true, }); - commonOptionsBuilder.addAxisConfig(builder, cfg, false); + commonOptionsBuilder.addAxisConfig(builder, cfg); commonOptionsBuilder.addHideFrom(builder); }, }) diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 81e272a6b98..4c57d757a05 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -4,6 +4,7 @@ import { useMeasure } from 'react-use'; import { DashboardCursorSync, DataFrame, PanelProps } from '@grafana/data'; import { + AxisPlacement, EventBusPlugin, Pagination, TooltipDisplayMode, @@ -209,15 +210,16 @@ export const StateTimelinePanel = ({ maxWidth={options.tooltip.maxWidth} /> )} - {/* Renders annotations */} - + {alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && ( + + )} ); diff --git a/public/app/plugins/panel/state-timeline/module.tsx b/public/app/plugins/panel/state-timeline/module.tsx index 972194fc52d..024d1e883ac 100644 --- a/public/app/plugins/panel/state-timeline/module.tsx +++ b/public/app/plugins/panel/state-timeline/module.tsx @@ -5,7 +5,7 @@ import { identityOverrideProcessor, PanelPlugin, } from '@grafana/data'; -import { VisibilityMode } from '@grafana/schema'; +import { AxisPlacement, VisibilityMode } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; import { InsertNullsEditor } from '../timeseries/InsertNullsEditor'; @@ -76,6 +76,11 @@ export const plugin = new PanelPlugin(StateTimelinePanel) }); commonOptionsBuilder.addHideFrom(builder); + commonOptionsBuilder.addAxisPlacement( + builder, + (placement) => placement === AxisPlacement.Auto || placement === AxisPlacement.Hidden + ); + commonOptionsBuilder.addAxisWidth(builder); }, }) .setPanelOptions((builder) => { diff --git a/public/app/plugins/panel/state-timeline/panelcfg.cue b/public/app/plugins/panel/state-timeline/panelcfg.cue index a186f5e12e8..5a9c34e2a28 100644 --- a/public/app/plugins/panel/state-timeline/panelcfg.cue +++ b/public/app/plugins/panel/state-timeline/panelcfg.cue @@ -41,6 +41,7 @@ composableKinds: PanelCfg: { perPage?: number & >=1 | *20 } @cuetsy(kind="interface") FieldConfig: { + ui.AxisConfig ui.HideableFieldConfig lineWidth?: uint32 & <=10 | *0 fillOpacity?: uint32 & <=100 | *70 diff --git a/public/app/plugins/panel/state-timeline/panelcfg.gen.ts b/public/app/plugins/panel/state-timeline/panelcfg.gen.ts index 0fc25fe65b8..5ea1b58af54 100644 --- a/public/app/plugins/panel/state-timeline/panelcfg.gen.ts +++ b/public/app/plugins/panel/state-timeline/panelcfg.gen.ts @@ -41,7 +41,7 @@ export const defaultOptions: Partial = { showValue: ui.VisibilityMode.Auto, }; -export interface FieldConfig extends ui.HideableFieldConfig { +export interface FieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { fillOpacity?: number; lineWidth?: number; } diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index fd1164c85f3..237f1469723 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -1,7 +1,14 @@ import { useMemo, useState } from 'react'; import { DashboardCursorSync, PanelProps } from '@grafana/data'; -import { EventBusPlugin, TooltipDisplayMode, TooltipPlugin2, usePanelContext, useTheme2 } from '@grafana/ui'; +import { + AxisPlacement, + EventBusPlugin, + TooltipDisplayMode, + TooltipPlugin2, + usePanelContext, + useTheme2, +} from '@grafana/ui'; import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart'; import { @@ -141,14 +148,16 @@ export const StatusHistoryPanel = ({ maxWidth={options.tooltip.maxWidth} /> )} - + {alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && ( + + )} ); diff --git a/public/app/plugins/panel/status-history/module.tsx b/public/app/plugins/panel/status-history/module.tsx index 8d42b1c4d1b..58ff47be783 100644 --- a/public/app/plugins/panel/status-history/module.tsx +++ b/public/app/plugins/panel/status-history/module.tsx @@ -1,5 +1,5 @@ import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data'; -import { VisibilityMode } from '@grafana/schema'; +import { AxisPlacement, VisibilityMode } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; import { StatusHistoryPanel } from './StatusHistoryPanel'; @@ -42,6 +42,11 @@ export const plugin = new PanelPlugin(StatusHistoryPanel) }); commonOptionsBuilder.addHideFrom(builder); + commonOptionsBuilder.addAxisPlacement( + builder, + (placement) => placement === AxisPlacement.Auto || placement === AxisPlacement.Hidden + ); + commonOptionsBuilder.addAxisWidth(builder); }, }) .setPanelOptions((builder) => { diff --git a/public/app/plugins/panel/status-history/panelcfg.cue b/public/app/plugins/panel/status-history/panelcfg.cue index ec5ab6c4fc7..55a96b501d2 100644 --- a/public/app/plugins/panel/status-history/panelcfg.cue +++ b/public/app/plugins/panel/status-history/panelcfg.cue @@ -37,6 +37,7 @@ composableKinds: PanelCfg: { colWidth?: float & <=1 | *0.9 } @cuetsy(kind="interface") FieldConfig: { + ui.AxisConfig ui.HideableFieldConfig lineWidth?: uint32 & <=10 | *1 fillOpacity?: uint32 & <=100 | *70 diff --git a/public/app/plugins/panel/status-history/panelcfg.gen.ts b/public/app/plugins/panel/status-history/panelcfg.gen.ts index 8bc46e363d0..fac1c7e19df 100644 --- a/public/app/plugins/panel/status-history/panelcfg.gen.ts +++ b/public/app/plugins/panel/status-history/panelcfg.gen.ts @@ -31,7 +31,7 @@ export const defaultOptions: Partial = { showValue: ui.VisibilityMode.Auto, }; -export interface FieldConfig extends ui.HideableFieldConfig { +export interface FieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { fillOpacity?: number; lineWidth?: number; } From 572be19f760640a8d3463b0aa1b1b50e60d76c34 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:47:42 -0600 Subject: [PATCH 038/894] DataLinks: Add one-click option (#98563) Co-authored-by: Leon Sorokin --- .../grafana-data/src/field/fieldOverrides.ts | 2 + packages/grafana-data/src/types/dataLink.ts | 3 ++ .../components/DataLinks/DataLinkEditor.tsx | 16 +++++- .../DataLinksInlineEditor.tsx | 9 ++++ .../DataLinksListItem.tsx | 11 +++- .../VizTooltip/VizTooltipFooter.tsx | 34 ++++++++++-- .../uPlot/plugins/TooltipPlugin2.tsx | 54 ++++++++++++++----- .../features/actions/ActionsInlineEditor.tsx | 2 +- .../app/plugins/panel/status-history/utils.ts | 2 + .../panel/timeseries/TimeSeriesTooltip.tsx | 14 +++-- public/locales/en-US/grafana.json | 11 +++- public/locales/pseudo-LOCALE/grafana.json | 11 +++- 12 files changed, 139 insertions(+), 30 deletions(-) diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 0924a417231..7e9aa1052aa 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -501,6 +501,7 @@ export const getLinksSupplier = }); }, origin: field, + oneClick: link.oneClick ?? false, }; } else { linkModel = { @@ -508,6 +509,7 @@ export const getLinksSupplier = title: replaceVariables(link.title || '', dataLinkScopedVars), target: link.targetBlank ? '_blank' : undefined, origin: field, + oneClick: link.oneClick ?? false, }; } diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 476f2441ed7..14ee3ad4301 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -55,6 +55,8 @@ export interface DataLink { correlationData?: ExploreCorrelationHelperData; transformations?: DataLinkTransformationConfig[]; }; + + oneClick?: boolean; } /** @@ -98,6 +100,7 @@ export interface LinkModel { // When a click callback exists, this is passed the raw mouse|react event onClick?: (e: any, origin?: any) => void; + oneClick?: boolean; } /** diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx index 98f5d67227f..cdd5e64eddf 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx @@ -5,7 +5,7 @@ import { VariableSuggestion, GrafanaTheme2, DataLink } from '@grafana/data'; import { useStyles2 } from '../../themes/index'; import { isCompactUrl } from '../../utils/dataLinks'; -import { Trans } from '../../utils/i18n'; +import { t, Trans } from '../../utils/i18n'; import { Field } from '../Forms/Field'; import { Input } from '../Input/Input'; import { Switch } from '../Switch/Switch'; @@ -45,6 +45,10 @@ export const DataLinkEditor = memo(({ index, value, onChange, suggestions, isLas onChange(index, { ...value, targetBlank: !value.targetBlank }); }; + const onOneClickChanged = () => { + onChange(index, { ...value, oneClick: !value.oneClick }); + }; + return (
@@ -63,6 +67,16 @@ export const DataLinkEditor = memo(({ index, value, onChange, suggestions, isLas + + + + {isLast && (
diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx index a431e241479..fa4e61589f3 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx @@ -51,6 +51,15 @@ export const DataLinksInlineEditor = ({ setIsNew(false); } } + + if (link.oneClick === true) { + linksSafe.forEach((link) => { + if (link.oneClick) { + link.oneClick = false; + } + }); + } + const update = cloneDeep(linksSafe); update[index] = link; onChange(update); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx index 939a4ec15b7..1c12a41dd2a 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx @@ -5,6 +5,8 @@ import { DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../themes'; import { isCompactUrl } from '../../../utils'; +import { t } from '../../../utils/i18n'; +import { Badge } from '../../Badge/Badge'; import { Icon } from '../../Icon/Icon'; import { IconButton } from '../../IconButton/IconButton'; import { Tooltip } from '../../Tooltip/Tooltip'; @@ -22,7 +24,7 @@ export interface DataLinksListItemProps { export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: DataLinksListItemProps) => { const styles = useStyles2(getDataLinkListItemStyles); - const { title = '', url = '' } = link; + const { title = '', url = '', oneClick = false } = link; const hasTitle = title.trim() !== ''; const hasUrl = url.trim() !== ''; @@ -52,6 +54,13 @@ export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: Da
+ {oneClick && ( + + )}
diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index 05b05f7518d..c3c4a34eb64 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { ActionModel, Field, GrafanaTheme2, LinkModel } from '@grafana/data'; -import { Button, DataLinkButton, Stack } from '..'; +import { Button, DataLinkButton, Icon, Stack } from '..'; import { useStyles2 } from '../../themes'; import { Trans } from '../../utils/i18n'; import { ActionButton } from '../Actions/ActionButton'; @@ -16,6 +16,21 @@ interface VizTooltipFooterProps { export const ADD_ANNOTATION_ID = 'add-annotation-button'; const renderDataLinks = (dataLinks: LinkModel[], styles: ReturnType) => { + const oneClickLink = dataLinks.find((link) => link.oneClick === true); + + if (oneClickLink != null) { + return ( + + + + + Click to open {{ linkTitle: oneClickLink.title }} + + + + ); + } + return ( {dataLinks.map((link, i) => ( @@ -35,14 +50,15 @@ const renderActions = (actions: ActionModel[]) => { ); }; -export const VizTooltipFooter = ({ dataLinks, actions, annotate }: VizTooltipFooterProps) => { +export const VizTooltipFooter = ({ dataLinks, actions = [], annotate }: VizTooltipFooterProps) => { const styles = useStyles2(getStyles); + const hasOneClickLink = dataLinks.some((link) => link.oneClick === true); return (
- {dataLinks?.length > 0 &&
{renderDataLinks(dataLinks, styles)}
} - {actions && actions.length > 0 &&
{renderActions(actions)}
} - {annotate != null && ( + {dataLinks.length > 0 &&
{renderDataLinks(dataLinks, styles)}
} + {!hasOneClickLink && actions.length > 0 &&
{renderActions(actions)}
} + {!hasOneClickLink && annotate != null && (
)} {showTime && {timestamp}} diff --git a/public/app/features/logs/components/LogRowMessage.tsx b/public/app/features/logs/components/LogRowMessage.tsx index f771d9a2bb6..d3788b3c7cf 100644 --- a/public/app/features/logs/components/LogRowMessage.tsx +++ b/public/app/features/logs/components/LogRowMessage.tsx @@ -157,6 +157,7 @@ export const LogRowMessage = memo((props: Props) => { [raw, prettifyLogMessage, wrapLogMessage, expanded] ); const shouldShowMenu = mouseIsOver || pinned; + return ( <> { @@ -165,9 +166,9 @@ export const LogRowMessage = memo((props: Props) => { }
- +
diff --git a/public/app/features/logs/components/getLogRowStyles.ts b/public/app/features/logs/components/getLogRowStyles.ts index 9b965b65ea1..8605753487e 100644 --- a/public/app/features/logs/components/getLogRowStyles.ts +++ b/public/app/features/logs/components/getLogRowStyles.ts @@ -100,6 +100,11 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { cursor: 'pointer', verticalAlign: 'top', + '&:focus-within': { + outline: `2px solid ${theme.colors.primary.border}`, + outlineOffset: '-2px', + }, + '&:hover': { '.log-row-menu': { zIndex: 1, @@ -139,7 +144,6 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { logsRowToggleDetails: css({ label: 'logs-row-toggle-details__level', fontSize: '9px', - paddingTop: '5px', maxWidth: '15px', }), logsRowLocalTime: css({ @@ -221,6 +225,29 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { backgroundColor: hoverBgColor, }, }), + detailsToggle: css({ + appearance: 'none', + background: 'none', + border: 'none', + padding: 0, + // Don't increase the height of the row + maxHeight: '19px', + + // Don't show default button box-shadow on focus, we apply outline to the entire row instead + '&:focus-visible': { + boxShadow: 'none', + }, + + '&:focus': { + outline: 0, + }, + '&:after': { + content: '""', + inset: 0, + position: 'absolute', + }, + }), + // Log row topVerticalAlign: css({ label: 'topVerticalAlign', @@ -287,9 +314,6 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { }, }), logLine: css({ - backgroundColor: 'transparent', - border: 'none', - diplay: 'inline', fontFamily: theme.typography.fontFamilyMonospace, fontSize: theme.typography.bodySmall.fontSize, letterSpacing: theme.typography.bodySmall.letterSpacing, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 979924df896..f8d8d6cf171 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1815,7 +1815,8 @@ }, "log-row-message": { "ellipsis": "… ", - "more": "more" + "more": "more", + "see-details": "See log details" }, "log-rows": { "disable-popover": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index d155653d0ed..a485a54fac9 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1815,7 +1815,8 @@ }, "log-row-message": { "ellipsis": "… ", - "more": "mőřę" + "more": "mőřę", + "see-details": "Ŝęę ľőģ đęŧäįľş" }, "log-rows": { "disable-popover": { From 060182a3ba65b3ad3f9de80b95c389554694c52d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:07:47 +0000 Subject: [PATCH 112/894] Update dependency ol-ext to v4.0.25 (#99606) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index acef18520a7..f2897992c57 100644 --- a/package.json +++ b/package.json @@ -355,7 +355,7 @@ "nanoid": "^5.0.4", "node-forge": "^1.3.1", "ol": "7.4.0", - "ol-ext": "4.0.24", + "ol-ext": "4.0.25", "pluralize": "^8.0.0", "prismjs": "1.29.0", "rc-slider": "11.1.8", diff --git a/yarn.lock b/yarn.lock index 40b998fd7f0..63a684dcd8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17970,7 +17970,7 @@ __metadata: node-notifier: "npm:10.0.1" nx: "npm:19.8.2" ol: "npm:7.4.0" - ol-ext: "npm:4.0.24" + ol-ext: "npm:4.0.25" pluralize: "npm:^8.0.0" postcss: "npm:8.5.1" postcss-loader: "npm:8.1.1" @@ -23335,12 +23335,12 @@ __metadata: languageName: node linkType: hard -"ol-ext@npm:4.0.24": - version: 4.0.24 - resolution: "ol-ext@npm:4.0.24" +"ol-ext@npm:4.0.25": + version: 4.0.25 + resolution: "ol-ext@npm:4.0.25" peerDependencies: ol: ">= 5.3.0" - checksum: 10/76ab4ee468429171be7495998c0ae80853733f9ff6241dfd7b66208f265e180357bb80c304b2d185c50a4afa635a36ed8ed67fcd5374b49d790e71b9be039df9 + checksum: 10/e3c8282fc67d9511b37c540f97217594a28f16b7ebeee5baf828eb56fdefa024fad6c1f846ac3cfd093f3b1e2cab5b6a56a97087152389fe4e35a2eb9b42f415 languageName: node linkType: hard From be8396cafa92c06566144dc363ba56e1b8a5c5c6 Mon Sep 17 00:00:00 2001 From: William Assis <35489495+gassiss@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:32:07 -0300 Subject: [PATCH 113/894] Setup legacy search based on mode (#98908) --- .../apis/dashboard/legacy/sql_dashboards.go | 18 +- pkg/registry/apis/dashboard/legacy/storage.go | 36 +--- .../dashboard/legacysearcher/search_client.go | 128 ++++++++++++++ pkg/registry/apis/dashboard/search.go | 7 +- pkg/registry/apis/dashboard/search_test.go | 160 ++++++++++++++++++ .../apis/dashboard/v0alpha1/register.go | 4 +- pkg/services/apiserver/client/client.go | 9 +- .../dashboards/service/dashboard_service.go | 2 +- pkg/setting/setting.go | 2 + pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/search_client.go | 20 +++ 11 files changed, 339 insertions(+), 49 deletions(-) create mode 100644 pkg/registry/apis/dashboard/legacysearcher/search_client.go create mode 100644 pkg/storage/unified/resource/search_client.go diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index c3a712a8811..999586b13a1 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" dashboard "github.com/grafana/grafana/pkg/apis/dashboard" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/dashboards" @@ -54,8 +55,9 @@ type dashboardSqlAccess struct { provisioning provisioning.ProvisioningService // Use for writing (not reading) - dashStore dashboards.Store - softDelete bool + dashStore dashboards.Store + softDelete bool + dashboardSearchClient legacysearcher.DashboardSearchClient // Typically one... the server wrapper subscribers []chan *resource.WrittenEvent @@ -68,12 +70,14 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, provisioning provisioning.ProvisioningService, softDelete bool, ) DashboardAccess { + dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore) return &dashboardSqlAccess{ - sql: sql, - namespacer: namespacer, - dashStore: dashStore, - provisioning: provisioning, - softDelete: softDelete, + sql: sql, + namespacer: namespacer, + dashStore: dashStore, + provisioning: provisioning, + softDelete: softDelete, + dashboardSearchClient: *dashboardSearchClient, } } diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index 0f88fdf15a5..e5f51bf78f9 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "strings" "time" claims "github.com/grafana/authlib/types" @@ -255,9 +254,8 @@ func (a *dashboardSqlAccess) Read(ctx context.Context, req *resource.ReadRequest return a.ReadResource(ctx, req), nil } -// TODO: this needs to be implemented func (a *dashboardSqlAccess) Search(ctx context.Context, req *resource.ResourceSearchRequest) (*resource.ResourceSearchResponse, error) { - return nil, fmt.Errorf("not yet (filter)") + return a.dashboardSearchClient.Search(ctx, req) } func (a *dashboardSqlAccess) ListRepositoryObjects(ctx context.Context, req *resource.ListRepositoryObjectsRequest) (*resource.ListRepositoryObjectsResponse, error) { @@ -270,35 +268,5 @@ func (a *dashboardSqlAccess) CountRepositoryObjects(context.Context, *resource.C // GetStats implements ResourceServer. func (a *dashboardSqlAccess) GetStats(ctx context.Context, req *resource.ResourceStatsRequest) (*resource.ResourceStatsResponse, error) { - info, err := claims.ParseNamespace(req.Namespace) - if err != nil { - return nil, fmt.Errorf("unable to read namespace") - } - if info.OrgID == 0 { - return nil, fmt.Errorf("invalid OrgID found in namespace") - } - - if len(req.Kinds) != 1 { - return nil, fmt.Errorf("only can query for dashboard kind in legacy fallback") - } - - parts := strings.SplitN(req.Kinds[0], "/", 2) - if len(parts) != 2 { - return nil, fmt.Errorf("invalid kind") - } - - count, err := a.dashStore.CountInOrg(ctx, info.OrgID) - if err != nil { - return nil, err - } - - return &resource.ResourceStatsResponse{ - Stats: []*resource.ResourceStatsResponse_Stats{ - { - Group: parts[0], - Resource: parts[1], - Count: count, - }, - }, - }, nil + return a.dashboardSearchClient.GetStats(ctx, req) } diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go new file mode 100644 index 00000000000..ac221ba86e2 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -0,0 +1,128 @@ +package legacysearcher + +import ( + "context" + "fmt" + "strings" + + claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "google.golang.org/grpc" +) + +type DashboardSearchClient struct { + resource.ResourceIndexClient + dashboardStore dashboards.Store +} + +func NewDashboardSearchClient(dashboardStore dashboards.Store) *DashboardSearchClient { + return &DashboardSearchClient{dashboardStore: dashboardStore} +} + +func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) { + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + + if req.Query == "*" { + req.Query = "" + } + + // TODO add missing support for the following query params: + // - tag + // - starred (won't support) + // - page (check) + // - type + // - sort + // - deleted + // - permission + // - dashboardIds + // - dashboardUIDs + // - folderIds + // - folderUIDs + // - sort (default by title) + query := &dashboards.FindPersistedDashboardsQuery{ + Title: req.Query, + Limit: req.Limit, + // FolderUIDs: req.FolderUIDs, + SignedInUser: user, + } + + // TODO need to test this + // emptyResponse, err := a.dashService.GetSharedDashboardUIDsQuery(ctx, query) + + // if err != nil { + // return nil, err + // } else if emptyResponse { + // return nil, nil + // } + + res, err := c.dashboardStore.FindDashboards(ctx, query) + if err != nil { + return nil, err + } + + // TODO sort if query.Sort == "" see sortedHits in services/search/service.go + + searchFields := resource.StandardSearchFields() + list := &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + searchFields.Field(resource.SEARCH_FIELD_TITLE), + searchFields.Field(resource.SEARCH_FIELD_FOLDER), + // searchFields.Field(resource.SEARCH_FIELD_TAGS), + }, + }, + } + + for _, dashboard := range res { + list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{ + Key: &resource.ResourceKey{ + Namespace: "default", + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: dashboard.UID, + }, + Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID)}, // TODO add tag + }) + } + + return list, nil +} + +func (c *DashboardSearchClient) GetStats(ctx context.Context, req *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) { + info, err := claims.ParseNamespace(req.Namespace) + if err != nil { + return nil, fmt.Errorf("unable to read namespace") + } + if info.OrgID == 0 { + return nil, fmt.Errorf("invalid OrgID found in namespace") + } + + if len(req.Kinds) != 1 { + return nil, fmt.Errorf("only can query for dashboard kind in legacy fallback") + } + + parts := strings.SplitN(req.Kinds[0], "/", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid kind") + } + + count, err := c.dashboardStore.CountInOrg(ctx, info.OrgID) + if err != nil { + return nil, err + } + + return &resource.ResourceStatsResponse{ + Stats: []*resource.ResourceStatsResponse_Stats{ + { + Group: parts[0], + Resource: parts[1], + Count: count, + }, + }, + }, nil +} diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 91d9217ed98..7f866943cbe 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/builder" dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/util/errhttp" ) @@ -32,9 +33,10 @@ type SearchHandler struct { tracer trace.Tracer } -func NewSearchHandler(client resource.ResourceIndexClient, tracer trace.Tracer) *SearchHandler { +func NewSearchHandler(client resource.ResourceIndexClient, tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient) *SearchHandler { + searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, client, legacyDashboardSearcher) return &SearchHandler{ - client: client, + client: searchClient, log: log.New("grafana-apiserver.dashboards.search"), tracer: tracer, } @@ -332,7 +334,6 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { searchRequest.Options.Fields = append(searchRequest.Options.Fields, namesFilter...) } - // Run the query result, err := s.client.Search(ctx, searchRequest) if err != nil { errhttp.Write(ctx, err, w) diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index 03dcd6cd9d5..23d7ad18278 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -7,13 +7,173 @@ import ( "testing" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "google.golang.org/grpc" ) +func TestSearchFallback(t *testing.T) { + t.Run("should hit legacy search handler on mode 0", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode0}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + if mockLegacyClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + }) + + t.Run("should hit legacy search handler on mode 1", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode1}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + if mockLegacyClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + }) + + t.Run("should hit legacy search handler on mode 2", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode2}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + if mockLegacyClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + }) + + t.Run("should hit unified storage search handler on mode 3", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode3}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + if mockLegacyClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + }) + + t.Run("should hit unified storage search handler on mode 4", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode4}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + if mockLegacyClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + }) + + t.Run("should hit unified storage search handler on mode 5", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode5}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + if mockLegacyClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + }) +} + func TestSearchHandlerFields(t *testing.T) { // Create a mock client mockClient := &MockClient{} diff --git a/pkg/registry/apis/dashboard/v0alpha1/register.go b/pkg/registry/apis/dashboard/v0alpha1/register.go index a6d05ec2de0..535cb0cb08b 100644 --- a/pkg/registry/apis/dashboard/v0alpha1/register.go +++ b/pkg/registry/apis/dashboard/v0alpha1/register.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -71,6 +72,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) dbp := legacysql.NewDatabaseProvider(sql) namespacer := request.GetNamespaceMapper(cfg) + legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore) builder := &DashboardsAPIBuilder{ log: log.New("grafana-apiserver.dashboards.v0alpha1"), DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{ @@ -80,7 +82,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, features: features, accessControl: accessControl, unified: unified, - search: dashboard.NewSearchHandler(unified, tracing), + search: dashboard.NewSearchHandler(unified, tracing, cfg, legacyDashboardSearcher), legacy: &dashboard.DashboardStorage{ Resource: dashboardv0alpha1.DashboardResourceInfo, diff --git a/pkg/services/apiserver/client/client.go b/pkg/services/apiserver/client/client.go index 8d054fc7a9b..2e25ca07ee2 100644 --- a/pkg/services/apiserver/client/client.go +++ b/pkg/services/apiserver/client/client.go @@ -12,8 +12,11 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher" "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" k8sUser "k8s.io/apiserver/pkg/authentication/user" k8sRequest "k8s.io/apiserver/pkg/endpoints/request" @@ -40,12 +43,14 @@ type k8sHandler struct { searcher resource.ResourceIndexClient } -func NewK8sHandler(namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient) K8sHandler { +func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient, dashStore dashboards.Store) K8sHandler { + legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore) + searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, searcher, legacySearcher) return &k8sHandler{ namespacer: namespacer, gvr: gvr, restConfigProvider: restConfigProvider, - searcher: searcher, + searcher: searchClient, } } diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 0f2957ee0ce..3a46f051a7d 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -91,7 +91,7 @@ func ProvideDashboardServiceImpl( restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper, ) (*DashboardServiceImpl, error) { - k8sHandler := client.NewK8sHandler(request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified) + k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore) dashSvc := &DashboardServiceImpl{ cfg: cfg, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 934fa3152a1..c78e455fa24 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -540,6 +540,8 @@ type Cfg struct { HttpsSkipVerify bool } +const UnifiedStorageConfigKeyDashboard = "dashboards.dashboard.grafana.app" + type UnifiedStorageConfig struct { DualWriterMode rest.DualWriterMode DualWriterPeriodicDataSyncJobEnabled bool diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index b70c1800e01..c2b1383e9c9 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -17,6 +17,7 @@ require ( github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible github.com/grafana/grafana-plugin-sdk-go v0.263.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d + github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/prometheus/client_golang v1.20.5 @@ -120,7 +121,6 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.29.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/pkg/apiserver v0.0.0-20250121113133-e747350fee2d // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/grafana/sqlds/v4 v4.1.3 // indirect diff --git a/pkg/storage/unified/resource/search_client.go b/pkg/storage/unified/resource/search_client.go new file mode 100644 index 00000000000..43e0faa7b1e --- /dev/null +++ b/pkg/storage/unified/resource/search_client.go @@ -0,0 +1,20 @@ +package resource + +import ( + "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/setting" +) + +func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient ResourceIndexClient, legacyClient ResourceIndexClient) ResourceIndexClient { + config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey] + if !ok { + return legacyClient + } + + switch config.DualWriterMode { + case rest.Mode0, rest.Mode1, rest.Mode2: + return legacyClient + default: + return unifiedClient + } +} From 8954800d37befde76e599448969865976c1cf77c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:34:18 +0000 Subject: [PATCH 114/894] Update dependency react-window-infinite-loader to v1.0.10 (#99607) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index f2897992c57..9224c0f5d0d 100644 --- a/package.json +++ b/package.json @@ -387,7 +387,7 @@ "react-virtual": "2.10.4", "react-virtualized-auto-sizer": "1.0.25", "react-window": "1.8.11", - "react-window-infinite-loader": "1.0.9", + "react-window-infinite-loader": "1.0.10", "react-zoom-pan-pinch": "^3.3.0", "reduce-reducers": "^1.0.4", "redux": "5.0.1", diff --git a/yarn.lock b/yarn.lock index 63a684dcd8b..739b90b7afa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18010,7 +18010,7 @@ __metadata: react-virtual: "npm:2.10.4" react-virtualized-auto-sizer: "npm:1.0.25" react-window: "npm:1.8.11" - react-window-infinite-loader: "npm:1.0.9" + react-window-infinite-loader: "npm:1.0.10" react-zoom-pan-pinch: "npm:^3.3.0" reduce-reducers: "npm:^1.0.4" redux: "npm:5.0.1" @@ -26553,13 +26553,13 @@ __metadata: languageName: node linkType: hard -"react-window-infinite-loader@npm:1.0.9": - version: 1.0.9 - resolution: "react-window-infinite-loader@npm:1.0.9" +"react-window-infinite-loader@npm:1.0.10": + version: 1.0.10 + resolution: "react-window-infinite-loader@npm:1.0.10" peerDependencies: - react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 - react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 - checksum: 10/4a11ad949443cd76f9304d0558516c53474f53ca370e54bc29c2c362657b43545c89aec3e7df42d8e8511df379aed79b6fdd2c8fc52e25a9559b64e41ac7aefe + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/4f4c097a2948f8da71d13199289d85c89720e41888ea50039d2b8e6d7cf160300e97f66421c53420d8cb95b3ece74940c07ebd23bddd7a705a679d7e2dc2957e languageName: node linkType: hard From 7ebe599389e895ab76c72445cda6008305483015 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Mon, 27 Jan 2025 11:35:30 -0700 Subject: [PATCH 115/894] Library elements: do not error if dashboard is not found (#99608) --- pkg/services/libraryelements/database.go | 3 +++ pkg/services/libraryelements/libraryelements_test.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index e07f3a08470..c68ee55c2a1 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -722,6 +722,9 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser i } ds, err := l.dashboardsService.GetDashboardUIDByID(c, &dashboards.GetDashboardRefByIDQuery{ID: connection.ConnectionID}) if err != nil { + if errors.Is(err, dashboards.ErrDashboardNotFound) { + continue + } return err } connections = append(connections, model.LibraryElementConnectionDTO{ diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index ef2876736d9..fd7bb559a12 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -166,6 +166,10 @@ func TestGetLibraryPanelConnections(t *testing.T) { err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) + // add a connection where the dashboard doesn't exist. Shouldn't be returned in the list + err = sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 99999999) + require.NoError(t, err) + var expected = func(res model.LibraryElementConnectionsResponse) model.LibraryElementConnectionsResponse { return model.LibraryElementConnectionsResponse{ Result: []model.LibraryElementConnectionDTO{ From 32ae292334f6893d61433f73ea6d50de8dcf1752 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 19:07:34 +0000 Subject: [PATCH 116/894] Update dependency rudder-sdk-js to v2.48.44 (#99609) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 9224c0f5d0d..b7c6dc24ac1 100644 --- a/package.json +++ b/package.json @@ -223,7 +223,7 @@ "react-select-event": "5.5.1", "redux-mock-store": "1.5.5", "rimraf": "6.0.1", - "rudder-sdk-js": "2.48.43", + "rudder-sdk-js": "2.48.44", "sass": "1.83.4", "sass-loader": "16.0.4", "smtp-tester": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 739b90b7afa..4c42d41e41d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18019,7 +18019,7 @@ __metadata: regenerator-runtime: "npm:0.14.1" reselect: "npm:5.1.1" rimraf: "npm:6.0.1" - rudder-sdk-js: "npm:2.48.43" + rudder-sdk-js: "npm:2.48.44" rxjs: "npm:7.8.1" sass: "npm:1.83.4" sass-loader: "npm:16.0.4" @@ -27482,10 +27482,10 @@ __metadata: languageName: node linkType: hard -"rudder-sdk-js@npm:2.48.43": - version: 2.48.43 - resolution: "rudder-sdk-js@npm:2.48.43" - checksum: 10/183888b1bd922d3e811fae5729b6a0ac93d5dbb44e8ee753ced80ae496d19c9127ef8de8908718d43ed4b33e4e9b738c3bbc562ea6cb02c050702f9b850ae2a5 +"rudder-sdk-js@npm:2.48.44": + version: 2.48.44 + resolution: "rudder-sdk-js@npm:2.48.44" + checksum: 10/538bc405c0cb7faf33b910a39d51bd6df53fbdb176d4e83c33821e7a454178b65ced216ad0f055b6dffd201ccc45cead814138e20683e15424a02fb54b894e15 languageName: node linkType: hard From d71904cb273d518e920dd0b5cc8eecdfc71efea7 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Mon, 27 Jan 2025 14:31:40 -0500 Subject: [PATCH 117/894] Alerting: Expose updated_by in rules GET APIs (#99525) --------- Signed-off-by: Yuri Tseretyan --- .../cloudmigrationimpl/cloudmigration_test.go | 12 +- pkg/services/ngalert/api/api.go | 3 + pkg/services/ngalert/api/api_ruler.go | 42 ++++- pkg/services/ngalert/api/api_ruler_test.go | 69 ++++++++ .../api/tooling/definitions/cortex-ruler.go | 11 +- pkg/services/ngalert/ngalert.go | 5 + pkg/services/ngalert/tests/util.go | 3 +- pkg/services/quota/quotaimpl/quota_test.go | 3 +- pkg/tests/api/alerting/api_ruler_test.go | 150 ++++++++++++++++-- 9 files changed, 265 insertions(+), 33 deletions(-) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 9bde512b504..257d2e23a07 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -12,6 +12,11 @@ import ( "time" "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -43,11 +48,8 @@ import ( secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes" secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" ) func Test_NoopServiceDoesNothing(t *testing.T) { @@ -874,7 +876,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf cfg, featureToggles, nil, nil, rr, sqlStore, kvStore, nil, nil, quotatest.New(false, nil), secretsService, nil, alertMetrics, mockFolder, fakeAccessControl, dashboardService, nil, bus, fakeAccessControlService, annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, - httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), + httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), usertest.NewUserServiceFake(), ) require.NoError(t, err) diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 55d29355952..811edf6e893 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -78,6 +79,7 @@ type API struct { Historian Historian Tracer tracing.Tracer AppUrl *url.URL + UserService user.Service // Hooks can be used to replace API handlers for specific paths. Hooks *Hooks @@ -135,6 +137,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { amConfigStore: api.AlertingStore, amRefresher: api.MultiOrgAlertmanager, featureManager: api.FeatureManager, + userService: api.UserService, }, ), m) api.RegisterTestingApiEndpoints(NewTestingApi( diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 55783cfe853..f0cc114d77a 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -53,6 +54,7 @@ type RulerSrv struct { cfg *setting.UnifiedAlertingSettings conditionValidator ConditionValidator authz RuleAccessControlService + userService user.Service amConfigStore AMConfigStore amRefresher AMRefresher @@ -211,7 +213,7 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *contextmodel.ReqContext, nam result := apimodels.NamespaceConfigResponse{} for groupKey, rules := range ruleGroups { - result[namespace.Fullpath] = append(result[namespace.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords)) + result[namespace.Fullpath] = append(result[namespace.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords, srv.resolveUserIdToNameFn(c.Req.Context()))) } return response.JSON(http.StatusAccepted, result) @@ -246,7 +248,7 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespa result := apimodels.RuleGroupConfigResponse{ // nolint:staticcheck - GettableRuleGroupConfig: toGettableRuleGroupConfig(finalRuleGroup, rules, provenanceRecords), + GettableRuleGroupConfig: toGettableRuleGroupConfig(finalRuleGroup, rules, provenanceRecords, srv.resolveUserIdToNameFn(c.Req.Context())), } return response.JSON(http.StatusAccepted, result) } @@ -300,7 +302,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Res srv.log.Error("Namespace not visible to the user", "user", id, "userNamespace", userNamespace, "namespace", groupKey.NamespaceUID) continue } - result[folder.Fullpath] = append(result[folder.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords)) + result[folder.Fullpath] = append(result[folder.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords, srv.resolveUserIdToNameFn(c.Req.Context()))) } return response.JSON(http.StatusOK, result) } @@ -323,7 +325,7 @@ func (srv RulerSrv) RouteGetRuleByUID(c *contextmodel.ReqContext, ruleUID string return response.ErrOrFallback(http.StatusInternalServerError, "failed to get rule provenance", err) } - result := toGettableExtendedRuleNode(rule, map[string]ngmodels.Provenance{rule.ResourceID(): provenance}) + result := toGettableExtendedRuleNode(rule, map[string]ngmodels.Provenance{rule.ResourceID(): provenance}, srv.resolveUserIdToNameFn(ctx)) return response.JSON(http.StatusOK, result) } @@ -533,7 +535,7 @@ func changesToResponse(finalChanges *store.GroupDelta) response.Response { return response.JSON(http.StatusAccepted, body) } -func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, provenanceRecords map[string]ngmodels.Provenance) apimodels.GettableRuleGroupConfig { +func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, provenanceRecords map[string]ngmodels.Provenance, userIdToName userIDToUserInfoFn) apimodels.GettableRuleGroupConfig { rules.SortByGroupIndex() ruleNodes := make([]apimodels.GettableExtendedRuleNode, 0, len(rules)) var interval time.Duration @@ -541,7 +543,7 @@ func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, prov interval = time.Duration(rules[0].IntervalSeconds) * time.Second } for _, r := range rules { - ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, provenanceRecords)) + ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, provenanceRecords, userIdToName)) } return apimodels.GettableRuleGroupConfig{ Name: groupName, @@ -550,7 +552,7 @@ func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, prov } } -func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance) apimodels.GettableExtendedRuleNode { +func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance, userIdToName userIDToUserInfoFn) apimodels.GettableExtendedRuleNode { provenance := ngmodels.ProvenanceNone if prov, exists := provenanceRecords[r.ResourceID()]; exists { provenance = prov @@ -564,6 +566,7 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[stri Condition: r.Condition, Data: ApiAlertQueriesFromAlertQueries(r.Data), Updated: r.Updated, + UpdatedBy: userIdToName(r.UpdatedBy), IntervalSeconds: r.IntervalSeconds, Version: r.Version, UID: r.UID, @@ -724,3 +727,28 @@ func (srv RulerSrv) searchAuthorizedAlertRules(ctx context.Context, q authorized } return byGroupKey, totalGroups, nil } + +type userIDToUserInfoFn func(id *ngmodels.UserUID) *apimodels.UserInfo + +// getIdentityName returns name of either user or service account +func (srv RulerSrv) resolveUserIdToNameFn(ctx context.Context) userIDToUserInfoFn { + return func(id *ngmodels.UserUID) *apimodels.UserInfo { + if id == nil { + return nil + } + u, err := srv.userService.GetByUID(ctx, &user.GetUserByUIDQuery{ + UID: string(*id), + }) + var result string + if err != nil { + srv.log.FromContext(ctx).Warn("Failed to get user by uid. Defaulting to an empty name", "uid", id, "error", err) + } + if u != nil { + result = u.NameOrFallback() + } + return &apimodels.UserInfo{ + UID: string(*id), + Name: result, + } + } +} diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index ede0a00b799..eec2d83729a 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -32,7 +32,9 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/cmputil" "github.com/grafana/grafana/pkg/web" ) @@ -357,6 +359,72 @@ func TestRouteGetRuleByUID(t *testing.T) { require.Equal(t, expectedRule.Title, result.GrafanaManagedAlert.Title) require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection) require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedNotificationsSection) + + t.Run("should resolve Updated_by with user service", func(t *testing.T) { + testcases := []struct { + desc string + UpdatedBy *models.UserUID + User *user.User + UserServiceError error + Expected *apimodels.UserInfo + }{ + { + desc: "nil if UpdatedBy is nil", + UpdatedBy: nil, + User: nil, + UserServiceError: nil, + Expected: nil, + }, + { + desc: "just UID if user is not found", + UpdatedBy: util.Pointer(models.UserUID("test-uid")), + User: nil, + UserServiceError: nil, + Expected: &apimodels.UserInfo{ + UID: "test-uid", + }, + }, + { + desc: "just UID if error", + UpdatedBy: util.Pointer(models.UserUID("test-uid")), + UserServiceError: errors.New("error"), + Expected: &apimodels.UserInfo{ + UID: "test-uid", + }, + }, + { + desc: "login if it's known user", + UpdatedBy: util.Pointer(models.UserUID("test-uid")), + User: &user.User{ + Login: "Test", + }, + UserServiceError: nil, + Expected: &apimodels.UserInfo{ + UID: "test-uid", + Name: "Test", + }, + }, + } + for _, tc := range testcases { + t.Run(tc.desc, func(t *testing.T) { + expectedRule.UpdatedBy = tc.UpdatedBy + svc := createService(ruleStore) + usvc := usertest.NewUserServiceFake() + usvc.ExpectedUser = tc.User + usvc.ExpectedError = tc.UserServiceError + svc.userService = usvc + + response := svc.RouteGetRuleByUID(req, expectedRule.UID) + + require.Equal(t, http.StatusOK, response.Status()) + result := &apimodels.GettableExtendedRuleNode{} + require.NoError(t, json.Unmarshal(response.Body(), result)) + require.NotNil(t, result) + + require.Equal(t, tc.Expected, result.GrafanaManagedAlert.UpdatedBy) + }) + } + }) }) t.Run("error when fetching rule with non-existent UID", func(t *testing.T) { @@ -659,6 +727,7 @@ func createService(store *fakes.RuleStore) *RulerSrv { amConfigStore: &fakeAMRefresher{}, amRefresher: &fakeAMRefresher{}, featureManager: featuremgmt.WithFeatures(featuremgmt.FlagGrafanaManagedRecordingRules), + userService: usertest.NewUserServiceFake(), } } diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index a6383efa0b6..502e167f10c 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -364,7 +364,7 @@ const ( type PostableExtendedRuleNode struct { // note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2) *ApiRuleNode `yaml:",inline"` - //GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"` + // GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"` GrafanaManagedAlert *PostableGrafanaRule `yaml:"grafana_alert,omitempty" json:"grafana_alert,omitempty"` } @@ -401,7 +401,7 @@ func (n *PostableExtendedRuleNode) validate() error { type GettableExtendedRuleNode struct { // note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2) *ApiRuleNode `yaml:",inline"` - //GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"` + // GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"` GrafanaManagedAlert *GettableGrafanaRule `yaml:"grafana_alert,omitempty" json:"grafana_alert,omitempty"` } @@ -541,6 +541,7 @@ type GettableGrafanaRule struct { Condition string `json:"condition" yaml:"condition"` Data []AlertQuery `json:"data" yaml:"data"` Updated time.Time `json:"updated" yaml:"updated"` + UpdatedBy *UserInfo `json:"updated_by" yaml:"updated_by"` IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` Version int64 `json:"version" yaml:"version"` UID string `json:"uid" yaml:"uid"` @@ -555,6 +556,12 @@ type GettableGrafanaRule struct { Metadata *AlertRuleMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` } +// UserInfo represents user-related information, including a unique identifier and a name. +type UserInfo struct { + UID string `json:"uid"` + Name string `json:"name"` +} + // AlertQuery represents a single query associated with an alert definition. type AlertQuery struct { // RefID is the unique identifier of the query, set by the frontend call. diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 51ad5caffe7..2270620dc76 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -49,6 +49,7 @@ import ( "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -78,6 +79,7 @@ func ProvideService( ruleStore *store.DBstore, httpClientProvider httpclient.Provider, resourcePermissions accesscontrol.ReceiverPermissionsService, + userService user.Service, ) (*AlertNG, error) { ng := &AlertNG{ Cfg: cfg, @@ -106,6 +108,7 @@ func ProvideService( store: ruleStore, httpClientProvider: httpClientProvider, ResourcePermissions: resourcePermissions, + userService: userService, } if ng.IsDisabled() { @@ -154,6 +157,7 @@ type AlertNG struct { ResourcePermissions accesscontrol.ReceiverPermissionsService annotationsRepo annotations.Repository store *store.DBstore + userService user.Service bus bus.Bus pluginsStore pluginstore.Store @@ -496,6 +500,7 @@ func (ng *AlertNG) init() error { Historian: history, Hooks: api.NewHooks(ng.Log), Tracer: ng.tracer, + UserService: ng.userService, } ng.Api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 8ea4fef13c3..0d356fabd7b 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -37,6 +37,7 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/database" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -90,7 +91,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration, opts ...TestEnvOpti ng, err := ngalert.ProvideService( cfg, options.featureToggles, nil, nil, routing.NewRouteRegister(), sqlStore, kvstore.NewFakeKVStore(), nil, nil, quotatest.New(false, nil), secretsService, nil, m, folderService, ac, &dashboards.FakeDashboardService{}, nil, bus, ac, - annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), + annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), usertest.NewUserServiceFake(), ) require.NoError(tb, err) diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 4030fe0c19b..5cf4a3acabf 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -50,6 +50,7 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -512,7 +513,7 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe _, err = ngalert.ProvideService( cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, ngalertfakes.NewFakeKVStore(t), nil, nil, quotaService, secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{}, - annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), + annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), usertest.NewUserServiceFake(), ) require.NoError(t, err) _, err = storesrv.ProvideService(sqlStore, featuremgmt.WithFeatures(), cfg, quotaService, storesrv.ProvideSystemUsersService()) diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 5c101cb3768..568205ef36e 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -9,7 +9,6 @@ import ( "math/rand" "net/http" "path" - "regexp" "slices" "strings" "testing" @@ -121,6 +120,7 @@ func TestIntegrationAlertRulePermissions(t *testing.T) { pathsToIgnore := []string{ "GrafanaManagedAlert.Updated", + "GrafanaManagedAlert.UpdatedBy", "GrafanaManagedAlert.UID", "GrafanaManagedAlert.ID", "GrafanaManagedAlert.Data.Model", @@ -422,6 +422,7 @@ func TestIntegrationAlertRuleNestedPermissions(t *testing.T) { pathsToIgnore := []string{ "GrafanaManagedAlert.Updated", + "GrafanaManagedAlert.UpdatedBy", "GrafanaManagedAlert.UID", "GrafanaManagedAlert.ID", "GrafanaManagedAlert.Data.Model", @@ -1144,6 +1145,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { } }], "updated": "2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds": 60, "is_paused": false, "version": 1, @@ -1183,6 +1188,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { } }], "updated": "2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds": 60, "is_paused": false, "version": 1, @@ -1234,6 +1243,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { } }], "updated": "2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds": 60, "is_paused": false, "version": 1, @@ -1498,8 +1511,9 @@ func TestIntegrationRuleCreate(t *testing.T) { client.CreateFolder(t, namespaceUID, namespaceUID) cases := []struct { - name string - config apimodels.PostableRuleGroupConfig + name string + config apimodels.PostableRuleGroupConfig + expected apimodels.GettableRuleGroupConfig }{{ name: "can create a rule with UTF-8", config: apimodels.PostableRuleGroupConfig{ @@ -1514,8 +1528,7 @@ func TestIntegrationRuleCreate(t *testing.T) { "_bar1": "baz🙂", }, Annotations: map[string]string{ - "Προμηθέας": "prom", // Prometheus in Greek - "犬": "Shiba Inu", // Dog in Japanese + "Προμηθέας": "prom", // Prometheus in Greek }, }, GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ @@ -1536,6 +1549,52 @@ func TestIntegrationRuleCreate(t *testing.T) { }, }, }, + expected: apimodels.GettableRuleGroupConfig{ + Name: "test1", + Interval: model.Duration(time.Minute), + Rules: []apimodels.GettableExtendedRuleNode{ + { + ApiRuleNode: &apimodels.ApiRuleNode{ + For: util.Pointer(model.Duration(2 * time.Minute)), + Labels: map[string]string{ + "foo🙂": "bar", + "_bar1": "baz🙂", + }, + Annotations: map[string]string{ + "Προμηθέας": "prom", // Prometheus in Greek + }, + }, + GrafanaManagedAlert: &apimodels.GettableGrafanaRule{ + OrgID: 1, + Title: "test1 rule1", + Condition: "A", + Data: []apimodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: apimodels.RelativeTimeRange{ + From: apimodels.Duration(0), + To: apimodels.Duration(15 * time.Minute), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage(`{"expression":"1","intervalMs":1000,"maxDataPoints":43200,"type":"math"}`), + }, + }, + UpdatedBy: &apimodels.UserInfo{ + Name: "admin", + }, + IntervalSeconds: 60, + Version: 1, + NamespaceUID: namespaceUID, + RuleGroup: "test1", + NoDataState: "NoData", + ExecErrState: "Alerting", + Provenance: "", + IsPaused: false, + Metadata: &apimodels.AlertRuleMetadata{}, + }, + }, + }, + }, }} for _, tc := range cases { @@ -1545,6 +1604,27 @@ func TestIntegrationRuleCreate(t *testing.T) { require.Len(t, resp.Created, 1) require.Len(t, resp.Updated, 0) require.Len(t, resp.Deleted, 0) + got, _, _ := client.GetRulesGroupWithStatus(t, namespaceUID, tc.config.Name) + + pathsToIgnore := []string{ + "GrafanaManagedAlert.Updated", + "GrafanaManagedAlert.UpdatedBy.UID", + "GrafanaManagedAlert.UID", + "GrafanaManagedAlert.ID", + "GrafanaManagedAlert.NamespaceID", + } + + // compare expected and actual and ignore the dynamic fields + diff := cmp.Diff(tc.expected, got.GettableRuleGroupConfig, cmp.FilterPath(func(path cmp.Path) bool { + for _, s := range pathsToIgnore { + if strings.HasSuffix(path.String(), s) { + return true + } + } + return false + }, cmp.Ignore())) + + require.Empty(t, diff) }) } } @@ -1696,6 +1776,18 @@ func TestIntegrationRuleUpdate(t *testing.T) { require.Equalf(t, http.StatusAccepted, status, "failed to post noop rule group. Response: %s", body) }) }) + t.Run("should set updated_by", func(t *testing.T) { + group := generateAlertRuleGroup(1, alertRuleGen()) + expected := model.Duration(10 * time.Second) + group.Rules[0].ApiRuleNode.For = &expected + + _, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group) + require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body) + getGroup := client.GetRulesGroup(t, folderUID, group.Name) + require.NotNil(t, getGroup.Rules[0].GrafanaManagedAlert.UpdatedBy) + assert.NotEmpty(t, getGroup.Rules[0].GrafanaManagedAlert.UpdatedBy.UID) + assert.Equal(t, "grafana", getGroup.Rules[0].GrafanaManagedAlert.UpdatedBy.Name) + }) } func TestIntegrationAlertAndGroupsQuery(t *testing.T) { @@ -2438,6 +2530,10 @@ func TestIntegrationQuota(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":2, @@ -2510,13 +2606,8 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) { require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) - - re := regexp.MustCompile(`"uid":"([\w|-]+)"`) - b = re.ReplaceAll(b, []byte(`"uid":""`)) - re = regexp.MustCompile(`"updated":"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)"`) - b = re.ReplaceAll(b, []byte(`"updated":"2021-05-19T19:47:55Z"`)) - - expectedGetRulesResponseBody := fmt.Sprintf(`{ + body, _ := rulesNamespaceWithoutVariableValues(t, b) + expectedGetRulesResponseBody := `{ "default": [ { "name": "arulegroup", @@ -2553,12 +2644,16 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) { } } ], - "updated": "2021-05-19T19:47:55Z", + "updated": "2021-02-21T01:10:30Z", + "updated_by" : { + "uid": "uid", + "name": "editor" + }, "intervalSeconds": 60, "is_paused": false, "version": 1, - "uid": "", - "namespace_uid": %q, + "uid": "uid", + "namespace_uid": "nsuid", "rule_group": "arulegroup", "no_data_state": "NoData", "exec_err_state": "Alerting", @@ -2573,8 +2668,8 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) { ] } ] - }`, namespaceUID) - assert.JSONEq(t, expectedGetRulesResponseBody, string(b)) + }` + assert.JSONEq(t, expectedGetRulesResponseBody, body) }) t.Run("editor can not delete the folder because it contains Grafana 8 alerts", func(t *testing.T) { u := fmt.Sprintf("http://editor:editor@%s/api/folders/%s", grafanaListedAddr, namespaceUID) @@ -3033,6 +3128,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":1, @@ -3075,6 +3174,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":1, @@ -3389,6 +3492,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":2, @@ -3504,6 +3611,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused":false, "version":3, @@ -3598,6 +3709,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused":false, "version":3, @@ -4289,6 +4404,7 @@ func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) (string, map[st rule.GrafanaManagedAlert.UID = "uid" rule.GrafanaManagedAlert.NamespaceUID = "nsuid" rule.GrafanaManagedAlert.Updated = time.Date(2021, time.Month(2), 21, 1, 10, 30, 0, time.UTC) + rule.GrafanaManagedAlert.UpdatedBy.UID = "uid" } } } From 82f457495abe6858a7166ffa69ffbf4fff4d1e40 Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Mon, 27 Jan 2025 14:59:50 -0500 Subject: [PATCH 118/894] Alerting: Correctly escape provisioning API exports (#99039) When exporting contact-points, mute-timings, and notification policies in the provisioning API, we need to escape the `$` character which is used in interpolation by file provisioning. Follow up to #97985 --- pkg/services/ngalert/api/api_provisioning.go | 61 ++++++- .../api/alerting/api_provisioning_test.go | 132 +++++++++++++++ .../provisioning-contact-points.yaml | 21 +++ .../test-data/provisioning-mixed-set.yaml | 159 ++++++++++++++++++ .../test-data/provisioning-mute-times.yaml | 20 +++ pkg/tests/api/alerting/testing.go | 52 ++++++ 6 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 pkg/tests/api/alerting/test-data/provisioning-contact-points.yaml create mode 100644 pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml create mode 100644 pkg/tests/api/alerting/test-data/provisioning-mute-times.yaml diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 935cf1bc8d3..06805da5b19 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "regexp" "strings" "github.com/grafana/grafana/pkg/api/response" @@ -19,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/util" + alertmanager_config "github.com/prometheus/alertmanager/config" ) const disableProvenanceHeaderName = "X-Disable-Provenance" @@ -600,10 +602,67 @@ func escapeAlertingFileExport(body definitions.AlertingFileExport) definitions.A for i, group := range body.Groups { body.Groups[i] = escapeRuleGroup(group) } - // TODO: implement escaping for the other export fields + for i, cp := range body.ContactPoints { + body.ContactPoints[i] = escapeContactPoint(cp) + } + for i, np := range body.Policies { + body.Policies[i] = escapeNotificationPolicy(np) + } return body } +func escapeRouteExport(r *definitions.RouteExport) { + r.Receiver = addEscapeCharactersToString(r.Receiver) + if r.GroupByStr != nil { + groupByStr := make([]string, len(*r.GroupByStr)) + for i, groupBy := range *r.GroupByStr { + groupByStr[i] = addEscapeCharactersToString(groupBy) + } + r.GroupByStr = &groupByStr + } + for k, v := range r.Match { + r.Match[k] = addEscapeCharactersToString(v) + } + for k, v := range r.MatchRE { + // convert regex to string, escape then covert back to regex + stringRepr := addEscapeCharactersToString(v.String()) + mutated := regexp.MustCompile(stringRepr) + r.MatchRE[k] = alertmanager_config.Regexp{Regexp: mutated} + } + if r.MuteTimeIntervals != nil { + muteTimeIntervals := make([]string, len(*r.MuteTimeIntervals)) + for i, muteTimeInterval := range *r.MuteTimeIntervals { + muteTimeIntervals[i] = addEscapeCharactersToString(muteTimeInterval) + } + r.MuteTimeIntervals = &muteTimeIntervals + } + for i := range r.Routes { + escapeRouteExport(r.Routes[i]) + } +} + +func escapeNotificationPolicy(np definitions.NotificationPolicyExport) definitions.NotificationPolicyExport { + escapeRouteExport(np.RouteExport) + return np +} + +func escapeContactPoint(cp definitions.ContactPointExport) definitions.ContactPointExport { + cp.Name = addEscapeCharactersToString(cp.Name) + for i, receiver := range cp.Receivers { + settingsJson, err := receiver.Settings.MarshalJSON() + if err != nil { + // This should never happen, as the settings are already marshaled to JSON in the API + panic(fmt.Errorf("failed to marshal settings to JSON: %w", err)) + } + settingsEscaped := []byte(addEscapeCharactersToString(string(settingsJson))) + if err := cp.Receivers[i].Settings.UnmarshalJSON(settingsEscaped); err != nil { + // This should never happen, as the settings are already marshaled to JSON in the API + panic(fmt.Errorf("failed to unmarshal settings from JSON: %w", err)) + } + } + return cp +} + // escape all strings except: // Alert rule annotations: groups[].rules[].annotations // Alert rule time range: groups[].rules[].relativeTimeRange diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index a2c151a53eb..ec9cda827b8 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -877,4 +877,136 @@ func TestIntegrationExportFileProvision(t *testing.T) { require.YAMLEq(t, string(expectedYaml), exportRaw) }) }) + t.Run("when provisioning mute times from files", func(t *testing.T) { + // add file provisioned mute times + fileProvisionedMuteTimings, err := testData.ReadFile(path.Join("test-data", "provisioning-mute-times.yaml")) + require.NoError(t, err) + + var expected definitions.AlertingFileExport + require.NoError(t, yaml.Unmarshal(fileProvisionedMuteTimings, &expected)) + expected.MuteTimings[0].OrgID = 1 // HACK to deal with weird goyaml behavior + expectedYamlRaw, err := yaml.Marshal(expected) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(alertingDir, "provisioning-mute-times.yaml"), fileProvisionedMuteTimings, 0750) + require.NoError(t, err) + + apiClient.ReloadAlertingFileProvisioning(t) + + t.Run("exported mute times shouldn't escape $ characters", func(t *testing.T) { + // call export endpoint + exportRaw := apiClient.ExportMuteTiming(t, "$mute_time_a", "yaml") + var export definitions.AlertingFileExport + require.NoError(t, yaml.Unmarshal([]byte(exportRaw), &export)) + expectedYaml := string(expectedYamlRaw) + // verify the file exported matches the file provisioned thing + require.Len(t, export.MuteTimings, 1) + require.YAMLEq(t, expectedYaml, exportRaw) + }) + }) +} + +func TestIntegrationExportFileProvisionMixed(t *testing.T) { + dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + }) + + provisioningDir := filepath.Join(dir, "conf", "provisioning") + alertingDir := filepath.Join(provisioningDir, "alerting") + err := os.MkdirAll(alertingDir, 0750) + require.NoError(t, err) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) + + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin") + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "admin", + Login: "admin", + IsAdmin: true, + }) + + apiClient.ReloadCachedPermissions(t) + t.Run("when provisioning mixed set of alerting configurations from files", func(t *testing.T) { + // add file provisioned mixed set of alerting configurations + fileProvisionedResources, err := testData.ReadFile(path.Join("test-data", "provisioning-mixed-set.yaml")) + require.NoError(t, err) + + var expected definitions.AlertingFileExport + require.NoError(t, yaml.Unmarshal(fileProvisionedResources, &expected)) + expected.MuteTimings[0].OrgID = 1 // HACK to deal with weird goyaml behavior + + err = os.WriteFile(filepath.Join(alertingDir, "provisioning-mixed-set.yaml"), fileProvisionedResources, 0750) + require.NoError(t, err) + + apiClient.ReloadAlertingFileProvisioning(t) + + t.Run("exported notification policy matches imported", func(t *testing.T) { + notificationPolicyExpected := expected + notificationPolicyExpected.MuteTimings = nil + notificationPolicyExpected.ContactPoints = nil + notificationPolicyExpected.Groups = nil + serializedExpected, err := yaml.Marshal(notificationPolicyExpected) + require.NoError(t, err) + + actual := apiClient.ExportNotificationPolicy(t, "yaml") + + require.YAMLEq(t, string(serializedExpected), actual) + }) + }) +} + +func TestIntegrationExportFileProvisionContactPoints(t *testing.T) { + dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + }) + + provisioningDir := filepath.Join(dir, "conf", "provisioning") + alertingDir := filepath.Join(provisioningDir, "alerting") + err := os.MkdirAll(alertingDir, 0750) + require.NoError(t, err) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) + + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin") + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "admin", + Login: "admin", + IsAdmin: true, + }) + + apiClient.ReloadCachedPermissions(t) + t.Run("when provisioning contact points from files", func(t *testing.T) { + // add file provisioned contact points + fileProvisionedContactPoints, err := testData.ReadFile(path.Join("test-data", "provisioning-contact-points.yaml")) + require.NoError(t, err) + + var expected definitions.AlertingFileExport + require.NoError(t, yaml.Unmarshal(fileProvisionedContactPoints, &expected)) + expectedYaml, err := yaml.Marshal(expected) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(alertingDir, "provisioning-contact-points.yaml"), fileProvisionedContactPoints, 0750) + require.NoError(t, err) + + apiClient.ReloadAlertingFileProvisioning(t) + + t.Run("exported contact points should escape $ characters", func(t *testing.T) { + // call export endpoint + exportRaw := apiClient.ExportReceiver(t, "cp_1_$escaped", "yaml", true) + var export definitions.AlertingFileExport + require.NoError(t, yaml.Unmarshal([]byte(exportRaw), &export)) + + // verify the file exported matches the file provisioned thing + require.Len(t, export.ContactPoints, 1) + require.YAMLEq(t, string(expectedYaml), exportRaw) + }) + }) } diff --git a/pkg/tests/api/alerting/test-data/provisioning-contact-points.yaml b/pkg/tests/api/alerting/test-data/provisioning-contact-points.yaml new file mode 100644 index 00000000000..0a759626c4c --- /dev/null +++ b/pkg/tests/api/alerting/test-data/provisioning-contact-points.yaml @@ -0,0 +1,21 @@ +# config file version +apiVersion: 1 + +# List of contact points to import or update +contactPoints: + # organization ID, default = 1 + - orgId: 1 + # name of the contact point + name: cp_1_$$escaped + + receivers: + # unique identifier for the receiver. Should not exceed 40 symbols. Only letters, numbers, - (hyphen), and _ (underscore) allowed. + - uid: first_uid + # type of the receiver + type: prometheus-alertmanager + # Disable the additional [Incident Resolved] follow-up alert, default = false + disableResolveMessage: false + # settings for the specific receiver type + settings: + url: http://test:9000 + something: $$escaped diff --git a/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml b/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml new file mode 100644 index 00000000000..b8845d29548 --- /dev/null +++ b/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml @@ -0,0 +1,159 @@ +# config file version +apiVersion: 1 + +contactPoints: + # organization ID, default = 1 + - orgId: 1 + # name of the contact point + name: $$xyz + + receivers: + # unique identifier for the receiver. Should not exceed 40 symbols. Only letters, numbers, - (hyphen), and _ (underscore) allowed. + - uid: first_uid + # type of the receiver + type: prometheus-alertmanager + # Disable the additional [Incident Resolved] follow-up alert, default = false + disableResolveMessage: false + # settings for the specific receiver type + settings: + url: http://test:9000 + something: $$escaped + +muteTimes: + # organization ID, default = 1 + - orgId: 1 + # name of the mute time interval, must be unique + name: $mute_time_1 + # time intervals that should trigger the muting + # refer to https://prometheus.io/docs/alerting/latest/configuration/#time_interval-0 + time_intervals: + - times: + - start_time: "06:00" + end_time: "23:59" + location: "UTC" + weekdays: ["monday:wednesday", "saturday", "sunday"] + months: ["1:3", "may:august", "december"] + years: ["2020:2022", "2030"] + days_of_month: ["1:5", "-3:-1"] + - orgId: 1 + # name of the mute time interval, must be unique + name: $mute_time_2 + # time intervals that should trigger the muting + # refer to https://prometheus.io/docs/alerting/latest/configuration/#time_interval-0 + time_intervals: + - times: + - start_time: "09:00" + end_time: "10:00" + location: "UTC" + weekdays: ["monday:wednesday", "saturday", "sunday"] + months: ["1:3", "may:august", "december"] + years: ["2020:2022", "2030"] + days_of_month: ["1:5", "-3:-1"] + +# ONLY THESE PATHS ARE NOT TEMPLATED and therefore don't need escaping: +# Alert rule annotations: groups[].rules[].annotations +# Alert rule time range: groups[].rules[].relativeTimeRange +# Alert rule query model: groups[].rules[].data.model +groups: + # organization ID, default = 1 + - orgId: 1 + # name of the rule group + name: my_rule_group + # name of the folder the rule group will be stored in + folder: my_first_folder_with_$$escaped_symbols + # interval that the rule group should evaluated at + interval: 60s + # list of rules that are part of the rule group + rules: + # unique identifier for the rule. Should not exceed 40 symbols. Only letters, numbers, - (hyphen), and _ (underscore) allowed. + - uid: my_id_1 + # title of the rule that will be displayed in the UI + title: my_first_rule_with_$$escaped_symbols + # which query should be used for the condition + condition: A + # list of query objects that should be executed on each + # evaluation - should be obtained through the API + data: + - refId: A + datasourceUid: "__expr__" + model: + conditions: + - evaluator: + params: + - 3 + type: gt + operator: + type: and + query: + params: + - A + reducer: + type: last + type: query + datasource: + type: __expr__ + uid: "__expr__" + expression: 1==0 + intervalMs: 1000 + maxDataPoints: 43200 + refId: A + type: math + # UID of a dashboard that the alert rule should be linked to + dashboardUid: my_dashboard + # ID of the panel that the alert rule should be linked to + panelId: 123 + # the state the alert rule will have when no data is returned + # possible values: "NoData", "Alerting", "OK", default = NoData + noDataState: Alerting + # the state the alert rule will have when the query execution + # failed - possible values: "Error", "Alerting", "OK" + # default = Alerting + execErrState: Alerting + # for how long should the alert fire before alerting + for: 60s + # > a map of strings to pass around any data + annotations: + some_key: some_value + $no_escaping_needed: $no_escaping_needed + # a map of strings that can be used to filter and + # route alerts + labels: + team: sre_team_1 + label_keys_not_$escaped: $$escaped_value + something: "escaped in the middle of things $$value" + templated: "{{ $$labels.team }}" + middle: "u$$ing_escaped_symbols" + notification_settings: + receiver: $$xyz + group_by: + - label_keys_not_$$escaped + - something + group_wait: 5m + group_interval: 10m + repeat_interval: 10m + mute_time_intervals: + - $mute_time_1 + - $mute_time_2 + +policies: + # organization ID, default = 1 + - orgId: 1 + # name of the contact point that should be used for this route + receiver: $$xyz + group_by: + - label_keys_not_$$escaped + # a list of prometheus-like matchers that an alert rule has to fulfill to match the node (allowed chars + # [a-zA-Z_:]) + matchers: + - alertname = Watchdog + - service_id_X = serviceX + - severity =~ "warning|critical" + # a list of grafana-like matchers that an alert rule has to fulfill to match the node + object_matchers: + - ["alertname", "=", "CPUUsage"] + - ["service_id-X", "=", "serviceX"] + - ["severity", "=~", "warning|critical"] + group_wait: 30s + group_interval: 5m + repeat_interval: 4h + routes: [] diff --git a/pkg/tests/api/alerting/test-data/provisioning-mute-times.yaml b/pkg/tests/api/alerting/test-data/provisioning-mute-times.yaml new file mode 100644 index 00000000000..3dc4d14872a --- /dev/null +++ b/pkg/tests/api/alerting/test-data/provisioning-mute-times.yaml @@ -0,0 +1,20 @@ +# config file version +apiVersion: 1 + +# ONLY THESE PATHS ARE NOT TEMPLATED and therefore don't need escaping: +# Mute timings name: muteTimes[].name +# Mute timings time intervals: muteTimes[].time_intervals[] +muteTimes: + - orgId: 1 + name: $mute_time_a + # time intervals that should trigger the muting + # refer to https://prometheus.io/docs/alerting/latest/configuration/#time_interval-0 + time_intervals: + - times: + - start_time: "06:00" + end_time: "23:59" + location: "UTC" + weekdays: ["monday:wednesday", "saturday", "sunday"] + months: ["1:3", "may:august", "december"] + years: ["2020:2022", "2030"] + days_of_month: ["1:5", "-3:-1"] diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index f599ec060a1..c66281d7af7 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -839,6 +839,32 @@ func (a apiClient) DeleteMuteTimingWithStatus(t *testing.T, name string) (int, s return resp.StatusCode, string(body) } +func (a apiClient) ExportMuteTiming(t *testing.T, name string, format string) string { + t.Helper() + + u, err := url.Parse(fmt.Sprintf("%s/api/v1/provisioning/mute-timings/%s/export", a.url, name)) + require.NoError(t, err) + q := url.Values{} + q.Set("format", format) + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + require.NoError(t, err) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + + defer func() { + _ = resp.Body.Close() + }() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + requireStatusCode(t, http.StatusOK, resp.StatusCode, string(body)) + return string(body) +} + func (a apiClient) GetRouteWithStatus(t *testing.T) (apimodels.Route, int, string) { t.Helper() @@ -883,6 +909,32 @@ func (a apiClient) UpdateRouteWithStatus(t *testing.T, route apimodels.Route, no return resp.StatusCode, string(body) } +func (a apiClient) ExportNotificationPolicy(t *testing.T, format string) string { + t.Helper() + + u, err := url.Parse(fmt.Sprintf("%s/api/v1/provisioning/policies/export", a.url)) + require.NoError(t, err) + q := url.Values{} + q.Set("format", format) + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + require.NoError(t, err) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + + defer func() { + _ = resp.Body.Close() + }() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + requireStatusCode(t, http.StatusOK, resp.StatusCode, string(body)) + return string(body) +} + func (a apiClient) UpdateRoute(t *testing.T, route apimodels.Route, noProvenance bool) { t.Helper() status, data := a.UpdateRouteWithStatus(t, route, noProvenance) From 078ce6a28955891cd517ceeaf9d14791a59d669a Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Mon, 27 Jan 2025 13:37:38 -0700 Subject: [PATCH 119/894] Library elements: Delete orphaned connections with the dashboard service (#99612) --- .../dashboards/service/dashboard_service.go | 4 +- pkg/services/libraryelements/database.go | 40 +++++++++++++------ .../libraryelements_delete_test.go | 10 +++++ 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 3a46f051a7d..0c90a37542c 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1074,7 +1074,9 @@ func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query * return nil, err } - if len(result) != 1 { + if len(result) == 0 { + return nil, dashboards.ErrDashboardNotFound + } else if len(result) > 1 { return nil, fmt.Errorf("unexpected number of dashboards found: %d. desired: 1", len(result)) } diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index c68ee55c2a1..057a7e3783c 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -36,16 +36,6 @@ SELECT DISTINCT , (SELECT COUNT(connection_id) FROM ` + model.LibraryElementConnectionTableName + ` WHERE element_id = le.id AND kind=1) AS connected_dashboards` ) -// redundant SELECT to trick mysql's optimizer -const deleteInvalidConnections = ` -DELETE FROM library_element_connection -WHERE connection_id IN ( - SELECT connection_id FROM ( - SELECT connection_id as id FROM library_element_connection - WHERE element_id=? AND connection_id NOT IN (SELECT id as connection_id from dashboard) - ) as dummy -)` - func getFromLibraryElementDTOWithMeta(dialect migrator.Dialect) string { user := dialect.Quote("user") userJoin := ` @@ -243,11 +233,37 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn } } - // Delete any hanging/invalid connections - if _, err = session.Exec(deleteInvalidConnections, element.ID); err != nil { + dashboardIDs := []int64{} + // get all connections for this element + if err := session.SQL("SELECT connection_id FROM library_element_connection where element_id = ?", element.ID).Find(&dashboardIDs); err != nil { return err } + // then find the dashboards that were supposed to be connected to this element + _, requester := identity.WithServiceIdentitiy(c, signedInUser.GetOrgID()) + dashs, err := l.dashboardsService.FindDashboards(c, &dashboards.FindPersistedDashboardsQuery{ + OrgId: signedInUser.GetOrgID(), + DashboardIds: dashboardIDs, + SignedInUser: requester, // a user may be able to delete a library element but not read all dashboards. We still need to run this check, so we don't allow deleting elements if dashboards are connected + }) + if err != nil { + return err + } + + foundDashes := make([]int64, len(dashs)) + for i, d := range dashs { + foundDashes[i] = d.ID + } + + // delete any connections that are orphaned for this element (i.e. the dashboard was deleted) + session.Table("library_element_connection") + session.Where("element_id = ?", element.ID) + session.NotIn("connection_id", foundDashes) + if _, err = session.Delete(model.LibraryElementConnectionWithMeta{}); err != nil { + return err + } + + // now try to delete the element, but fail if it is connected to any dashboards var connectionIDs []struct { ConnectionID int64 `xorm:"connection_id"` } diff --git a/pkg/services/libraryelements/libraryelements_delete_test.go b/pkg/services/libraryelements/libraryelements_delete_test.go index 597787d6ee0..4246b0ac66d 100644 --- a/pkg/services/libraryelements/libraryelements_delete_test.go +++ b/pkg/services/libraryelements/libraryelements_delete_test.go @@ -82,4 +82,14 @@ func TestDeleteLibraryElement(t *testing.T) { resp := sc.service.deleteHandler(sc.reqContext) require.Equal(t, 403, resp.Status()) }) + + scenarioWithPanel(t, "When an admin tries to delete a library panel that is connected to a non-existent dashboard, it should succeed", + func(t *testing.T, sc scenarioContext) { + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 9999999) + require.NoError(t, err) + + sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) + resp := sc.service.deleteHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + }) } From fd85ddf647e9c6d0e9b4f70e0eee9e7a54e56b05 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Mon, 27 Jan 2025 14:39:36 -0600 Subject: [PATCH 120/894] Unified Storage: Fix search case sensitivity (#99603) lowercase search query when doing a text query. Doing this makes the NewWildcardQuery be case-insensitive --- pkg/storage/unified/search/bleve.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 18859d984ad..79a5149efc7 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -575,7 +575,7 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resource.Res if req.Query != "" && req.Query != "*" { searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE) // mimic the behavior of the sql search - query := req.Query + query := strings.ToLower(req.Query) if !strings.Contains(query, "*") { query = "*" + query + "*" } From 61c5b4a25e943b4ebef41532b8e5b24d205770fc Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Mon, 27 Jan 2025 16:39:18 -0700 Subject: [PATCH 121/894] Library elements: remove usage of dashboard table on get (#99619) --- pkg/services/libraryelements/database.go | 32 +++++++++----- .../libraryelements_get_test.go | 34 ++++++++++++++ .../libraryelements/libraryelements_test.go | 44 ++++++++++++++++++- 3 files changed, 98 insertions(+), 12 deletions(-) diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 057a7e3783c..8b23f58cf5e 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -79,15 +79,12 @@ func syncFieldsWithModel(libraryElement *model.LibraryElement) error { return nil } -func GetLibraryElement(dialect migrator.Dialect, session *db.Session, uid string, orgID int64) (model.LibraryElementWithMeta, error) { +func (l *LibraryElementService) GetLibraryElement(c context.Context, signedInUser identity.Requester, session *db.Session, uid string) (model.LibraryElementWithMeta, error) { elements := make([]model.LibraryElementWithMeta, 0) sql := selectLibraryElementDTOWithMeta + - ", coalesce(dashboard.title, 'General') AS folder_name" + - ", coalesce(dashboard.uid, '') AS folder_uid" + - getFromLibraryElementDTOWithMeta(dialect) + - " LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" + + getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect()) + " WHERE le.uid=? AND le.org_id=?" - sess := session.SQL(sql, uid, orgID) + sess := session.SQL(sql, uid, signedInUser.GetOrgID()) err := sess.Find(&elements) if err != nil { return model.LibraryElementWithMeta{}, err @@ -99,6 +96,19 @@ func GetLibraryElement(dialect migrator.Dialect, session *db.Session, uid string return model.LibraryElementWithMeta{}, fmt.Errorf("found %d elements, while expecting at most one", len(elements)) } + // get the folder title + f, err := l.folderService.Get(c, &folder.GetFolderQuery{ + OrgID: elements[0].OrgID, + UID: &elements[0].FolderUID, + SignedInUser: signedInUser, + }) + if err == nil { + elements[0].FolderName = f.Title + } else { + // default to General if we cannot find the folder + elements[0].FolderName = "General" + } + return elements[0], nil } @@ -220,7 +230,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) { var elementID int64 err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { - element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID()) + element, err := l.GetLibraryElement(c, signedInUser, session, uid) if err != nil { return err } @@ -593,7 +603,7 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU return model.LibraryElementDTO{}, err } err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { - elementInDB, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID()) + elementInDB, err := l.GetLibraryElement(c, signedInUser, session, uid) if err != nil { return err } @@ -610,7 +620,7 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU return model.ErrLibraryElementUIDTooLong } - _, err := GetLibraryElement(l.SQLStore.GetDialect(), session, updateUID, signedInUser.GetOrgID()) + _, err := l.GetLibraryElement(c, signedInUser, session, updateUID) if !errors.Is(err, model.ErrLibraryElementNotFound) { return model.ErrLibraryElementAlreadyExists } @@ -705,7 +715,7 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser i } err = l.SQLStore.WithDbSession(c, func(session *db.Session) error { - element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID()) + element, err := l.GetLibraryElement(c, signedInUser, session, uid) if err != nil { return err } @@ -832,7 +842,7 @@ func (l *LibraryElementService) connectElementsToDashboardID(c context.Context, return err } for _, elementUID := range elementUIDs { - element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, elementUID, signedInUser.GetOrgID()) + element, err := l.GetLibraryElement(c, signedInUser, session, elementUID) if err != nil { return err } diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index b441505639b..96f64f4c35f 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -1,12 +1,15 @@ package libraryelements import ( + "encoding/json" "testing" "github.com/google/go-cmp/cmp" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/web" @@ -91,6 +94,37 @@ func TestGetLibraryElement(t *testing.T) { } }) + scenarioWithPanel(t, "When an admin tries to get a library panel that exists, but the original folder does not, it should succeed and return correct result", + func(t *testing.T, sc scenarioContext) { + b, err := json.Marshal(map[string]string{"test": "test"}) + require.NoError(t, err) + newFolder := createFolder(t, sc, "NewFolder", nil) + sc.reqContext.SignedInUser.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll} + sc.reqContext.SignedInUser.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll} + result, err := sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{ + FolderID: newFolder.ID, // nolint:staticcheck + FolderUID: &newFolder.UID, + Name: "Testing Library Panel With Deleted Folder", + Kind: 1, + Model: b, + UID: "panel-with-deleted-folder", + }) + require.NoError(t, err) + err = sc.service.folderService.Delete(sc.reqContext.Req.Context(), &folder.DeleteFolderCommand{ + UID: newFolder.UID, + OrgID: sc.reqContext.OrgID, + SignedInUser: sc.reqContext.SignedInUser, + }) + require.NoError(t, err) + err = sc.sqlStore.WithDbSession(sc.reqContext.Req.Context(), func(session *db.Session) error { + elem, err := sc.service.GetLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, session, result.UID) + require.NoError(t, err) + require.Equal(t, elem.FolderName, "General") + return nil + }) + require.NoError(t, err) + }) + scenarioWithPanel(t, "When an admin tries to get a connected library panel, it should succeed and return correct connected dashboards", func(t *testing.T, sc scenarioContext) { dashJSON := map[string]any{ diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index fd7bb559a12..7ea77dd22ff 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -32,10 +32,13 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements/model" + ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" @@ -198,6 +201,39 @@ func TestGetLibraryPanelConnections(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } }) + + scenarioWithPanel(t, "When an admin tries to create a connection with an element that exists, but the original folder does not, it should still succeed", + func(t *testing.T, sc scenarioContext) { + b, err := json.Marshal(map[string]string{"test": "test"}) + require.NoError(t, err) + newFolder := createFolder(t, sc, "NewFolder", nil) + sc.reqContext.SignedInUser.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll} + sc.reqContext.SignedInUser.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll} + _, err = sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{ + FolderID: newFolder.ID, // nolint:staticcheck + FolderUID: &newFolder.UID, + Name: "Testing Library Panel With Deleted Folder", + Kind: 1, + Model: b, + UID: "panel-with-deleted-folder", + }) + require.NoError(t, err) + err = sc.service.folderService.Delete(sc.reqContext.Req.Context(), &folder.DeleteFolderCommand{ + UID: newFolder.UID, + OrgID: sc.reqContext.OrgID, + SignedInUser: sc.reqContext.SignedInUser, + }) + require.NoError(t, err) + + dash := dashboards.Dashboard{ + Title: "Testing create element", + Data: simplejson.NewFromAny(map[string]any{}), + } + // nolint:staticcheck + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID) + err = sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) + require.NoError(t, err) + }) } type libraryElement struct { @@ -477,9 +513,15 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo dashboardPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) fStore := folderimpl.ProvideStore(sqlStore) + publicDash := &publicdashboards.FakePublicDashboardServiceWrapper{} + publicDash.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) folderSvc := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), publicDash, cfg, nil, tracing.InitializeTracerForTest()) + alertStore, err := ngstore.ProvideDBStore(cfg, features, sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) + require.NoError(t, err) + err = folderSvc.RegisterService(alertStore) + require.NoError(t, err) dashService, dashSvcErr := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, ac, From 4e703576b075a4fab5c90a6d6b3e4d0aef705cfd Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 28 Jan 2025 10:30:53 +0300 Subject: [PATCH 122/894] K8s/OpenAPI: Render openapi into a static file (#99561) --- .github/CODEOWNERS | 7 +- openapi/README.md | 3 + openapi/dashboard.grafana.app-v0alpha1.json | 2767 +++++++++++++++++++ openapi/folder.grafana.app-v0alpha1.json | 1764 ++++++++++++ openapi/peakq.grafana.app-v0alpha1.json | 2523 +++++++++++++++++ pkg/tests/apis/core/openapi_test.go | 120 + pkg/tests/apis/helper.go | 27 - pkg/tests/apis/playlist/playlist_test.go | 3 - 8 files changed, 7181 insertions(+), 33 deletions(-) create mode 100644 openapi/README.md create mode 100644 openapi/dashboard.grafana.app-v0alpha1.json create mode 100644 openapi/folder.grafana.app-v0alpha1.json create mode 100644 openapi/peakq.grafana.app-v0alpha1.json create mode 100644 pkg/tests/apis/core/openapi_test.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bb3c9871f91..e43d98d7724 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -593,9 +593,6 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/AppWrapper.tsx @grafana/frontend-ops /public/app/partials/ @grafana/grafana-frontend-platform - - - /scripts/benchmark-access-control.sh @grafana/access-squad /scripts/check-breaking-changes.sh @grafana/plugins-platform-frontend /scripts/ci-* @grafana/grafana-developer-enablement-squad @@ -734,6 +731,10 @@ embed.go @grafana/grafana-as-code /public/app/plugins/*gen.go @grafana/grafana-as-code /cue.mod/ @grafana/grafana-as-code +# Rendered OpenAPI from app platform +# Eventually each file owned by the right team, OR a structure with the rendered value under /apis/{group}/openapi +/openapi/ @grafana/grafana-app-platform-squad + # GitHub Workflows and Templates /.github/CODEOWNERS @tolzhabayev /.github/ISSUE_TEMPLATE/ @torkelo @sympatheticmoose diff --git a/openapi/README.md b/openapi/README.md new file mode 100644 index 00000000000..93fdfdbeba9 --- /dev/null +++ b/openapi/README.md @@ -0,0 +1,3 @@ +This folder contains a rendered OpenAPI for each group/version + +The "real" openapi is generated by the running server, but this is used to build static frontend clients diff --git a/openapi/dashboard.grafana.app-v0alpha1.json b/openapi/dashboard.grafana.app-v0alpha1.json new file mode 100644 index 00000000000..2cf10939f94 --- /dev/null +++ b/openapi/dashboard.grafana.app-v0alpha1.json @@ -0,0 +1,2767 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Grafana dashboards as resources", + "title": "dashboard.grafana.app/v0alpha1" + }, + "paths": { + "/apis/dashboard.grafana.app/v0alpha1/": { + "get": { + "tags": ["API Discovery"], + "description": "get available resources", + "operationId": "getAPIResources", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + } + } + } + } + } + }, + "/apis/dashboard.grafana.app/v0alpha1/librarypanels": { + "get": { + "tags": ["LibraryPanel"], + "description": "list objects of kind LibraryPanel", + "operationId": "listLibraryPanelForAllNamespaces", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/dashboards": { + "get": { + "tags": ["Dashboard"], + "description": "list or watch objects of kind Dashboard", + "operationId": "listDashboard", + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "post": { + "tags": ["Dashboard"], + "description": "create a Dashboard", + "operationId": "createDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "delete": { + "tags": ["Dashboard"], + "description": "delete collection of Dashboard", + "operationId": "deletecollectionDashboard", + "parameters": [ + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/dashboards/{name}": { + "get": { + "tags": ["Dashboard"], + "description": "read the specified Dashboard", + "operationId": "getDashboard", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "put": { + "tags": ["Dashboard"], + "description": "replace the specified Dashboard", + "operationId": "replaceDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "delete": { + "tags": ["Dashboard"], + "description": "delete a Dashboard", + "operationId": "deleteDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "patch": { + "tags": ["Dashboard"], + "description": "partially update the specified Dashboard", + "operationId": "updateDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Dashboard", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/dashboards/{name}/dto": { + "get": { + "tags": ["Dashboard"], + "description": "connect GET requests to dto of Dashboard", + "operationId": "getDashboardDto", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardWithAccessInfo" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "DashboardWithAccessInfo" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the DashboardWithAccessInfo", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "path", + "in": "query", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "version", + "in": "query", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/librarypanels": { + "get": { + "tags": ["LibraryPanel"], + "description": "list objects of kind LibraryPanel", + "operationId": "listLibraryPanel", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/librarypanels/{name}": { + "get": { + "tags": ["LibraryPanel"], + "description": "read the specified LibraryPanel", + "operationId": "getLibraryPanel", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the LibraryPanel", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search": { + "get": { + "tags": ["Search"], + "description": "Dashboard search", + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "workspace", + "required": true, + "schema": { + "type": "string" + }, + "example": "default" + }, + { + "name": "query", + "in": "query", + "description": "user query string", + "schema": { + "type": "string" + } + }, + { + "name": "folder", + "in": "query", + "description": "search/list within a folder (not recursive)", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "in": "query", + "description": "sortable field", + "schema": { + "type": "string" + }, + "examples": { + "": { + "summary": "default sorting" + }, + "-title": { + "summary": "title descending", + "value": "-title" + }, + "title": { + "summary": "title ascending", + "value": "title" + } + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["totalHits", "hits"], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "facets": { + "description": "Facet results", + "type": "object", + "additionalProperties": { + "default": {} + } + }, + "hits": { + "description": "The dashboard body (unstructured for now)", + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "maxScore": { + "description": "Max score", + "type": "number", + "format": "double" + }, + "offset": { + "description": "Where the query started from", + "type": "integer", + "format": "int64" + }, + "queryCost": { + "description": "Cost of running the query", + "type": "number", + "format": "double" + }, + "sortBy": { + "description": "How are the results sorted" + }, + "totalHits": { + "description": "The number of matching results", + "type": "integer", + "format": "int64", + "default": 0 + } + } + } + } + } + } + } + } + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search/sortable": { + "get": { + "tags": ["Search"], + "description": "Get sortable fields", + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "workspace", + "required": true, + "schema": { + "type": "string" + }, + "example": "default" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["fields"], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "fields": { + "description": "Sortable fields (depends on backend support)", + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + } + } + } + } + } + } + } + } + }, + "/apis/dashboard.grafana.app/v0alpha1/search": null, + "/apis/dashboard.grafana.app/v0alpha1/watch/namespaces/{namespace}/dashboards": { + "get": { + "tags": ["Dashboard"], + "description": "watch individual changes to a list of Dashboard. deprecated: use the 'watch' parameter with a list operation instead.", + "operationId": "watchDashboardList", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + } + } + }, + "x-kubernetes-action": "watchlist", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v0alpha1/watch/namespaces/{namespace}/dashboards/{name}": { + "get": { + "tags": ["Dashboard"], + "description": "watch changes to an object of kind Dashboard. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", + "operationId": "watchDashboard", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + } + } + }, + "x-kubernetes-action": "watch", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "name", + "in": "path", + "description": "name of the Dashboard", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + } + }, + "components": { + "schemas": { + "com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataQuery": { + "description": "Generic query properties", + "type": "object", + "properties": { + "datasource": { + "description": "The datasource", + "type": "object", + "required": ["type"], + "properties": { + "apiVersion": { + "description": "The apiserver version", + "type": "string" + }, + "type": { + "description": "The datasource plugin type", + "type": "string" + }, + "uid": { + "description": "Datasource UID (NOTE: name in k8s)", + "type": "string" + } + }, + "additionalProperties": false + }, + "hide": { + "description": "true if query is disabled (ie should not be returned to the dashboard)\nNOTE: this does not always imply that the query should not be executed since\nthe results from a hidden query may be used as the input to other queries (SSE etc)", + "type": "boolean" + }, + "intervalMs": { + "description": "Interval is the suggested duration between time points in a time series query.\nNOTE: the values for intervalMs is not saved in the query model. It is typically calculated\nfrom the interval required to fill a pixels in the visualization", + "type": "number" + }, + "maxDataPoints": { + "description": "MaxDataPoints is the maximum number of data points that should be returned from a time series query.\nNOTE: the values for maxDataPoints is not saved in the query model. It is typically calculated\nfrom the number of pixels visible in a visualization", + "type": "integer" + }, + "queryType": { + "description": "QueryType is an optional identifier for the type of query.\nIt can be used to distinguish different types of queries.", + "type": "string" + }, + "refId": { + "description": "RefID is the unique identifier of the query, set by the frontend call.", + "type": "string" + }, + "resultAssertions": { + "description": "Optionally define expected query result behavior", + "type": "object", + "required": ["typeVersion"], + "properties": { + "maxFrames": { + "description": "Maximum frame count", + "type": "integer" + }, + "type": { + "description": "Type asserts that the frame matches a known type structure.\n\n\nPossible enum values:\n - `\"\"` \n - `\"timeseries-wide\"` \n - `\"timeseries-long\"` \n - `\"timeseries-many\"` \n - `\"timeseries-multi\"` \n - `\"directory-listing\"` \n - `\"table\"` \n - `\"numeric-wide\"` \n - `\"numeric-multi\"` \n - `\"numeric-long\"` \n - `\"log-lines\"` ", + "type": "string", + "enum": [ + "", + "timeseries-wide", + "timeseries-long", + "timeseries-many", + "timeseries-multi", + "directory-listing", + "table", + "numeric-wide", + "numeric-multi", + "numeric-long", + "log-lines" + ], + "x-enum-description": {} + }, + "typeVersion": { + "description": "TypeVersion is the version of the Type property. Versions greater than 0.0 correspond to the dataplane\ncontract documentation https://grafana.github.io/dataplane/contract/.", + "type": "array", + "maxItems": 2, + "minItems": 2, + "items": { + "type": "integer" + } + } + }, + "additionalProperties": false + }, + "timeRange": { + "description": "TimeRange represents the query range\nNOTE: unlike generic /ds/query, we can now send explicit time values in each query\nNOTE: the values for timeRange are not saved in a dashboard, they are constructed on the fly", + "type": "object", + "required": ["from", "to"], + "properties": { + "from": { + "description": "From is the start time of the query.", + "type": "string", + "default": "now-6h", + "examples": ["now-1h"] + }, + "to": { + "description": "To is the end time of the query.", + "type": "string", + "default": "now", + "examples": ["now"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true, + "$schema": "https://json-schema.org/draft-04/schema" + }, + "com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataSourceRef": { + "type": "object", + "additionalProperties": true + }, + "com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured": { + "type": "object", + "additionalProperties": true, + "x-kubernetes-preserve-unknown-fields": true + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationActions": { + "type": "object", + "required": ["canAdd", "canEdit", "canDelete"], + "properties": { + "canAdd": { + "type": "boolean", + "default": false + }, + "canDelete": { + "type": "boolean", + "default": false + }, + "canEdit": { + "type": "boolean", + "default": false + } + } + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationPermission": { + "type": "object", + "required": ["dashboard", "organization"], + "properties": { + "dashboard": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationActions" + } + ] + }, + "organization": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationActions" + } + ] + } + } + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard": { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "description": "Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "The dashboard body (unstructured for now)", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "Dashboard", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardAccess": { + "description": "Information about how the requesting user can use a given dashboard", + "type": "object", + "required": ["canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"], + "properties": { + "annotationsPermissions": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationPermission" + }, + "canAdmin": { + "type": "boolean", + "default": false + }, + "canDelete": { + "type": "boolean", + "default": false + }, + "canEdit": { + "type": "boolean", + "default": false + }, + "canSave": { + "description": "The permissions part", + "type": "boolean", + "default": false + }, + "canStar": { + "type": "boolean", + "default": false + }, + "slug": { + "description": "Metadata fields", + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardList": { + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "DashboardList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardWithAccessInfo": { + "description": "This is like the legacy DTO where access and metadata are all returned in a single call", + "type": "object", + "required": ["spec", "access"], + "properties": { + "access": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardAccess" + } + ] + }, + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "description": "Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "The dashboard body (unstructured for now)", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "DashboardWithAccessInfo", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanel": { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "description": "Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Panel properties", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelSpec" + } + ] + }, + "status": { + "description": "Status will show errors", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "LibraryPanel", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList": { + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "LibraryPanelList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelSpec": { + "type": "object", + "required": ["type", "options", "fieldConfig"], + "properties": { + "datasource": { + "description": "The default datasource type", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataSourceRef" + } + ] + }, + "description": { + "description": "Library panel description", + "type": "string" + }, + "fieldConfig": { + "description": "The fieldConfig schema depends on the panel type", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + }, + "options": { + "description": "The options schema depends on the panel type", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + }, + "pluginVersion": { + "description": "The panel type", + "type": "string" + }, + "targets": { + "description": "The datasource queries", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataQuery" + }, + "x-kubernetes-list-type": "set" + }, + "title": { + "description": "The panel title", + "type": "string" + }, + "type": { + "description": "The panel type", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelStatus": { + "type": "object", + "properties": { + "missing": { + "description": "The properties previously stored in SQL that are not included in this model", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + }, + "warnings": { + "description": "Translation warnings (mostly things that were in SQL columns but not found in the saved body)", + "type": "array", + "items": { + "type": "string", + "default": "" + } + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { + "description": "APIResource specifies the name of a resource and whether it is namespaced.", + "type": "object", + "required": ["name", "singularName", "namespaced", "kind", "verbs"], + "properties": { + "categories": { + "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", + "type": "string" + }, + "kind": { + "description": "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", + "type": "string", + "default": "" + }, + "name": { + "description": "name is the plural name of the resource.", + "type": "string", + "default": "" + }, + "namespaced": { + "description": "namespaced indicates if a resource is namespaced or not.", + "type": "boolean", + "default": false + }, + "shortNames": { + "description": "shortNames is a list of suggested short names of the resource.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "singularName": { + "description": "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", + "type": "string", + "default": "" + }, + "storageVersionHash": { + "description": "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", + "type": "string" + }, + "verbs": { + "description": "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", + "type": "array", + "items": { + "type": "string", + "default": "" + } + }, + "version": { + "description": "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { + "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", + "type": "object", + "required": ["groupVersion", "resources"], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "groupVersion": { + "description": "groupVersion is the group and version this APIResourceList is for.", + "type": "string", + "default": "" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "resources": { + "description": "resources contains the name of the resources and if they are namespaced.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResource" + } + ] + }, + "x-kubernetes-list-type": "atomic" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions": { + "description": "DeleteOptions may be provided when deleting an API object.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "dryRun": { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "gracePeriodSeconds": { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "type": "integer", + "format": "int64" + }, + "ignoreStoreReadErrorWithClusterBreakingPotential": { + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "type": "boolean" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "orphanDependents": { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "type": "boolean" + }, + "preconditions": { + "description": "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions" + } + ] + }, + "propagationPolicy": { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { + "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", + "type": "object", + "properties": { + "continue": { + "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", + "type": "string" + }, + "remainingItemCount": { + "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", + "type": "integer", + "format": "int64" + }, + "resourceVersion": { + "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": "string" + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": "string" + }, + "fieldsV1": { + "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" + } + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": "string" + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": "string" + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": "string" + }, + "time": { + "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "type": "object", + "properties": { + "annotations": { + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "creationTimestamp": { + "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "type": "integer", + "format": "int64" + }, + "deletionTimestamp": { + "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set", + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": "string" + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "type": "integer", + "format": "int64" + }, + "labels": { + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": "string" + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" + } + ] + }, + "x-kubernetes-list-map-keys": ["uid"], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "type": "object", + "required": ["apiVersion", "kind", "name", "uid"], + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string", + "default": "" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": "boolean" + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": "boolean" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string", + "default": "" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string", + "default": "" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string", + "default": "" + } + }, + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Patch": { + "description": "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions": { + "description": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", + "type": "object", + "properties": { + "resourceVersion": { + "description": "Specifies the target ResourceVersion", + "type": "string" + }, + "uid": { + "description": "Specifies the target UID.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Status": { + "description": "Status is a return value for calls that don't return other objects.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "code": { + "description": "Suggested HTTP return code for this status, 0 if not set.", + "type": "integer", + "format": "int32" + }, + "details": { + "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails" + } + ], + "x-kubernetes-list-type": "atomic" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "message": { + "description": "A human-readable description of the status of this operation.", + "type": "string" + }, + "metadata": { + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + }, + "reason": { + "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", + "type": "string" + }, + "status": { + "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause": { + "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", + "type": "object", + "properties": { + "field": { + "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", + "type": "string" + }, + "message": { + "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", + "type": "string" + }, + "reason": { + "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails": { + "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", + "type": "object", + "properties": { + "causes": { + "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "The group attribute of the resource associated with the status StatusReason.", + "type": "string" + }, + "kind": { + "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", + "type": "string" + }, + "retryAfterSeconds": { + "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", + "type": "integer", + "format": "int32" + }, + "uid": { + "description": "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "type": "string", + "format": "date-time" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": { + "description": "Event represents a single event to a watched resource.", + "type": "object", + "required": ["type", "object"], + "properties": { + "object": { + "description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension" + } + ] + }, + "type": { + "type": "string", + "default": "" + } + } + }, + "io.k8s.apimachinery.pkg.runtime.RawExtension": { + "description": "RawExtension is used to hold extensions in external versions.\n\nTo use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types.\n\n// Internal package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.Object `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// External package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// On the wire, the JSON will look something like this:\n\n\t{\n\t\t\"kind\":\"MyAPIObject\",\n\t\t\"apiVersion\":\"v1\",\n\t\t\"myPlugin\": {\n\t\t\t\"kind\":\"PluginA\",\n\t\t\t\"aOption\":\"foo\",\n\t\t},\n\t}\n\nSo what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.)", + "type": "object" + } + } + } +} diff --git a/openapi/folder.grafana.app-v0alpha1.json b/openapi/folder.grafana.app-v0alpha1.json new file mode 100644 index 00000000000..426d681c6bd --- /dev/null +++ b/openapi/folder.grafana.app-v0alpha1.json @@ -0,0 +1,1764 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Grafana folders", + "title": "folder.grafana.app/v0alpha1" + }, + "paths": { + "/apis/folder.grafana.app/v0alpha1/": { + "get": { + "tags": ["API Discovery"], + "description": "get available resources", + "operationId": "getAPIResources", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + } + } + } + } + } + }, + "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders": { + "get": { + "tags": ["Folder"], + "description": "list objects of kind Folder", + "operationId": "listFolder", + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "post": { + "tags": ["Folder"], + "description": "create a Folder", + "operationId": "createFolder", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "delete": { + "tags": ["Folder"], + "description": "delete collection of Folder", + "operationId": "deletecollectionFolder", + "parameters": [ + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}": { + "get": { + "tags": ["Folder"], + "description": "read the specified Folder", + "operationId": "getFolder", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "put": { + "tags": ["Folder"], + "description": "replace the specified Folder", + "operationId": "replaceFolder", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "delete": { + "tags": ["Folder"], + "description": "delete a Folder", + "operationId": "deleteFolder", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "patch": { + "tags": ["Folder"], + "description": "partially update the specified Folder", + "operationId": "updateFolder", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Folder", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}/access": { + "get": { + "tags": ["Folder"], + "description": "connect GET requests to access of Folder", + "operationId": "getFolderAccess", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderAccessInfo" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "FolderAccessInfo" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the FolderAccessInfo", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}/counts": { + "get": { + "tags": ["Folder"], + "description": "connect GET requests to counts of Folder", + "operationId": "getFolderCounts", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.DescendantCounts" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "DescendantCounts" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the DescendantCounts", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}/parents": { + "get": { + "tags": ["Folder"], + "description": "connect GET requests to parents of Folder", + "operationId": "getFolderParents", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderInfoList" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "FolderInfoList" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the FolderInfoList", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + } + }, + "components": { + "schemas": { + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.DescendantCounts": { + "type": "object", + "required": ["counts"], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "counts": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.ResourceStats" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "DescendantCounts", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "DescendantCounts", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder": { + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Spec" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "Folder", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "Folder", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderAccessInfo": { + "description": "Access control information for the current user", + "type": "object", + "required": ["canSave", "canEdit", "canAdmin", "canDelete"], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "canAdmin": { + "type": "boolean", + "default": false + }, + "canDelete": { + "type": "boolean", + "default": false + }, + "canEdit": { + "type": "boolean", + "default": false + }, + "canSave": { + "type": "boolean", + "default": false + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "FolderAccessInfo", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "FolderAccessInfo", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderInfo": { + "description": "FolderInfo briefly describes a folder -- unlike a folder resource, this is a partial record of the folder metadata used for navigating parents and children", + "type": "object", + "required": ["name", "title"], + "properties": { + "description": { + "description": "The folder description", + "type": "string" + }, + "detached": { + "description": "This folder does not resolve", + "type": "boolean" + }, + "name": { + "description": "Name is the k8s name (eg, the unique identifier) for a folder", + "type": "string", + "default": "" + }, + "parent": { + "description": "The parent folder UID", + "type": "string" + }, + "title": { + "description": "Title is the display value", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderInfoList": { + "description": "FolderInfoList returns a list of folder references (parents or children) Unlike FolderList, each item is not a full k8s object", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderInfo" + } + ] + }, + "x-kubernetes-list-map-keys": ["uid"], + "x-kubernetes-list-type": "map" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "FolderInfoList", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "FolderInfoList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderList": { + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "FolderList", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "FolderList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.ResourceStats": { + "type": "object", + "required": ["group", "resource", "count"], + "properties": { + "count": { + "type": "integer", + "format": "int64", + "default": 0 + }, + "group": { + "type": "string", + "default": "" + }, + "resource": { + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Spec": { + "type": "object", + "required": ["title"], + "properties": { + "description": { + "description": "Describe the feature toggle", + "type": "string" + }, + "title": { + "description": "Describe the feature toggle", + "type": "string", + "default": "" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { + "description": "APIResource specifies the name of a resource and whether it is namespaced.", + "type": "object", + "required": ["name", "singularName", "namespaced", "kind", "verbs"], + "properties": { + "categories": { + "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", + "type": "string" + }, + "kind": { + "description": "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", + "type": "string", + "default": "" + }, + "name": { + "description": "name is the plural name of the resource.", + "type": "string", + "default": "" + }, + "namespaced": { + "description": "namespaced indicates if a resource is namespaced or not.", + "type": "boolean", + "default": false + }, + "shortNames": { + "description": "shortNames is a list of suggested short names of the resource.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "singularName": { + "description": "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", + "type": "string", + "default": "" + }, + "storageVersionHash": { + "description": "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", + "type": "string" + }, + "verbs": { + "description": "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", + "type": "array", + "items": { + "type": "string", + "default": "" + } + }, + "version": { + "description": "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { + "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", + "type": "object", + "required": ["groupVersion", "resources"], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "groupVersion": { + "description": "groupVersion is the group and version this APIResourceList is for.", + "type": "string", + "default": "" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "resources": { + "description": "resources contains the name of the resources and if they are namespaced.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResource" + } + ] + }, + "x-kubernetes-list-type": "atomic" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions": { + "description": "DeleteOptions may be provided when deleting an API object.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "dryRun": { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "gracePeriodSeconds": { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "type": "integer", + "format": "int64" + }, + "ignoreStoreReadErrorWithClusterBreakingPotential": { + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "type": "boolean" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "orphanDependents": { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "type": "boolean" + }, + "preconditions": { + "description": "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions" + } + ] + }, + "propagationPolicy": { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { + "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", + "type": "object", + "properties": { + "continue": { + "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", + "type": "string" + }, + "remainingItemCount": { + "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", + "type": "integer", + "format": "int64" + }, + "resourceVersion": { + "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": "string" + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": "string" + }, + "fieldsV1": { + "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" + } + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": "string" + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": "string" + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": "string" + }, + "time": { + "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "type": "object", + "properties": { + "annotations": { + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "creationTimestamp": { + "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "type": "integer", + "format": "int64" + }, + "deletionTimestamp": { + "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set", + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": "string" + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "type": "integer", + "format": "int64" + }, + "labels": { + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": "string" + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" + } + ] + }, + "x-kubernetes-list-map-keys": ["uid"], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "type": "object", + "required": ["apiVersion", "kind", "name", "uid"], + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string", + "default": "" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": "boolean" + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": "boolean" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string", + "default": "" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string", + "default": "" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string", + "default": "" + } + }, + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Patch": { + "description": "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions": { + "description": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", + "type": "object", + "properties": { + "resourceVersion": { + "description": "Specifies the target ResourceVersion", + "type": "string" + }, + "uid": { + "description": "Specifies the target UID.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Status": { + "description": "Status is a return value for calls that don't return other objects.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "code": { + "description": "Suggested HTTP return code for this status, 0 if not set.", + "type": "integer", + "format": "int32" + }, + "details": { + "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails" + } + ], + "x-kubernetes-list-type": "atomic" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "message": { + "description": "A human-readable description of the status of this operation.", + "type": "string" + }, + "metadata": { + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + }, + "reason": { + "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", + "type": "string" + }, + "status": { + "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause": { + "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", + "type": "object", + "properties": { + "field": { + "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", + "type": "string" + }, + "message": { + "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", + "type": "string" + }, + "reason": { + "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails": { + "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", + "type": "object", + "properties": { + "causes": { + "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "The group attribute of the resource associated with the status StatusReason.", + "type": "string" + }, + "kind": { + "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", + "type": "string" + }, + "retryAfterSeconds": { + "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", + "type": "integer", + "format": "int32" + }, + "uid": { + "description": "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "type": "string", + "format": "date-time" + } + } + } +} diff --git a/openapi/peakq.grafana.app-v0alpha1.json b/openapi/peakq.grafana.app-v0alpha1.json new file mode 100644 index 00000000000..b931b96dde8 --- /dev/null +++ b/openapi/peakq.grafana.app-v0alpha1.json @@ -0,0 +1,2523 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "peakq.grafana.app/v0alpha1" + }, + "paths": { + "/apis/peakq.grafana.app/v0alpha1/": { + "get": { + "description": "get available resources", + "operationId": "getAPIResources", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + } + } + } + } + } + }, + "/apis/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates": { + "get": { + "tags": ["QueryTemplate"], + "description": "list or watch objects of kind QueryTemplate", + "operationId": "listQueryTemplate", + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "post": { + "tags": ["QueryTemplate"], + "description": "create a QueryTemplate", + "operationId": "createQueryTemplate", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "delete": { + "tags": ["QueryTemplate"], + "description": "delete collection of QueryTemplate", + "operationId": "deletecollectionQueryTemplate", + "parameters": [ + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates/{name}": { + "get": { + "tags": ["QueryTemplate"], + "description": "read the specified QueryTemplate", + "operationId": "getQueryTemplate", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "put": { + "tags": ["QueryTemplate"], + "description": "replace the specified QueryTemplate", + "operationId": "replaceQueryTemplate", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "delete": { + "tags": ["QueryTemplate"], + "description": "delete a QueryTemplate", + "operationId": "deleteQueryTemplate", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "patch": { + "tags": ["QueryTemplate"], + "description": "partially update the specified QueryTemplate", + "operationId": "updateQueryTemplate", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the QueryTemplate", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates/{name}/render": { + "get": { + "tags": ["QueryTemplate"], + "description": "connect GET requests to render of QueryTemplate", + "operationId": "getQueryTemplateRender", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "RenderedQuery" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the RenderedQuery", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/peakq.grafana.app/v0alpha1/querytemplates": { + "get": { + "tags": ["QueryTemplate"], + "description": "list or watch objects of kind QueryTemplate", + "operationId": "listQueryTemplateForAllNamespaces", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/peakq.grafana.app/v0alpha1/render": { + "summary": "an example at the root level", + "description": "longer description here?", + "post": { + "parameters": [ + { + "name": "variables", + "in": "query", + "description": "Each variable is prefixed with var-{variable}={value}", + "style": "form", + "explode": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "var-another": ["first", "second"], + "var-metricName": ["up"] + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {}, + "examples": { + "test": { + "summary": "hello", + "value": { + "title": "Test", + "vars": [ + { + "key": "metricName", + "defaultValues": ["down"] + } + ], + "targets": [ + { + "variables": { + "metricName": [ + { + "path": "$.expr", + "position": { + "start": 0, + "end": 10 + } + }, + { + "path": "$.expr", + "position": { + "start": 13, + "end": 23 + } + } + ] + }, + "properties": { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "foo" + }, + "editorMode": "builder", + "expr": "metricName + metricName + 42", + "instant": true, + "range": false, + "exemplar": false + } + } + ] + } + }, + "test2": { + "summary": "hello2", + "value": { + "title": "Test", + "vars": [ + { + "key": "metricName", + "defaultValues": ["down"] + } + ], + "targets": [ + { + "variables": { + "metricName": [ + { + "path": "$.expr", + "position": { + "start": 0, + "end": 10 + } + }, + { + "path": "$.expr", + "position": { + "start": 13, + "end": 23 + } + } + ] + }, + "properties": { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "foo" + }, + "instant": true, + "range": false, + "exemplar": false, + "editorMode": "builder", + "expr": "metricName + metricName + 42" + } + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "description": "Dummy object that represents a real query object", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "default": {} + }, + "x-kubernetes-list-type": "atomic" + } + } + } + } + } + } + } + } + }, + "/apis/peakq.grafana.app/v0alpha1/watch/namespaces/{namespace}/querytemplates": { + "get": { + "tags": ["QueryTemplate"], + "description": "watch individual changes to a list of QueryTemplate. deprecated: use the 'watch' parameter with a list operation instead.", + "operationId": "watchQueryTemplateList", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + } + } + }, + "x-kubernetes-action": "watchlist", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/peakq.grafana.app/v0alpha1/watch/namespaces/{namespace}/querytemplates/{name}": { + "get": { + "tags": ["QueryTemplate"], + "description": "watch changes to an object of kind QueryTemplate. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", + "operationId": "watchQueryTemplate", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + } + } + }, + "x-kubernetes-action": "watch", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "name", + "in": "path", + "description": "name of the QueryTemplate", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/peakq.grafana.app/v0alpha1/watch/querytemplates": { + "get": { + "tags": ["QueryTemplate"], + "description": "watch individual changes to a list of QueryTemplate. deprecated: use the 'watch' parameter with a list operation instead.", + "operationId": "watchQueryTemplateListForAllNamespaces", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + } + } + }, + "x-kubernetes-action": "watchlist", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + } + }, + "components": { + "schemas": { + "com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataQuery": { + "description": "Generic query properties", + "type": "object", + "properties": { + "datasource": { + "description": "The datasource", + "type": "object", + "required": ["type"], + "properties": { + "apiVersion": { + "description": "The apiserver version", + "type": "string" + }, + "type": { + "description": "The datasource plugin type", + "type": "string" + }, + "uid": { + "description": "Datasource UID (NOTE: name in k8s)", + "type": "string" + } + }, + "additionalProperties": false + }, + "hide": { + "description": "true if query is disabled (ie should not be returned to the dashboard)\nNOTE: this does not always imply that the query should not be executed since\nthe results from a hidden query may be used as the input to other queries (SSE etc)", + "type": "boolean" + }, + "intervalMs": { + "description": "Interval is the suggested duration between time points in a time series query.\nNOTE: the values for intervalMs is not saved in the query model. It is typically calculated\nfrom the interval required to fill a pixels in the visualization", + "type": "number" + }, + "maxDataPoints": { + "description": "MaxDataPoints is the maximum number of data points that should be returned from a time series query.\nNOTE: the values for maxDataPoints is not saved in the query model. It is typically calculated\nfrom the number of pixels visible in a visualization", + "type": "integer" + }, + "queryType": { + "description": "QueryType is an optional identifier for the type of query.\nIt can be used to distinguish different types of queries.", + "type": "string" + }, + "refId": { + "description": "RefID is the unique identifier of the query, set by the frontend call.", + "type": "string" + }, + "resultAssertions": { + "description": "Optionally define expected query result behavior", + "type": "object", + "required": ["typeVersion"], + "properties": { + "maxFrames": { + "description": "Maximum frame count", + "type": "integer" + }, + "type": { + "description": "Type asserts that the frame matches a known type structure.\n\n\nPossible enum values:\n - `\"\"` \n - `\"timeseries-wide\"` \n - `\"timeseries-long\"` \n - `\"timeseries-many\"` \n - `\"timeseries-multi\"` \n - `\"directory-listing\"` \n - `\"table\"` \n - `\"numeric-wide\"` \n - `\"numeric-multi\"` \n - `\"numeric-long\"` \n - `\"log-lines\"` ", + "type": "string", + "enum": [ + "", + "timeseries-wide", + "timeseries-long", + "timeseries-many", + "timeseries-multi", + "directory-listing", + "table", + "numeric-wide", + "numeric-multi", + "numeric-long", + "log-lines" + ], + "x-enum-description": {} + }, + "typeVersion": { + "description": "TypeVersion is the version of the Type property. Versions greater than 0.0 correspond to the dataplane\ncontract documentation https://grafana.github.io/dataplane/contract/.", + "type": "array", + "maxItems": 2, + "minItems": 2, + "items": { + "type": "integer" + } + } + }, + "additionalProperties": false + }, + "timeRange": { + "description": "TimeRange represents the query range\nNOTE: unlike generic /ds/query, we can now send explicit time values in each query\nNOTE: the values for timeRange are not saved in a dashboard, they are constructed on the fly", + "type": "object", + "required": ["from", "to"], + "properties": { + "from": { + "description": "From is the start time of the query.", + "type": "string", + "default": "now-6h", + "examples": ["now-1h"] + }, + "to": { + "description": "To is the end time of the query.", + "type": "string", + "default": "now", + "examples": ["now"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true, + "$schema": "https://json-schema.org/draft-04/schema" + }, + "com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured": { + "type": "object", + "additionalProperties": true, + "x-kubernetes-preserve-unknown-fields": true + }, + "com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate": { + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.QueryTemplate" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "peakq.grafana.app", + "kind": "QueryTemplate", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList": { + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "peakq.grafana.app", + "kind": "QueryTemplateList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Position": { + "description": "Position is where to do replacement in the targets during render.", + "type": "object", + "required": ["start", "end"], + "properties": { + "end": { + "description": "End is the byte offset of the end of the variable.", + "type": "integer", + "format": "int64", + "default": 0 + }, + "start": { + "description": "Start is the byte offset within TargetKey's property of the variable. It is the start location for replacements).", + "type": "integer", + "format": "int64", + "default": 0 + } + } + }, + "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.QueryTemplate": { + "type": "object", + "required": ["targets"], + "properties": { + "description": { + "description": "Longer description for why it is interesting", + "type": "string" + }, + "targets": { + "description": "Output variables", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Target" + } + ] + }, + "x-kubernetes-list-type": "set" + }, + "title": { + "description": "A display name", + "type": "string" + }, + "vars": { + "description": "The variables that can be used to render", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.TemplateVariable" + } + ] + }, + "x-kubernetes-list-map-keys": ["key"], + "x-kubernetes-list-type": "map" + } + } + }, + "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Target": { + "type": "object", + "required": ["variables", "properties"], + "properties": { + "dataType": { + "description": "DataType is the returned Dataplane type from the query.", + "type": "string" + }, + "properties": { + "description": "Query target", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataQuery" + } + ] + }, + "variables": { + "description": "Variables that will be replaced in the query", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.VariableReplacement" + } + ] + } + } + } + } + }, + "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.TemplateVariable": { + "description": "TemplateVariable is the definition of a variable that will be interpolated in targets.", + "type": "object", + "required": ["key"], + "properties": { + "defaultValues": { + "description": "DefaultValue is the value to be used when there is no selected value during render.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "key": { + "description": "Key is the name of the variable.", + "type": "string", + "default": "" + }, + "valueListDefinition": { + "description": "ValueListDefinition is the object definition used by the FE to get a list of possible values to select for render.", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + } + } + }, + "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.VariableReplacement": { + "description": "QueryVariable is the definition of a variable that will be interpolated in targets.", + "type": "object", + "required": ["path"], + "properties": { + "format": { + "description": "How values should be interpolated\n\nPossible enum values:\n - `\"csv\"` Formats variables with multiple values as a comma-separated string.\n - `\"doublequote\"` Formats single- and multi-valued variables into a comma-separated string\n - `\"json\"` Formats variables with multiple values as a comma-separated string.\n - `\"pipe\"` Formats variables with multiple values into a pipe-separated string.\n - `\"raw\"` Formats variables with multiple values into comma-separated string. This is the default behavior when no format is specified\n - `\"singlequote\"` Formats single- and multi-valued variables into a comma-separated string", + "type": "string", + "enum": ["csv", "doublequote", "json", "pipe", "raw", "singlequote"] + }, + "path": { + "description": "Path is the location of the property within a target. The format for this is not figured out yet (Maybe JSONPath?). Idea: [\"string\", int, \"string\"] where int indicates array offset", + "type": "string", + "default": "" + }, + "position": { + "description": "Positions is a list of where to perform the interpolation within targets during render. The first string is the Idx of the target as a string, since openAPI does not support ints as map keys", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Position" + } + ] + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { + "description": "APIResource specifies the name of a resource and whether it is namespaced.", + "type": "object", + "required": ["name", "singularName", "namespaced", "kind", "verbs"], + "properties": { + "categories": { + "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", + "type": "string" + }, + "kind": { + "description": "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", + "type": "string", + "default": "" + }, + "name": { + "description": "name is the plural name of the resource.", + "type": "string", + "default": "" + }, + "namespaced": { + "description": "namespaced indicates if a resource is namespaced or not.", + "type": "boolean", + "default": false + }, + "shortNames": { + "description": "shortNames is a list of suggested short names of the resource.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "singularName": { + "description": "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", + "type": "string", + "default": "" + }, + "storageVersionHash": { + "description": "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", + "type": "string" + }, + "verbs": { + "description": "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", + "type": "array", + "items": { + "type": "string", + "default": "" + } + }, + "version": { + "description": "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { + "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", + "type": "object", + "required": ["groupVersion", "resources"], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "groupVersion": { + "description": "groupVersion is the group and version this APIResourceList is for.", + "type": "string", + "default": "" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "resources": { + "description": "resources contains the name of the resources and if they are namespaced.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResource" + } + ] + }, + "x-kubernetes-list-type": "atomic" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions": { + "description": "DeleteOptions may be provided when deleting an API object.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "dryRun": { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "gracePeriodSeconds": { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "type": "integer", + "format": "int64" + }, + "ignoreStoreReadErrorWithClusterBreakingPotential": { + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "type": "boolean" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "orphanDependents": { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "type": "boolean" + }, + "preconditions": { + "description": "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions" + } + ] + }, + "propagationPolicy": { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { + "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", + "type": "object", + "properties": { + "continue": { + "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", + "type": "string" + }, + "remainingItemCount": { + "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", + "type": "integer", + "format": "int64" + }, + "resourceVersion": { + "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": "string" + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": "string" + }, + "fieldsV1": { + "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" + } + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": "string" + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": "string" + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": "string" + }, + "time": { + "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "type": "object", + "properties": { + "annotations": { + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "creationTimestamp": { + "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "type": "integer", + "format": "int64" + }, + "deletionTimestamp": { + "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set", + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": "string" + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "type": "integer", + "format": "int64" + }, + "labels": { + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": "string" + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" + } + ] + }, + "x-kubernetes-list-map-keys": ["uid"], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "type": "object", + "required": ["apiVersion", "kind", "name", "uid"], + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string", + "default": "" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": "boolean" + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": "boolean" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string", + "default": "" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string", + "default": "" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string", + "default": "" + } + }, + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Patch": { + "description": "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions": { + "description": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", + "type": "object", + "properties": { + "resourceVersion": { + "description": "Specifies the target ResourceVersion", + "type": "string" + }, + "uid": { + "description": "Specifies the target UID.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Status": { + "description": "Status is a return value for calls that don't return other objects.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "code": { + "description": "Suggested HTTP return code for this status, 0 if not set.", + "type": "integer", + "format": "int32" + }, + "details": { + "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails" + } + ], + "x-kubernetes-list-type": "atomic" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "message": { + "description": "A human-readable description of the status of this operation.", + "type": "string" + }, + "metadata": { + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + }, + "reason": { + "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", + "type": "string" + }, + "status": { + "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause": { + "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", + "type": "object", + "properties": { + "field": { + "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", + "type": "string" + }, + "message": { + "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", + "type": "string" + }, + "reason": { + "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails": { + "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", + "type": "object", + "properties": { + "causes": { + "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "The group attribute of the resource associated with the status StatusReason.", + "type": "string" + }, + "kind": { + "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", + "type": "string" + }, + "retryAfterSeconds": { + "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", + "type": "integer", + "format": "int32" + }, + "uid": { + "description": "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "type": "string", + "format": "date-time" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": { + "description": "Event represents a single event to a watched resource.", + "type": "object", + "required": ["type", "object"], + "properties": { + "object": { + "description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension" + } + ] + }, + "type": { + "type": "string", + "default": "" + } + } + }, + "io.k8s.apimachinery.pkg.runtime.RawExtension": { + "description": "RawExtension is used to hold extensions in external versions.\n\nTo use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types.\n\n// Internal package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.Object `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// External package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// On the wire, the JSON will look something like this:\n\n\t{\n\t\t\"kind\":\"MyAPIObject\",\n\t\t\"apiVersion\":\"v1\",\n\t\t\"myPlugin\": {\n\t\t\t\"kind\":\"PluginA\",\n\t\t\t\"aOption\":\"foo\",\n\t\t},\n\t}\n\nSo what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.)", + "type": "object" + } + } + } +} diff --git a/pkg/tests/apis/core/openapi_test.go b/pkg/tests/apis/core/openapi_test.go new file mode 100644 index 00000000000..c861f54f4fb --- /dev/null +++ b/pkg/tests/apis/core/openapi_test.go @@ -0,0 +1,120 @@ +package core + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/version" + apimachineryversion "k8s.io/apimachinery/pkg/version" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestIntegrationOpenAPIs(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + check := []schema.GroupVersion{{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + }, { + Group: "folder.grafana.app", + Version: "v0alpha1", + }, { + Group: "peakq.grafana.app", + Version: "v0alpha1", + }} + + h := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesFoldersServiceV2, // Will be default on by G12 + featuremgmt.FlagQueryService, // Query Library + }, + }) + + t.Run("check valid version response", func(t *testing.T) { + disco := h.NewDiscoveryClient() + req := disco.RESTClient().Get(). + Prefix("version"). + SetHeader("Accept", "application/json") + + result := req.Do(context.Background()) + require.NoError(t, result.Error()) + + raw, err := result.Raw() + require.NoError(t, err) + info := apimachineryversion.Info{} + err = json.Unmarshal(raw, &info) + require.NoError(t, err) + + // Make sure the gitVersion is parsable + v, err := version.Parse(info.GitVersion) + require.NoError(t, err) + require.Equal(t, info.Major, fmt.Sprintf("%d", v.Major())) + require.Equal(t, info.Minor, fmt.Sprintf("%d", v.Minor())) + }) + + t.Run("build open", func(t *testing.T) { + // Now write each OpenAPI spec to a static file + dir := filepath.Join("..", "..", "..", "..", "openapi") + for _, gv := range check { + path := fmt.Sprintf("/openapi/v3/apis/%s/%s", gv.Group, gv.Version) + rsp := apis.DoRequest(h, apis.RequestParams{ + Method: http.MethodGet, + Path: path, + User: h.Org1.Admin, + }, &apis.AnyResource{}) + + require.NotNil(t, rsp.Response) + require.Equal(t, 200, rsp.Response.StatusCode, path) + + var prettyJSON bytes.Buffer + err := json.Indent(&prettyJSON, rsp.Body, "", " ") + require.NoError(t, err) + pretty := prettyJSON.String() + + write := false + fpath := filepath.Join(dir, fmt.Sprintf("%s-%s.json", gv.Group, gv.Version)) + + // nolint:gosec + // We can ignore the gosec G304 warning since this is a test and the function is only called with explicit paths + body, err := os.ReadFile(fpath) + if err == nil { + if !assert.JSONEq(t, string(body), pretty) { + t.Logf("openapi spec has changed: %s", path) + t.Fail() + write = true + } + } else { + t.Errorf("missing openapi spec for: %s", path) + write = true + } + + if write { + e2 := os.WriteFile(fpath, []byte(pretty), 0644) + if e2 != nil { + t.Errorf("error writing file: %s", e2.Error()) + } + } + } + }) +} diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index 2403743d76a..efcd10c9a98 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -20,9 +20,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer/yaml" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/version" yamlutil "k8s.io/apimachinery/pkg/util/yaml" - apimachineryversion "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" @@ -608,31 +606,6 @@ func (c *K8sTestHelper) NewDiscoveryClient() *discovery.DiscoveryClient { return client } -func (c *K8sTestHelper) GetVersionInfo() apimachineryversion.Info { - c.t.Helper() - - disco := c.NewDiscoveryClient() - req := disco.RESTClient().Get(). - Prefix("version"). - SetHeader("Accept", "application/json") - - result := req.Do(context.Background()) - require.NoError(c.t, result.Error()) - - raw, err := result.Raw() - require.NoError(c.t, err) - info := apimachineryversion.Info{} - err = json.Unmarshal(raw, &info) - require.NoError(c.t, err) - - // Make sure the gitVersion is parsable - v, err := version.Parse(info.GitVersion) - require.NoError(c.t, err) - require.Equal(c.t, info.Major, fmt.Sprintf("%d", v.Major())) - require.Equal(c.t, info.Minor, fmt.Sprintf("%d", v.Minor())) - return info -} - func (c *K8sTestHelper) GetGroupVersionInfoJSON(group string) string { c.t.Helper() diff --git a/pkg/tests/apis/playlist/playlist_test.go b/pkg/tests/apis/playlist/playlist_test.go index fa93da02e26..49ee9fee4ca 100644 --- a/pkg/tests/apis/playlist/playlist_test.go +++ b/pkg/tests/apis/playlist/playlist_test.go @@ -49,9 +49,6 @@ func TestIntegrationPlaylist(t *testing.T) { EnableFeatureToggles: []string{}, })) - // Ensure the k8s version is valid - _ = h.GetVersionInfo() - // The accepted verbs will change when dual write is enabled disco := h.GetGroupVersionInfoJSON("playlist.grafana.app") // fmt.Printf("%s", disco) From acbdc1f415c032a39ce74db409aad361dd7d54ca Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Tue, 28 Jan 2025 08:20:23 +0000 Subject: [PATCH 123/894] Tempo: Add separate options groups (#99310) * Separate options groups * Editor row for styling --- .../traceql/TempoQueryBuilderOptions.tsx | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx index 25a13cc349a..f273bc788ca 100644 --- a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx @@ -1,7 +1,9 @@ +import { css } from '@emotion/css'; import * as React from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; import { EditorField, EditorRow } from '@grafana/experimental'; -import { AutoSizeInput, RadioButtonGroup } from '@grafana/ui'; +import { AutoSizeInput, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { QueryOptionGroup } from '../_importedDependencies/datasources/prometheus/QueryOptionGroup'; import { SearchTableType } from '../dataquery.gen'; @@ -28,6 +30,8 @@ const parseIntWithFallback = (val: string, fallback: number) => { }; export const TempoQueryBuilderOptions = React.memo(({ onChange, query, isStreaming }) => { + const styles = useStyles2(getStyles); + if (!query.hasOwnProperty('limit')) { query.limit = DEFAULT_LIMIT; } @@ -60,21 +64,23 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query, is // } // }; - const collapsedInfoList = [ + const collapsedSearchOptions = [ `Limit: ${query.limit || DEFAULT_LIMIT}`, `Spans Limit: ${query.spss || DEFAULT_SPSS}`, `Table Format: ${query.tableType === SearchTableType.Traces ? 'Traces' : 'Spans'}`, '|', - `Step: ${query.step || 'auto'}`, - // `Exemplars: ${query.exemplars !== undefined ? query.exemplars : 'auto'}`, - '|', `Streaming: ${isStreaming ? 'Enabled' : 'Disabled'}`, ]; + const collapsedMetricsOptions = [ + `Step: ${query.step || 'auto'}`, + // `Exemplars: ${query.exemplars !== undefined ? query.exemplars : 'auto'}`, + ]; + return ( - <> - - + +
+ (({ onChange, query, is onChange={onTableTypeChange} /> + } tooltipInteractive> +
{isStreaming ? 'Enabled' : 'Disabled'}
+
+
+ + (({ onChange, query, is {/* value={query.exemplars}*/} {/* />*/} {/**/} - } tooltipInteractive> -
{isStreaming ? 'Enabled' : 'Disabled'}
-
- - +
+
); }); @@ -163,3 +172,17 @@ const StreamingTooltip = () => { }; TempoQueryBuilderOptions.displayName = 'TempoQueryBuilderOptions'; + +const getStyles = (theme: GrafanaTheme2) => { + return { + options: css({ + display: 'flex', + width: '-webkit-fill-available', + gap: theme.spacing(1), + + '> div': { + width: 'auto', + }, + }), + }; +}; From 3ed138543d9ddbdf33ee5337c0febfd96998c4ce Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Tue, 28 Jan 2025 08:36:31 +0000 Subject: [PATCH 124/894] Chore: Fix translation imports from `@grafana/ui` (#99614) --- .betterer.results | 36 +++++-------------- .../TimePicker/TimePickerWithHistory.tsx | 2 +- .../components/rule-viewer/RuleViewer.tsx | 2 +- .../share-snapshot/UpsertSnapshot.tsx | 3 +- .../features/dashboard/services/TimeSrv.ts | 2 +- .../explore/ExploreRunQueryButton.tsx | 4 +-- public/app/features/explore/state/time.ts | 2 +- .../ServiceAccountCreatePage.tsx | 2 +- .../app/features/trails/DataTrailSettings.tsx | 2 +- .../MetricSelect/NativeHistogramBadge.tsx | 2 +- .../trails/banners/NativeHistogramBanner.tsx | 2 +- 11 files changed, 19 insertions(+), 40 deletions(-) diff --git a/.betterer.results b/.betterer.results index 2c72e385439..3d15c748b2e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1194,9 +1194,6 @@ exports[`better eslint`] = { "public/app/core/components/TagFilter/TagOption.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], - "public/app/core/components/TimePicker/TimePickerWithHistory.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/core/components/TimeSeries/TimeSeries.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/uPlot/PlotLegend\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], [0, 0, 0, "\'@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder\' import is restricted from being used by a pattern. Import from the public export instead.", "1"], @@ -2499,12 +2496,11 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], + [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. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] ], "public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -3728,8 +3724,7 @@ exports[`better eslint`] = { ], "public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/UpsertSnapshot.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/Input/Input\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "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()", "1"] ], "public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawerConfirmAction.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/ConfirmModal/ConfirmContent\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] @@ -4230,9 +4225,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], - "public/app/features/dashboard/services/TimeSrv.ts:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/dashboard/state/DashboardMigrator.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -4654,9 +4646,8 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], "public/app/features/explore/ExploreRunQueryButton.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "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()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] ], "public/app/features/explore/ExploreToolbar.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -5063,9 +5054,6 @@ exports[`better eslint`] = { "public/app/features/explore/state/time.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/explore/state/time.ts:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/explore/state/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -5970,9 +5958,6 @@ exports[`better eslint`] = { "public/app/features/search/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/serviceaccounts/ServiceAccountPage.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"], @@ -6173,8 +6158,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/trails/DataTrailSettings.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/trails/DataTrailsHistory.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/datetime/rangeutil\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], @@ -6201,9 +6185,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], [0, 0, 0, "No untranslated strings. Wrap text with ", "8"] ], - "public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/trails/MetricsHeader.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] @@ -6217,8 +6198,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/trails/banners/NativeHistogramBanner.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "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()", "0"] ], "public/app/features/transformers/FilterByValueTransformer/FilterByValueFilterEditor.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/transformations/transformers/filterByValue\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], diff --git a/public/app/core/components/TimePicker/TimePickerWithHistory.tsx b/public/app/core/components/TimePicker/TimePickerWithHistory.tsx index 64c08d31b67..db4ac5ac21d 100644 --- a/public/app/core/components/TimePicker/TimePickerWithHistory.tsx +++ b/public/app/core/components/TimePicker/TimePickerWithHistory.tsx @@ -2,8 +2,8 @@ import { uniqBy } from 'lodash'; import { AppEvents, TimeRange, isDateTime, rangeUtil } from '@grafana/data'; import { TimeRangePickerProps, TimeRangePicker } from '@grafana/ui'; -import { t } from '@grafana/ui/src/utils/i18n'; import appEvents from 'app/core/app_events'; +import { t } from 'app/core/internationalization'; import { LocalStorageValueProvider } from '../LocalStorageValueProvider'; diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx index bb5bd689b32..f1d3064fadb 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx @@ -5,9 +5,9 @@ import { useMeasure } from 'react-use'; import { NavModelItem, UrlQueryValue } from '@grafana/data'; import { Alert, LinkButton, LoadingBar, Stack, TabContent, Text, TextLink, useStyles2 } from '@grafana/ui'; -import { Trans, t } from '@grafana/ui/src/utils/i18n'; import { PageInfoItem } from 'app/core/components/Page/types'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; +import { Trans, t } from 'app/core/internationalization'; import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule'; import { RuleActionsButtons } from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; import { AlertInstanceTotalState, CombinedRule, RuleHealth, RuleIdentifier } from 'app/types/unified-alerting'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/UpsertSnapshot.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/UpsertSnapshot.tsx index 5458db42ec1..57cd584afb1 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/UpsertSnapshot.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/UpsertSnapshot.tsx @@ -5,8 +5,7 @@ import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { SceneObjectRef, VizPanel } from '@grafana/scenes'; import { Alert, Button, Divider, Field, RadioButtonGroup, Stack, Text, useStyles2 } from '@grafana/ui'; import { Input } from '@grafana/ui/src/components/Input/Input'; -import { t } from '@grafana/ui/src/utils/i18n'; -import { Trans } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; import { getExpireOptions } from '../../ShareSnapshotTab'; diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index 7761ed37fbc..b8261ec1d32 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -15,9 +15,9 @@ import { } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { sceneGraph } from '@grafana/scenes'; -import { t } from '@grafana/ui/src/utils/i18n'; import appEvents from 'app/core/app_events'; import { config } from 'app/core/config'; +import { t } from 'app/core/internationalization'; import { AutoRefreshInterval, contextSrv, ContextSrv } from 'app/core/services/context_srv'; import { getCopiedTimeRange, getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker'; import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; diff --git a/public/app/features/explore/ExploreRunQueryButton.tsx b/public/app/features/explore/ExploreRunQueryButton.tsx index 7dd87dbd6c3..7cb37a62a45 100644 --- a/public/app/features/explore/ExploreRunQueryButton.tsx +++ b/public/app/features/explore/ExploreRunQueryButton.tsx @@ -4,7 +4,7 @@ import { ConnectedProps, connect } from 'react-redux'; import { config, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Button, ButtonVariant, Dropdown, Menu, ToolbarButton } from '@grafana/ui'; -import { t } from '@grafana/ui/src/utils/i18n'; +import { t } from 'app/core/internationalization'; import { useSelector } from 'app/types'; import { changeDatasource } from './state/datasource'; @@ -28,7 +28,7 @@ interface ExploreRunQueryButtonProps { export type Props = ConnectedProps & ExploreRunQueryButtonProps; -/* +/* This component does not validate datasources before running them. Root datasource validation should happen outside this component and can pass in an undefined if invalid If query level validation is done and a query datasource is invalid, pass in disabled = true */ diff --git a/public/app/features/explore/state/time.ts b/public/app/features/explore/state/time.ts index 38683f45bf4..f794042c113 100644 --- a/public/app/features/explore/state/time.ts +++ b/public/app/features/explore/state/time.ts @@ -10,8 +10,8 @@ import { } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { RefreshPicker } from '@grafana/ui'; -import { t } from '@grafana/ui/src/utils/i18n'; import appEvents from 'app/core/app_events'; +import { t } from 'app/core/internationalization'; import { getTimeRange, refreshIntervalToSortOrder, stopQueryState } from 'app/core/utils/explore'; import { getCopiedTimeRange, getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; diff --git a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx index fa24596b328..c9816d0fb1a 100644 --- a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx @@ -3,13 +3,13 @@ import { FormProvider, useForm } from 'react-hook-form'; import { config, getBackendSrv, locationService } from '@grafana/runtime'; import { Button, Input, Field, FieldSet } from '@grafana/ui'; -import { t, Trans } from '@grafana/ui/src/utils/i18n'; import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api'; import { RolePickerSelect } from 'app/core/components/RolePickerDrawer/RolePickerSelect'; import { contextSrv } from 'app/core/core'; +import { t, Trans } from 'app/core/internationalization'; import { AccessControlAction, OrgRole, Role, ServiceAccountCreateApiResponse, ServiceAccountDTO } from 'app/types'; import { OrgRolePicker } from '../admin/OrgRolePicker'; diff --git a/public/app/features/trails/DataTrailSettings.tsx b/public/app/features/trails/DataTrailSettings.tsx index 2584b7bb930..7007b511ad0 100644 --- a/public/app/features/trails/DataTrailSettings.tsx +++ b/public/app/features/trails/DataTrailSettings.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { Dropdown, Switch, ToolbarButton, useStyles2 } from '@grafana/ui'; -import { Trans } from '@grafana/ui/src/utils/i18n'; +import { Trans } from 'app/core/internationalization'; import { MetricScene } from './MetricScene'; import { MetricSelectScene } from './MetricSelect/MetricSelectScene'; diff --git a/public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx b/public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx index f2c551f3d70..21fed5c0ce7 100644 --- a/public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx +++ b/public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { SceneObjectBase } from '@grafana/scenes'; import { Badge, useStyles2 } from '@grafana/ui'; -import { Trans } from '@grafana/ui/src/utils/i18n'; +import { Trans } from 'app/core/internationalization'; export class NativeHistogramBadge extends SceneObjectBase { public static Component = () => { diff --git a/public/app/features/trails/banners/NativeHistogramBanner.tsx b/public/app/features/trails/banners/NativeHistogramBanner.tsx index 5813c06d30f..68ca35c0c67 100644 --- a/public/app/features/trails/banners/NativeHistogramBanner.tsx +++ b/public/app/features/trails/banners/NativeHistogramBanner.tsx @@ -3,7 +3,7 @@ import { useState, type Dispatch, type SetStateAction } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2, useTheme2, Alert, Button } from '@grafana/ui'; -import { t, Trans } from '@grafana/ui/src/utils/i18n'; +import { t, Trans } from 'app/core/internationalization'; import { DataTrail } from '../DataTrail'; import { reportExploreMetrics } from '../interactions'; From 9949a56f3b831b1b37633c6343ec5a4bcf000146 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 02:08:48 -0700 Subject: [PATCH 125/894] K8s: Fix legacy stats (#99623) --- pkg/storage/unified/federated/stats.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/pkg/storage/unified/federated/stats.go b/pkg/storage/unified/federated/stats.go index 98440932ace..a126bcb30fb 100644 --- a/pkg/storage/unified/federated/stats.go +++ b/pkg/storage/unified/federated/stats.go @@ -31,7 +31,17 @@ func (s *LegacyStatsGetter) GetStats(ctx context.Context, in *resource.ResourceS rsp := &resource.ResourceStatsResponse{} err = helper.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { - fn := func(table, where, g, r string) error { + fn := func(table, where, g, r string, existCheck bool) error { + // if existCheck is true, do not error out if the table does not exist + if existCheck { + exists, err := sess.IsTableExist(helper.Table(table)) + if !exists { + return nil + } else if err != nil { + return err + } + } + count, err := sess.Table(helper.Table(table)).Where(where, info.OrgID, in.Folder).Count() if err != nil { return err @@ -47,25 +57,25 @@ func (s *LegacyStatsGetter) GetStats(ctx context.Context, in *resource.ResourceS group := "sql-fallback" // Legacy alert rule table - err = fn("alert_rule", "org_id=? AND dashboard_uid=?", group, "alertrules") + err = fn("alert_rule", "org_id=? AND dashboard_uid=?", group, "alertrules", false) if err != nil { return err } // Legacy dashboard table - err = fn("dashboard", "org_id=? AND folder_uid=?", group, "dashboards") + err = fn("dashboard", "org_id=? AND folder_uid=?", group, "dashboards", true) if err != nil { return err } // Legacy folder table - err = fn("folder", "org_id=? AND parent_uid=?", group, "folders") + err = fn("folder", "org_id=? AND parent_uid=?", group, "folders", true) if err != nil { return err } // Legacy library_elements table - err = fn("library_element", "org_id=? AND folder_uid=?", group, "library_elements") + err = fn("library_element", "org_id=? AND folder_uid=?", group, "library_elements", false) if err != nil { return err } From 959a942b5cec3dd024e923874b765b9853f862ac Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 02:28:12 -0700 Subject: [PATCH 126/894] K8s: Dashboards: fix in folder count (#99622) --- .../dashboards/service/dashboard_service.go | 12 ++++ .../service/dashboard_service_test.go | 62 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 0c90a37542c..dd8d95348ad 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1401,6 +1401,18 @@ func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *das } func (dr DashboardServiceImpl) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) { + if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { + dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ + OrgId: orgID, + FolderUIDs: folderUIDs, + }) + if err != nil { + return 0, err + } + + return int64(len(dashs)), nil + } + return dr.dashboardStore.CountDashboardsInFolders(ctx, &dashboards.CountDashboardsInFolderRequest{FolderUIDs: folderUIDs, OrgID: orgID}) } diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 27653a41b63..fbe99cf0fb4 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -1818,6 +1818,68 @@ func TestCountDashboardsInOrg(t *testing.T) { }) } +func TestCountInFolders(t *testing.T) { + fakeStore := dashboards.FakeDashboardStore{} + defer fakeStore.AssertExpectations(t) + service := &DashboardServiceImpl{ + cfg: setting.NewCfg(), + dashboardStore: &fakeStore, + } + dashs := &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder 1"), + }, + }, + { + Key: &resource.ResourceKey{ + Name: "uid2", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 2"), + []byte("folder 1"), + }, + }, + }, + }, + TotalHits: 2, + } + + t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { + service.features = featuremgmt.WithFeatures() + fakeStore.On("CountDashboardsInFolders", mock.Anything, mock.Anything).Return(int64(1), nil).Once() + _, err := service.CountInFolders(context.Background(), 1, []string{"folder1"}, &user.SignedInUser{}) + require.NoError(t, err) + fakeStore.AssertExpectations(t) + }) + + t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { + ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(dashs, nil).Once() + result, err := service.CountInFolders(ctx, 1, []string{"folder1"}, &user.SignedInUser{}) + require.NoError(t, err) + require.Equal(t, result, int64(2)) + }) +} + func TestLegacySaveCommandToUnstructured(t *testing.T) { namespace := "test-namespace" t.Run("successfully converts save command to unstructured", func(t *testing.T) { From ae0de61b92316661f9c44cb1a3897665dd7a59ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 09:39:55 +0000 Subject: [PATCH 127/894] Update dependency @types/babel__preset-env to v7.10.0 (#99611) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4c42d41e41d..dfca5abd1a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8892,9 +8892,9 @@ __metadata: linkType: hard "@types/babel__preset-env@npm:^7": - version: 7.9.7 - resolution: "@types/babel__preset-env@npm:7.9.7" - checksum: 10/624425a84d9149aec04795fed6b1ac2f27dfd5d7976fde479bb1a4d754de34c92cdc28a1a373a5826382a68127b536420a0e090aa5fae522cb62724b7a571cb5 + version: 7.10.0 + resolution: "@types/babel__preset-env@npm:7.10.0" + checksum: 10/7d4d12758d89708afe327079d7d7580e8af3292295f087b8a9a48e12ac1d90aadc18ac3bc00f9b0cbc8778f3ce9fe778801d4d49b7691a75e3f13a901b69fd07 languageName: node linkType: hard From 58a3e0ae869b7161b94993b2c0057e972b8eb7c0 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:48:19 +0100 Subject: [PATCH 128/894] Remove @grafana/experimental in @grafana/o11y-ds-frontend and @grafana/sql (#99501) * Remove @grafana/experimental in @grafana/o11y-ds-frontend * Remove @grafana/experimental in @grafana/sql * Fix mock in test --- packages/grafana-o11y-ds-frontend/package.json | 2 +- .../src/NodeGraph/NodeGraphSettings.tsx | 2 +- .../grafana-o11y-ds-frontend/src/SpanBar/SpanBarSettings.tsx | 2 +- .../src/TraceToLogs/TraceToLogsSettings.tsx | 2 +- .../src/TraceToMetrics/TraceToMetricsSettings.tsx | 2 +- .../src/TraceToProfiles/TraceToProfilesSettings.tsx | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-sql/src/components/QueryEditor.tsx | 2 +- packages/grafana-sql/src/components/QueryHeader.tsx | 2 +- .../src/components/configuration/ConnectionLimits.tsx | 4 ++-- .../src/components/query-editor-raw/QueryEditorRaw.tsx | 2 +- .../src/components/visual-query-builder/GroupByRow.tsx | 2 +- .../src/components/visual-query-builder/OrderByRow.tsx | 2 +- .../src/components/visual-query-builder/SelectColumn.tsx | 2 +- .../visual-query-builder/SelectFunctionParameters.tsx | 2 +- .../src/components/visual-query-builder/SelectRow.test.tsx | 1 + .../src/components/visual-query-builder/SelectRow.tsx | 2 +- .../src/components/visual-query-builder/VisualEditor.tsx | 2 +- packages/grafana-sql/src/datasource/SqlDatasource.ts | 2 +- packages/grafana-sql/src/defaults.ts | 2 +- packages/grafana-sql/src/types.ts | 2 +- yarn.lock | 4 ++-- 22 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index fbd1ee55713..4da97db322a 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -20,7 +20,7 @@ "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", "@grafana/e2e-selectors": "11.5.0-pre", - "@grafana/experimental": "2.1.6", + "@grafana/plugin-ui": "0.9.6", "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/packages/grafana-o11y-ds-frontend/src/NodeGraph/NodeGraphSettings.tsx b/packages/grafana-o11y-ds-frontend/src/NodeGraph/NodeGraphSettings.tsx index 8b6f3c5f4b6..fd9ba9cd6c9 100644 --- a/packages/grafana-o11y-ds-frontend/src/NodeGraph/NodeGraphSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/NodeGraph/NodeGraphSettings.tsx @@ -7,7 +7,7 @@ import { GrafanaTheme2, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/plugin-ui'; import { InlineField, InlineFieldRow, InlineSwitch, useStyles2 } from '@grafana/ui'; export interface NodeGraphOptions { diff --git a/packages/grafana-o11y-ds-frontend/src/SpanBar/SpanBarSettings.tsx b/packages/grafana-o11y-ds-frontend/src/SpanBar/SpanBarSettings.tsx index a612dee380d..11710cef528 100644 --- a/packages/grafana-o11y-ds-frontend/src/SpanBar/SpanBarSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/SpanBar/SpanBarSettings.tsx @@ -7,7 +7,7 @@ import { toOption, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/plugin-ui'; import { InlineField, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui'; export interface SpanBarOptions { diff --git a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx index d94e09b0411..b5bca15f317 100644 --- a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx @@ -3,7 +3,7 @@ import { useCallback, useMemo } from 'react'; import * as React from 'react'; import { DataSourceJsonData, DataSourceInstanceSettings, DataSourcePluginOptionsEditorProps } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSection } from '@grafana/plugin-ui'; import { DataSourcePicker } from '@grafana/runtime'; import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui'; diff --git a/packages/grafana-o11y-ds-frontend/src/TraceToMetrics/TraceToMetricsSettings.tsx b/packages/grafana-o11y-ds-frontend/src/TraceToMetrics/TraceToMetricsSettings.tsx index 98cff44f304..966c1703b54 100644 --- a/packages/grafana-o11y-ds-frontend/src/TraceToMetrics/TraceToMetricsSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TraceToMetrics/TraceToMetricsSettings.tsx @@ -7,7 +7,7 @@ import { GrafanaTheme2, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSection } from '@grafana/plugin-ui'; import { DataSourcePicker } from '@grafana/runtime'; import { Button, InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui'; diff --git a/packages/grafana-o11y-ds-frontend/src/TraceToProfiles/TraceToProfilesSettings.tsx b/packages/grafana-o11y-ds-frontend/src/TraceToProfiles/TraceToProfilesSettings.tsx index 9b2d5ca31cc..cb5af242966 100644 --- a/packages/grafana-o11y-ds-frontend/src/TraceToProfiles/TraceToProfilesSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TraceToProfiles/TraceToProfilesSettings.tsx @@ -9,7 +9,7 @@ import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSection } from '@grafana/plugin-ui'; import { DataSourcePicker, DataSourceWithBackend, getDataSourceSrv } from '@grafana/runtime'; import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui'; diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 509a4b9832f..d2a690cde8e 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -17,7 +17,7 @@ "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", "@grafana/e2e-selectors": "11.5.0-pre", - "@grafana/experimental": "2.1.6", + "@grafana/plugin-ui": "0.9.6", "@grafana/runtime": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", "@react-awesome-query-builder/ui": "6.6.4", diff --git a/packages/grafana-sql/src/components/QueryEditor.tsx b/packages/grafana-sql/src/components/QueryEditor.tsx index 487c4f2c1c9..fa7bc199209 100644 --- a/packages/grafana-sql/src/components/QueryEditor.tsx +++ b/packages/grafana-sql/src/components/QueryEditor.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useAsync } from 'react-use'; import { QueryEditorProps } from '@grafana/data'; -import { EditorMode } from '@grafana/experimental'; +import { EditorMode } from '@grafana/plugin-ui'; import { Space } from '@grafana/ui'; import { SqlDatasource } from '../datasource/SqlDatasource'; diff --git a/packages/grafana-sql/src/components/QueryHeader.tsx b/packages/grafana-sql/src/components/QueryHeader.tsx index f1bede6cb4f..d7f7412d453 100644 --- a/packages/grafana-sql/src/components/QueryHeader.tsx +++ b/packages/grafana-sql/src/components/QueryHeader.tsx @@ -3,7 +3,7 @@ import { useCopyToClipboard } from 'react-use'; import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { EditorField, EditorHeader, EditorMode, EditorRow, FlexItem, InlineSelect } from '@grafana/experimental'; +import { EditorField, EditorHeader, EditorMode, EditorRow, FlexItem, InlineSelect } from '@grafana/plugin-ui'; import { reportInteraction } from '@grafana/runtime'; import { Button, InlineSwitch, RadioButtonGroup, Tooltip, Space } from '@grafana/ui'; diff --git a/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx b/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx index 17218d5188a..50bc7e82ae7 100644 --- a/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx +++ b/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx @@ -1,7 +1,7 @@ import { DataSourceSettings } from '@grafana/data'; -import { ConfigSubSection, Stack } from '@grafana/experimental'; +import { ConfigSubSection } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; -import { Field, Icon, InlineLabel, Label, Switch, Tooltip } from '@grafana/ui'; +import { Field, Icon, InlineLabel, Label, Stack, Switch, Tooltip } from '@grafana/ui'; import { SQLConnectionLimits, SQLOptions } from '../../types'; diff --git a/packages/grafana-sql/src/components/query-editor-raw/QueryEditorRaw.tsx b/packages/grafana-sql/src/components/query-editor-raw/QueryEditorRaw.tsx index 2f9f7bd6eeb..ed79057cd55 100644 --- a/packages/grafana-sql/src/components/query-editor-raw/QueryEditorRaw.tsx +++ b/packages/grafana-sql/src/components/query-editor-raw/QueryEditorRaw.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef } from 'react'; import * as React from 'react'; -import { LanguageDefinition, SQLEditor } from '@grafana/experimental'; +import { LanguageDefinition, SQLEditor } from '@grafana/plugin-ui'; import { SQLQuery } from '../../types'; diff --git a/packages/grafana-sql/src/components/visual-query-builder/GroupByRow.tsx b/packages/grafana-sql/src/components/visual-query-builder/GroupByRow.tsx index 33460b4e169..8f6b463c8ee 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/GroupByRow.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/GroupByRow.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { SelectableValue, toOption } from '@grafana/data'; -import { AccessoryButton, EditorList, InputGroup } from '@grafana/experimental'; +import { AccessoryButton, EditorList, InputGroup } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import { QueryEditorGroupByExpression } from '../../expressions'; diff --git a/packages/grafana-sql/src/components/visual-query-builder/OrderByRow.tsx b/packages/grafana-sql/src/components/visual-query-builder/OrderByRow.tsx index 3c2c274eeab..8e7286153a9 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/OrderByRow.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/OrderByRow.tsx @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import * as React from 'react'; import { SelectableValue, toOption } from '@grafana/data'; -import { EditorField, InputGroup } from '@grafana/experimental'; +import { EditorField, InputGroup } from '@grafana/plugin-ui'; import { Input, RadioButtonGroup, Select, Space } from '@grafana/ui'; import { SQLExpression } from '../../types'; diff --git a/packages/grafana-sql/src/components/visual-query-builder/SelectColumn.tsx b/packages/grafana-sql/src/components/visual-query-builder/SelectColumn.tsx index 774ef7501f8..cea8b01e72a 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/SelectColumn.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/SelectColumn.tsx @@ -2,7 +2,7 @@ import { useId } from 'react'; import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; interface Props { diff --git a/packages/grafana-sql/src/components/visual-query-builder/SelectFunctionParameters.tsx b/packages/grafana-sql/src/components/visual-query-builder/SelectFunctionParameters.tsx index 3d5c2203447..16241e3aac4 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/SelectFunctionParameters.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/SelectFunctionParameters.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useId, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { InlineLabel, Input, Select, Stack, useStyles2 } from '@grafana/ui'; import { QueryEditorExpressionType } from '../../expressions'; diff --git a/packages/grafana-sql/src/components/visual-query-builder/SelectRow.test.tsx b/packages/grafana-sql/src/components/visual-query-builder/SelectRow.test.tsx index a54e1ef72ab..3b31add19ad 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/SelectRow.test.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/SelectRow.test.tsx @@ -12,6 +12,7 @@ import { SelectRow } from './SelectRow'; // Mock featureToggle sqlQuerybuilderFunctionParameters jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), config: { featureToggles: { sqlQuerybuilderFunctionParameters: true, diff --git a/packages/grafana-sql/src/components/visual-query-builder/SelectRow.tsx b/packages/grafana-sql/src/components/visual-query-builder/SelectRow.tsx index 302977d88cd..a553ece4a6c 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/SelectRow.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/SelectRow.tsx @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { SelectableValue, toOption } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { Button, Select, Stack, useStyles2 } from '@grafana/ui'; diff --git a/packages/grafana-sql/src/components/visual-query-builder/VisualEditor.tsx b/packages/grafana-sql/src/components/visual-query-builder/VisualEditor.tsx index ca5e37fd41d..3a267301163 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/VisualEditor.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/VisualEditor.tsx @@ -1,6 +1,6 @@ import { useAsync } from 'react-use'; -import { EditorRows, EditorRow, EditorField } from '@grafana/experimental'; +import { EditorRows, EditorRow, EditorField } from '@grafana/plugin-ui'; import { DB, QueryEditorProps, QueryRowFilter } from '../../types'; import { QueryToolbox } from '../query-editor-raw/QueryToolbox'; diff --git a/packages/grafana-sql/src/datasource/SqlDatasource.ts b/packages/grafana-sql/src/datasource/SqlDatasource.ts index 121d30a4731..ca4675034c7 100644 --- a/packages/grafana-sql/src/datasource/SqlDatasource.ts +++ b/packages/grafana-sql/src/datasource/SqlDatasource.ts @@ -17,7 +17,7 @@ import { VariableWithMultiSupport, TimeRange, } from '@grafana/data'; -import { EditorMode } from '@grafana/experimental'; +import { EditorMode } from '@grafana/plugin-ui'; import { BackendDataSourceResponse, DataSourceWithBackend, diff --git a/packages/grafana-sql/src/defaults.ts b/packages/grafana-sql/src/defaults.ts index 4532bdcc3a6..06930dcc8cc 100644 --- a/packages/grafana-sql/src/defaults.ts +++ b/packages/grafana-sql/src/defaults.ts @@ -1,4 +1,4 @@ -import { EditorMode } from '@grafana/experimental'; +import { EditorMode } from '@grafana/plugin-ui'; import { QueryFormat, SQLQuery } from './types'; import { createFunctionField, setGroupByField } from './utils/sql.utils'; diff --git a/packages/grafana-sql/src/types.ts b/packages/grafana-sql/src/types.ts index dd2af526b81..00c1a6e0456 100644 --- a/packages/grafana-sql/src/types.ts +++ b/packages/grafana-sql/src/types.ts @@ -9,7 +9,7 @@ import { TimeRange, toOption as toOptionFromData, } from '@grafana/data'; -import { CompletionItemKind, EditorMode, LanguageDefinition } from '@grafana/experimental'; +import { CompletionItemKind, EditorMode, LanguageDefinition } from '@grafana/plugin-ui'; import { QueryWithDefaults } from './defaults'; import { diff --git a/yarn.lock b/yarn.lock index dfca5abd1a2..899a9ca41b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3497,7 +3497,7 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/experimental": "npm:2.1.6" + "@grafana/plugin-ui": "npm:0.9.6" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/schema": "npm:11.5.0-pre" "@grafana/tsconfig": "npm:^2.0.0" @@ -3875,7 +3875,7 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/experimental": "npm:2.1.6" + "@grafana/plugin-ui": "npm:0.9.6" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/tsconfig": "npm:^2.0.0" "@grafana/ui": "npm:11.5.0-pre" From 9c7618160d8fc8aa27f153fa228bbca4d483feb6 Mon Sep 17 00:00:00 2001 From: Stephanie Closson Date: Tue, 28 Jan 2025 04:52:50 -0500 Subject: [PATCH 129/894] Update queries-conditions.md (#99592) --- .../alerting/fundamentals/alert-rules/queries-conditions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md index 229816db306..877d1b5e76a 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md +++ b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md @@ -103,7 +103,7 @@ The threshold expression allows you to compare two single values. It returns `0` - Is above (x > y) - Is below (x < y) - Is within range (x > y1 AND x < y2) -- Is outside range (x < y1 AND x > y2) +- Is outside range (x < y1 OR x > y2) **Classic condition (legacy)** From a05f539dd2413cab20b72466928c789f01d2a3b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:02:23 +0000 Subject: [PATCH 130/894] Update dependency @types/lodash to v4.17.15 (#99632) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/mssql/package.json | 2 +- .../app/plugins/datasource/mysql/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 44 +++++++++---------- 19 files changed, 40 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index b7c6dc24ac1..54a32f06f62 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "@types/jquery": "3.5.32", "@types/js-yaml": "^4.0.5", "@types/jsurl": "^1.2.28", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", "@types/node": "22.10.10", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index db7b6f85e1c..8531d80b874 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -64,7 +64,7 @@ "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "16.0.0", "@types/history": "4.7.11", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/papaparse": "5.3.15", "@types/react": "18.3.18", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 0ff1830c8a2..66de5e2225d 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -67,7 +67,7 @@ "@testing-library/user-event": "14.5.2", "@types/d3": "^7", "@types/jest": "^29.5.4", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "@types/react-virtualized-auto-sizer": "1.0.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 44a22efae28..740e9309832 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -92,7 +92,7 @@ "@types/eslint": "9.6.1", "@types/jest": "29.5.14", "@types/jquery": "3.5.32", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 6df8bf7b619..56876aab254 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -58,7 +58,7 @@ "@types/angular": "1.8.9", "@types/history": "4.7.11", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/systemjs": "6.15.1", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index d2a690cde8e..a5093fc6aee 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -40,7 +40,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 548c93ee375..a8eb8f1be2b 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -64,7 +64,7 @@ "@react-aria/utils": "3.27.0", "@tanstack/react-virtual": "^3.5.1", "@types/jquery": "3.5.32", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/react-table": "7.7.20", "calculate-size": "1.1.1", "classnames": "2.5.1", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 1a6ea15bb03..69ca46d32f5 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -32,7 +32,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index c278483c99f..8aee980ffa1 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -34,7 +34,7 @@ "@testing-library/user-event": "14.5.2", "@types/debounce-promise": "3.1.9", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index 5ebdb88eb3e..4094ca3e08b 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -22,7 +22,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 75c7b27c73d..bba725e85e2 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -26,7 +26,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 3726f299ff1..a1666e38d59 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -29,7 +29,7 @@ "@testing-library/user-event": "14.5.2", "@types/d3-random": "^3.0.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index a0aefef0d6f..5f1fd12a7ad 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -29,7 +29,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/logfmt": "^1.2.3", "@types/node": "22.10.10", "@types/react": "18.3.18", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index af7fab99453..1c422fdda68 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -22,7 +22,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index b35ff254ad9..b1a791dd64b 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -22,7 +22,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index df78383ef64..becc966e421 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -22,7 +22,7 @@ "@testing-library/dom": "10.4.0", "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index b42d506a96c..9f6e20f1d5b 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -45,7 +45,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index b52961af94a..f9f8a40298c 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -25,7 +25,7 @@ "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.1.0", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "22.10.10", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/yarn.lock b/yarn.lock index 899a9ca41b1..14402fbda83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2717,7 +2717,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -2759,7 +2759,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" @@ -2789,7 +2789,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -2831,7 +2831,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2871,7 +2871,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/logfmt": "npm:^1.2.3" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" @@ -2912,7 +2912,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" @@ -2943,7 +2943,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" @@ -2971,7 +2971,7 @@ __metadata: "@testing-library/dom": "npm:10.4.0" "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3009,7 +3009,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -3062,7 +3062,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -3112,7 +3112,7 @@ __metadata: "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.1.0" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3205,7 +3205,7 @@ __metadata: "@rollup/plugin-node-resolve": "npm:16.0.0" "@types/d3-interpolate": "npm:^3.0.0" "@types/history": "npm:4.7.11" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/papaparse": "npm:5.3.15" "@types/react": "npm:18.3.18" @@ -3398,7 +3398,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" "@types/react-virtualized-auto-sizer": "npm:1.0.4" @@ -3627,7 +3627,7 @@ __metadata: "@types/eslint": "npm:9.6.1" "@types/jest": "npm:29.5.14" "@types/jquery": "npm:3.5.32" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.5" @@ -3719,7 +3719,7 @@ __metadata: "@types/angular": "npm:1.8.9" "@types/history": "npm:4.7.11" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/systemjs": "npm:6.15.1" @@ -3885,7 +3885,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.10.10" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -4048,7 +4048,7 @@ __metadata: "@types/is-hotkey": "npm:0.1.10" "@types/jest": "npm:29.5.14" "@types/jquery": "npm:3.5.32" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/mock-raf": "npm:1.0.6" "@types/node": "npm:22.10.10" "@types/prismjs": "npm:1.26.5" @@ -9647,10 +9647,10 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:*, @types/lodash@npm:4.17.14, @types/lodash@npm:^4.14.172": - version: 4.17.14 - resolution: "@types/lodash@npm:4.17.14" - checksum: 10/6ee40725f3e192f5ef1f493caca19210aa7acd7adc3136b8dba84d418a35be0abea0668105aed9f696ad62a54310a9c0d328971ad4b157f5bcda700424ed5aae +"@types/lodash@npm:*, @types/lodash@npm:4.17.15, @types/lodash@npm:^4.14.172": + version: 4.17.15 + resolution: "@types/lodash@npm:4.17.15" + checksum: 10/27b348b5971b9c670215331b52448a13d7d65bf1fbd320a7049c9c153c1186ff5d116ba75f05f07d32d7ece8a992b26a30c7bdc9be22a3d1e4e3e6068aa04603 languageName: node linkType: hard @@ -17819,7 +17819,7 @@ __metadata: "@types/jquery": "npm:3.5.32" "@types/js-yaml": "npm:^4.0.5" "@types/jsurl": "npm:^1.2.28" - "@types/lodash": "npm:4.17.14" + "@types/lodash": "npm:4.17.15" "@types/logfmt": "npm:^1.2.3" "@types/lucene": "npm:^2" "@types/node": "npm:22.10.10" From 8c2824cf3b0ab8fa46d01b9b10aca400679e9215 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:03:08 +0100 Subject: [PATCH 131/894] MultiCombobox: Autosize (#99510) * Add input auto resizing * Initial auotsize * Initial implementation * Remove px * Remove unused import * Handle backspace and support the width prop * Make sizing work with useComboboxFloat * Remove unused expression * Add supoport for min and max width * Change space for clicking --- .../Combobox/MultiCombobox.internal.story.tsx | 18 ++++++++ .../src/components/Combobox/MultiCombobox.tsx | 43 ++++++++++++------- .../src/components/Combobox/SuffixIcon.tsx | 17 ++++++++ .../Combobox/getMultiComboboxStyles.ts | 20 ++++++++- .../Combobox/useMultiInputAutoSize.tsx | 33 ++++++++++++++ 5 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 packages/grafana-ui/src/components/Combobox/SuffixIcon.tsx create mode 100644 packages/grafana-ui/src/components/Combobox/useMultiInputAutoSize.tsx diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx index 0dc4f2e9128..5d27d2ac594 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx @@ -48,6 +48,24 @@ export const Basic: Story = { }, }; +export const AutoSize: Story = { + args: { ...commonArgs, width: 'auto', minWidth: 20 }, + render: (args) => { + const [{ value }, setArgs] = useArgs(); + + return ( + { + action('onChange')(val); + setArgs({ value: val }); + }} + /> + ); + }, +}; + const ManyOptionsStory: StoryFn = ({ numberOfOptions = 1e4, ...args }) => { const [value, setValue] = useState([]); const [options, setOptions] = useState([]); diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 0cede71af9e..4b819ab4a28 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -10,13 +10,13 @@ import { Box } from '../Layout/Box/Box'; import { Stack } from '../Layout/Stack/Stack'; import { Portal } from '../Portal/Portal'; import { ScrollContainer } from '../ScrollContainer/ScrollContainer'; -import { Spinner } from '../Spinner/Spinner'; import { Text } from '../Text/Text'; import { Tooltip } from '../Tooltip'; import { ComboboxBaseProps, AutoSizeConditionals, VIRTUAL_OVERSCAN_ITEMS } from './Combobox'; import { NotFoundError } from './MessageRows'; import { OptionListItem } from './OptionListItem'; +import { SuffixIcon } from './SuffixIcon'; import { ValuePill } from './ValuePill'; import { itemFilter, itemToString } from './filter'; import { getComboboxStyles, MENU_OPTION_HEIGHT, MENU_OPTION_HEIGHT_DESCRIPTION } from './getComboboxStyles'; @@ -24,6 +24,7 @@ import { getMultiComboboxStyles } from './getMultiComboboxStyles'; import { ALL_OPTION_VALUE, ComboboxOption } from './types'; import { useComboboxFloat } from './useComboboxFloat'; import { MAX_SHOWN_ITEMS, useMeasureMulti } from './useMeasureMulti'; +import { useMultiInputAutoSize } from './useMultiInputAutoSize'; interface MultiComboboxBaseProps extends Omit, 'value' | 'onChange'> { value?: T[] | Array>; @@ -34,7 +35,19 @@ interface MultiComboboxBaseProps extends Omit = MultiComboboxBaseProps & AutoSizeConditionals; export const MultiCombobox = (props: MultiComboboxProps) => { - const { options, placeholder, onChange, value, width, enableAllOption, invalid, loading, disabled } = props; + const { + options, + placeholder, + onChange, + value, + width, + enableAllOption, + invalid, + loading, + disabled, + minWidth, + maxWidth, + } = props; const isAsync = typeof options === 'function'; const selectedItems = useMemo(() => { @@ -129,7 +142,7 @@ export const MultiCombobox = (props: MultiComboboxPro }); const { - //getToggleButtonProps, + getToggleButtonProps, //getLabelProps, isOpen, highlightedIndex, @@ -199,7 +212,7 @@ export const MultiCombobox = (props: MultiComboboxPro }); const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, isOpen); - const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled); + const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled, width, minWidth, maxWidth); const virtualizerOptions = { count: items.length, @@ -214,13 +227,10 @@ export const MultiCombobox = (props: MultiComboboxPro // Selected items that show up in the input field const visibleItems = isOpen ? selectedItems.slice(0, MAX_SHOWN_ITEMS) : selectedItems.slice(0, shownItems); + const { inputRef, inputWidth } = useMultiInputAutoSize(inputValue); return ( -
-
+
+
{visibleItems.map((item, index) => ( (props: MultiComboboxPro getDropdownProps({ disabled, preventKeyAction: isOpen, - placeholder: selectedItems.length > 0 ? undefined : placeholder, + placeholder, + ref: inputRef, + style: { width: inputWidth }, }) )} /> - {loading && ( -
- -
- )} + +
+ +
diff --git a/packages/grafana-ui/src/components/Combobox/SuffixIcon.tsx b/packages/grafana-ui/src/components/Combobox/SuffixIcon.tsx new file mode 100644 index 00000000000..04fa29b699b --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/SuffixIcon.tsx @@ -0,0 +1,17 @@ +import { Icon } from '../Icon/Icon'; + +interface Props { + isLoading: boolean; + isOpen: boolean; +} + +export const SuffixIcon = ({ isLoading, isOpen }: Props) => { + const suffixIcon = isLoading + ? 'spinner' + : // If it's loading, show loading icon. Otherwise, icon indicating menu state + isOpen + ? 'search' + : 'angle-down'; + + return ; +}; diff --git a/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts b/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts index 4347acdb0e7..06bd2dd2a22 100644 --- a/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts +++ b/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts @@ -9,18 +9,33 @@ export const getMultiComboboxStyles = ( theme: GrafanaTheme2, isOpen: boolean, invalid?: boolean, - disabled?: boolean + disabled?: boolean, + width?: number | 'auto', + minWidth?: number, + maxWidth?: number ) => { const inputStyles = getInputStyles({ theme, invalid }); const focusStyles = getFocusStyles(theme); + const wrapperWidth = width && width !== 'auto' ? theme.spacing(width) : '100%'; + const wrapperMinWidth = minWidth ? theme.spacing(minWidth) : ''; + const wrapperMaxWidth = maxWidth ? theme.spacing(maxWidth) : ''; + return { + container: css({ + width: width === 'auto' ? 'auto' : wrapperWidth, + minWidth: wrapperMinWidth, + maxWidth: wrapperMaxWidth, + display: width === 'auto' ? 'inline-block' : 'block', + }), // wraps everything wrapper: cx( inputStyles.input, css({ display: 'flex', + width: '100%', gap: theme.spacing(0.5), padding: theme.spacing(0.5), + paddingRight: 28, // Account for suffix '&:focus-within': { ...focusStyles, }, @@ -31,7 +46,8 @@ export const getMultiComboboxStyles = ( outline: 'none', background: 'transparent', flexGrow: 1, - minWidth: '0', + maxWidth: '100%', + minWidth: 40, // This is a bit arbitrary, but is used to leave some space for clicking. This will override the minWidth property '&::placeholder': { color: theme.colors.text.disabled, }, diff --git a/packages/grafana-ui/src/components/Combobox/useMultiInputAutoSize.tsx b/packages/grafana-ui/src/components/Combobox/useMultiInputAutoSize.tsx new file mode 100644 index 00000000000..30ade93a632 --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/useMultiInputAutoSize.tsx @@ -0,0 +1,33 @@ +import { useLayoutEffect, useRef, useState } from 'react'; + +import { measureText } from '../../utils'; + +export function useMultiInputAutoSize(inputValue: string) { + const inputRef = useRef(null); + const initialInputWidth = useRef(0); // Store initial width to prevent resizing on backspace + const [inputWidth, setInputWidth] = useState(''); + + useLayoutEffect(() => { + if (inputRef.current && inputValue == null && initialInputWidth.current === 0) { + initialInputWidth.current = inputRef?.current.getBoundingClientRect().width; + } + + if (!inputRef.current || inputValue == null) { + setInputWidth(''); + return; + } + + const fontSize = window.getComputedStyle(inputRef.current).fontSize; + const textWidth = measureText(inputRef.current.value || '', parseInt(fontSize, 10)).width; + + if (textWidth < initialInputWidth.current) { + // Let input fill all space before resizing + setInputWidth(''); + } else { + // Add pixels to prevent clipping + setInputWidth(`${textWidth + 5}px`); + } + }, [inputValue]); + + return { inputRef, inputWidth }; +} From 58a279e109571cf4f2d4128f62e15347ece8e88f Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:28:22 +0000 Subject: [PATCH 132/894] Release: update changelog for 10.4.15 (#99634) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bb3ea46a8e..34fd866c684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 10.4.15 (2025-01-28) + +### Features and enhancements + +- **Security:** Update to Go 1.22.11 - Backport to v10.4.x [#99128](https://github.com/grafana/grafana/pull/99128), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.22.11 - Backport to v10.4.x (Enterprise) + +### Bug fixes + +- **Azure/GCM:** Improve error display [#97590](https://github.com/grafana/grafana/pull/97590), [@aangelisc](https://github.com/aangelisc) + + # 11.4.0 (2024-12-05) From f6fc39e71fa02fd454bfcb5b0672b2a2cd08ab16 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:30:33 +0000 Subject: [PATCH 133/894] Update dependency @types/webpack-env to v1.18.7 (#99633) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 14402fbda83..3485996dde5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10280,9 +10280,9 @@ __metadata: linkType: hard "@types/webpack-env@npm:^1.18.4": - version: 1.18.6 - resolution: "@types/webpack-env@npm:1.18.6" - checksum: 10/463e545441d206e52bf24b4577a503bf9a96198a69b3ad1234d15bf96e9a6d5ccb25bb1197e520541cb8da968232305fe8d8400595d5745e16abfdc4089e73d0 + version: 1.18.7 + resolution: "@types/webpack-env@npm:1.18.7" + checksum: 10/b07ca300b8e8af9ffad2bfdd6a662adffe49f86710a72cec01e4d10bb99444ed1ce45efbedda66ccd399b26ee22e7eadefd7c118a64047f5ea465b556ba86cf3 languageName: node linkType: hard From a28328d7645dbbde185aae1bca6b620899621b61 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 28 Jan 2025 11:34:26 +0100 Subject: [PATCH 134/894] Alerting: Call the deletion reason provider even if the rule is no longer scheduled (#99571) Alerting: Call the deletion reason provider even if the rule is not scheduled anymore --- pkg/services/ngalert/schedule/alert_rule.go | 8 +++++++- pkg/services/ngalert/schedule/alert_rule_test.go | 8 ++++++++ pkg/services/ngalert/schedule/recording_rule.go | 14 +++++++++----- .../ngalert/schedule/recording_rule_test.go | 11 ++++++++++- pkg/services/ngalert/schedule/schedule.go | 10 +++++----- .../ngalert/schedule/schedule_unit_test.go | 6 +++--- 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 8f56fdfa8a9..f4a301aff78 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -42,6 +42,8 @@ type Rule interface { Type() ngmodels.RuleType // Status indicates the status of the evaluating rule. Status() ngmodels.RuleStatus + // Identifier returns the identifier of the rule. + Identifier() ngmodels.AlertRuleKeyWithGroup } type ruleFactoryFunc func(context.Context, *ngmodels.AlertRule) Rule @@ -71,7 +73,7 @@ func newRuleFactory( if rule.Type() == ngmodels.RuleTypeRecording { return newRecordingRule( ctx, - rule.GetKey(), + rule.GetKeyWithGroup(), maxAttempts, clock, evalFactory, @@ -178,6 +180,10 @@ func newAlertRule( } } +func (a *alertRule) Identifier() ngmodels.AlertRuleKeyWithGroup { + return a.key +} + func (a *alertRule) Type() ngmodels.RuleType { return ngmodels.RuleTypeAlerting } diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index bb9e648c291..4593a4052e0 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -269,6 +269,14 @@ func TestAlertRule(t *testing.T) { }) } +func TestAlertRuleIdentifier(t *testing.T) { + t.Run("should return correct identifier", func(t *testing.T) { + key := models.GenerateRuleKeyWithGroup(1) + r := blankRuleForTests(context.Background(), key) + require.Equal(t, key, r.Identifier()) + }) +} + func blankRuleForTests(ctx context.Context, key models.AlertRuleKeyWithGroup) *alertRule { return newAlertRule(ctx, key, nil, false, 0, nil, nil, nil, nil, nil, nil, log.NewNopLogger(), nil, nil, nil) } diff --git a/pkg/services/ngalert/schedule/recording_rule.go b/pkg/services/ngalert/schedule/recording_rule.go index 3abf44b6457..954b0869a4a 100644 --- a/pkg/services/ngalert/schedule/recording_rule.go +++ b/pkg/services/ngalert/schedule/recording_rule.go @@ -30,7 +30,7 @@ type RuleStatus struct { } type recordingRule struct { - key ngmodels.AlertRuleKey + key ngmodels.AlertRuleKeyWithGroup ctx context.Context evalCh chan *Evaluation @@ -56,8 +56,8 @@ type recordingRule struct { tracer tracing.Tracer } -func newRecordingRule(parent context.Context, key ngmodels.AlertRuleKey, maxAttempts int64, clock clock.Clock, evalFactory eval.EvaluatorFactory, cfg setting.RecordingRuleSettings, logger log.Logger, metrics *metrics.Scheduler, tracer tracing.Tracer, writer RecordingWriter, evalAppliedHook evalAppliedFunc, stopAppliedHook stopAppliedFunc) *recordingRule { - ctx, stop := util.WithCancelCause(ngmodels.WithRuleKey(parent, key)) +func newRecordingRule(parent context.Context, key ngmodels.AlertRuleKeyWithGroup, maxAttempts int64, clock clock.Clock, evalFactory eval.EvaluatorFactory, cfg setting.RecordingRuleSettings, logger log.Logger, metrics *metrics.Scheduler, tracer tracing.Tracer, writer RecordingWriter, evalAppliedHook evalAppliedFunc, stopAppliedHook stopAppliedFunc) *recordingRule { + ctx, stop := util.WithCancelCause(ngmodels.WithRuleKey(parent, key.AlertRuleKey)) return &recordingRule{ key: key, ctx: ctx, @@ -80,6 +80,10 @@ func newRecordingRule(parent context.Context, key ngmodels.AlertRuleKey, maxAtte } } +func (r *recordingRule) Identifier() ngmodels.AlertRuleKeyWithGroup { + return r.key +} + func (r *recordingRule) Type() ngmodels.RuleType { return ngmodels.RuleTypeRecording } @@ -301,7 +305,7 @@ func (r *recordingRule) evaluationDoneTestHook(ev *Evaluation) { return } - r.evalAppliedHook(r.key, ev.scheduledAt) + r.evalAppliedHook(r.key.AlertRuleKey, ev.scheduledAt) } // frameRef gets frames from a QueryDataResponse for a particular refID. It returns an error if the frames do not exist or have no data. @@ -328,5 +332,5 @@ func (r *recordingRule) stopApplied() { return } - r.stopAppliedHook(r.key) + r.stopAppliedHook(r.key.AlertRuleKey) } diff --git a/pkg/services/ngalert/schedule/recording_rule_test.go b/pkg/services/ngalert/schedule/recording_rule_test.go index 3a85921fdfe..7e72453bb9f 100644 --- a/pkg/services/ngalert/schedule/recording_rule_test.go +++ b/pkg/services/ngalert/schedule/recording_rule_test.go @@ -152,11 +152,20 @@ func TestRecordingRule(t *testing.T) { }) } +func TestRecordingRuleIdentifier(t *testing.T) { + t.Run("should return correct identifier", func(t *testing.T) { + key := models.GenerateRuleKeyWithGroup(1) + r := blankRecordingRuleForTests(context.Background()) + r.key = key + require.Equal(t, key, r.Identifier()) + }) +} + func blankRecordingRuleForTests(ctx context.Context) *recordingRule { st := setting.RecordingRuleSettings{ Enabled: true, } - return newRecordingRule(context.Background(), models.AlertRuleKey{}, 0, nil, nil, st, log.NewNopLogger(), nil, nil, writer.FakeWriter{}, nil, nil) + return newRecordingRule(context.Background(), models.AlertRuleKeyWithGroup{}, 0, nil, nil, st, log.NewNopLogger(), nil, nil, writer.FakeWriter{}, nil, nil) } func TestRecordingRule_Integration(t *testing.T) { diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index e02755eb0ec..3bc21115ca0 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -198,7 +198,7 @@ func (sch *schedule) deleteAlertRule(ctx context.Context, keys ...ngmodels.Alert // It can happen that the scheduler has deleted the alert rule before the // Ruler API has called DeleteAlertRule. This can happen as requests to // the Ruler API do not hold an exclusive lock over all scheduler operations. - rule, ok := sch.schedulableAlertRules.del(key) + _, ok := sch.schedulableAlertRules.del(key) if !ok { sch.log.Info("Alert rule cannot be removed from the scheduler as it is not scheduled", key.LogContext()...) } @@ -210,7 +210,7 @@ func (sch *schedule) deleteAlertRule(ctx context.Context, keys ...ngmodels.Alert } // stop rule evaluation - reason := sch.getRuleStopReason(ctx, key, rule) + reason := sch.getRuleStopReason(ctx, ruleRoutine.Identifier()) ruleRoutine.Stop(reason) } // Our best bet at this point is that we update the metrics with what we hope to schedule in the next tick. @@ -218,14 +218,14 @@ func (sch *schedule) deleteAlertRule(ctx context.Context, keys ...ngmodels.Alert sch.updateRulesMetrics(alertRules) } -func (sch *schedule) getRuleStopReason(ctx context.Context, key ngmodels.AlertRuleKey, rule *ngmodels.AlertRule) error { +func (sch *schedule) getRuleStopReason(ctx context.Context, key ngmodels.AlertRuleKeyWithGroup) error { // If the ruleStopReasonProvider is defined, we will use it to get the reason why the // alert rule was stopped. If it returns an error, we will use the default reason. - if sch.ruleStopReasonProvider == nil || rule == nil { + if sch.ruleStopReasonProvider == nil { return errRuleDeleted } - stopReason, err := sch.ruleStopReasonProvider.FindReason(ctx, sch.log, rule.GetKeyWithGroup()) + stopReason, err := sch.ruleStopReasonProvider.FindReason(ctx, sch.log, key) if err != nil { sch.log.New(key.LogContext()...).Error("Failed to get stop reason", "error", err) return errRuleDeleted diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 311a419f88b..575f19cda27 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -991,7 +991,7 @@ func TestSchedule_deleteAlertRule(t *testing.T) { require.False(t, sch.registry.exists(key)) }) - t.Run("it should not call ruleStopReasonProvider if the rule is not found in the registry", func(t *testing.T) { + t.Run("it should still call ruleStopReasonProvider if the rule is not found in the registry", func(t *testing.T) { mockReasonProvider := new(mockAlertRuleStopReasonProvider) expectedReason := errors.New("some rule deletion reason") mockReasonProvider.On("FindReason", mock.Anything, mock.Anything, mock.Anything).Return(expectedReason, nil) @@ -1008,9 +1008,9 @@ func TestSchedule_deleteAlertRule(t *testing.T) { sch.deleteAlertRule(ctx, key) - mockReasonProvider.AssertNotCalled(t, "FindReason") + mockReasonProvider.AssertCalled(t, "FindReason", mock.Anything, mock.Anything, rule.GetKeyWithGroup()) - require.ErrorIs(t, info.(*alertRule).ctx.Err(), errRuleDeleted) + require.ErrorIs(t, info.(*alertRule).ctx.Err(), expectedReason) require.False(t, sch.registry.exists(key)) }) }) From a66857dfbea5c1d7c111742095859bf04e124e7c Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:54:23 +0000 Subject: [PATCH 135/894] Release: update changelog for 11.0.10 (#99636) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34fd866c684..337028dfe77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 11.0.10 (2025-01-28) + +### Features and enhancements + +- **Security:** Update to Go 1.22.11 - Backport to v11.0.x [#99127](https://github.com/grafana/grafana/pull/99127), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.22.11 - Backport to v11.0.x (Enterprise) + +### Bug fixes + +- **Azure/GCM:** Improve error display [#97592](https://github.com/grafana/grafana/pull/97592), [@aangelisc](https://github.com/aangelisc) + + # 10.4.15 (2025-01-28) From 8e6a868178623a4ec7b479246f33ddc630a6f8d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:56:34 +0000 Subject: [PATCH 136/894] Update dependency papaparse to v5.5.2 (#99635) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/grafana-data/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 8531d80b874..8ba4574e8df 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -51,7 +51,7 @@ "moment": "2.30.1", "moment-timezone": "0.5.46", "ol": "7.4.0", - "papaparse": "5.5.1", + "papaparse": "5.5.2", "react-use": "17.6.0", "rxjs": "7.8.1", "string-hash": "^1.1.3", diff --git a/yarn.lock b/yarn.lock index 3485996dde5..7b812284201 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3225,7 +3225,7 @@ __metadata: moment: "npm:2.30.1" moment-timezone: "npm:0.5.46" ol: "npm:7.4.0" - papaparse: "npm:5.5.1" + papaparse: "npm:5.5.2" react: "npm:18.3.1" react-dom: "npm:18.3.1" react-use: "npm:17.6.0" @@ -23759,10 +23759,10 @@ __metadata: languageName: node linkType: hard -"papaparse@npm:5.5.1": - version: 5.5.1 - resolution: "papaparse@npm:5.5.1" - checksum: 10/6c3b47bd316cf11936641abe5590c6fb1e5b04297d1c594965a44dba89cc0f26b4e4d61eb1933352c538a30c4915d110628d14cf4d60cef0b8245c9a26477602 +"papaparse@npm:5.5.2": + version: 5.5.2 + resolution: "papaparse@npm:5.5.2" + checksum: 10/f5d25ccb10b1b9e87a0fe2d2965c7f8a83223cd79ec8963dd7e8766e8e97a6228dc6c20738a78148dfc74f32b2372003de732a7d50593f38b6a2bb5092710bc7 languageName: node linkType: hard From d2eac460cf04f2fbd876f923be61400133b7849b Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:25:01 +0000 Subject: [PATCH 137/894] Release: update changelog for 11.1.11 (#99639) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 337028dfe77..f5ca2310ca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 11.1.11 (2025-01-28) + +### Features and enhancements + +- **Security:** Update to Go 1.22.11 - Backport to v11.1.x [#99126](https://github.com/grafana/grafana/pull/99126), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.22.11 - Backport to v11.1.x (Enterprise) + +### Bug fixes + +- **Azure/GCM:** Improve error display [#97595](https://github.com/grafana/grafana/pull/97595), [@aangelisc](https://github.com/aangelisc) + + # 11.0.10 (2025-01-28) From ecc9b0c10c6693d596bcd961d2e522978d94176b Mon Sep 17 00:00:00 2001 From: Gareth Dawson Date: Tue, 28 Jan 2025 18:44:22 +0700 Subject: [PATCH 138/894] partner data sources: remove experimental imports from datasources (#99370) * remove experimental imports from datasources * add plugin-ui deps * remove * trigger workflow * Make versions of plugin-ui fixed * Fix import in test --------- Co-authored-by: Ivana Huckova --- .../components/ArgQueryEditor/ArgQueryEditor.tsx | 2 +- .../ConfigEditor/AzureCredentialsForm.tsx | 2 +- .../components/ConfigEditor/ConfigEditor.tsx | 2 +- .../ConfigEditor/CurrentUserFallbackCredentials.tsx | 2 +- .../LogsQueryEditor/AdvancedResourcePicker.tsx | 2 +- .../components/LogsQueryEditor/LogsQueryEditor.tsx | 2 +- .../MetricsQueryEditor/AdvancedResourcePicker.tsx | 2 +- .../MetricsQueryEditor/DimensionFields.tsx | 2 +- .../MetricsQueryEditor/MetricsQueryEditor.tsx | 2 +- .../components/QueryEditor/QueryHeader.tsx | 2 +- .../components/TracesQueryEditor/Filter.tsx | 2 +- .../components/TracesQueryEditor/Filters.tsx | 2 +- .../TracesQueryEditor/TracesQueryEditor.tsx | 2 +- .../azuremonitor/components/shared/Field.tsx | 2 +- .../plugins/datasource/azuremonitor/package.json | 2 +- .../cloud-monitoring/components/Aggregation.tsx | 2 +- .../cloud-monitoring/components/AliasBy.tsx | 2 +- .../cloud-monitoring/components/Alignment.tsx | 2 +- .../components/AnnotationQueryEditor.tsx | 2 +- .../components/ConfigEditor/ConfigEditor.tsx | 2 +- .../cloud-monitoring/components/GraphPeriod.tsx | 2 +- .../cloud-monitoring/components/GroupBy.tsx | 2 +- .../cloud-monitoring/components/LabelFilter.tsx | 2 +- .../components/LookbackPeriodSelect.tsx | 2 +- .../components/MetricQueryEditor.tsx | 3 ++- .../cloud-monitoring/components/Preprocessor.tsx | 2 +- .../cloud-monitoring/components/Project.tsx | 2 +- .../cloud-monitoring/components/PromQLEditor.tsx | 2 +- .../cloud-monitoring/components/QueryEditor.tsx | 2 +- .../cloud-monitoring/components/QueryHeader.tsx | 2 +- .../datasource/cloud-monitoring/components/SLO.tsx | 2 +- .../cloud-monitoring/components/SLOQueryEditor.tsx | 2 +- .../cloud-monitoring/components/Selector.tsx | 2 +- .../cloud-monitoring/components/Service.tsx | 2 +- .../components/VisualMetricQueryEditor.tsx | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../editor/query/influxql/visual/FromSection.tsx | 2 +- .../query/influxql/visual/PartListSection.tsx | 2 +- .../editor/query/influxql/visual/TagsSection.tsx | 2 +- .../visual/VisualInfluxQLEditor.tags.test.tsx | 13 ++++++------- .../influxdb/fsql/datasource.flightsql.ts | 2 +- .../influxdb/fsql/sqlCompletionProvider.ts | 2 +- .../mssql/configuration/ConfigurationEditor.tsx | 2 +- .../datasource/mssql/configuration/Kerberos.tsx | 2 +- public/app/plugins/datasource/mssql/datasource.ts | 2 +- public/app/plugins/datasource/mssql/package.json | 2 +- .../datasource/mssql/sqlCompletionProvider.ts | 2 +- yarn.lock | 6 +++--- 48 files changed, 56 insertions(+), 56 deletions(-) diff --git a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx index a06b3739072..4c326a44fb3 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx @@ -1,7 +1,7 @@ import { intersection } from 'lodash'; import { useState, useMemo } from 'react'; -import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental'; +import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx index fa80caadc82..6ab8bc69a18 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { AzureAuthType, AzureCredentials, getAzureClouds } from '@grafana/azure-sdk'; import { SelectableValue } from '@grafana/data'; -import { ConfigSection } from '@grafana/experimental'; +import { ConfigSection } from '@grafana/plugin-ui'; import { Select, Field } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx index e4246af0996..bdf2129e11d 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx @@ -1,7 +1,7 @@ import { PureComponent } from 'react'; import { DataSourcePluginOptionsEditorProps, SelectableValue, updateDatasourcePluginOption } from '@grafana/data'; -import { ConfigSection, DataSourceDescription } from '@grafana/experimental'; +import { ConfigSection, DataSourceDescription } from '@grafana/plugin-ui'; import { getBackendSrv, getTemplateSrv, isFetchError, TemplateSrv, config } from '@grafana/runtime'; import { Alert, Divider, SecureSocksProxySettings } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx index 951afa88344..d13296dc1c5 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { AadCurrentUserCredentials, AzureCredentials, instanceOfAzureCredential } from '@grafana/azure-sdk'; import { SelectableValue } from '@grafana/data'; -import { ConfigSection } from '@grafana/experimental'; +import { ConfigSection } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { Select, Field, RadioButtonGroup, Alert, Stack } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AdvancedResourcePicker.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AdvancedResourcePicker.tsx index 26efe10eb20..640fdd1a6c7 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AdvancedResourcePicker.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AdvancedResourcePicker.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { Icon, Input, Tooltip, Label, Button, useStyles2 } from '@grafana/ui'; export interface ResourcePickerProps { diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx index b1b13313f7c..622d6b660c8 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { PanelData, TimeRange } from '@grafana/data'; -import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental'; +import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui'; import { getTemplateSrv } from '@grafana/runtime'; import { Alert, LinkButton, Text, TextLink } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx index b052df9448c..7ab12383dc5 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { Input, Label, InlineField, Button, useStyles2 } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx index b7b7c2f13d6..9a4e1975a74 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from 'react'; import { SelectableValue, DataFrame, PanelData, Labels } from '@grafana/data'; -import { EditorList, AccessoryButton } from '@grafana/experimental'; +import { EditorList, AccessoryButton } from '@grafana/plugin-ui'; import { Select, HorizontalGroup, MultiSelect } from '@grafana/ui'; import { AzureMetricDimension, AzureMonitorOption, AzureMonitorQuery, AzureQueryEditorFieldProps } from '../../types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx index 751392acf44..6048ebca21e 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -1,5 +1,5 @@ import { PanelData } from '@grafana/data'; -import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental'; +import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/plugin-ui'; import { multiResourceCompatibleTypes } from '../../azureMetadata'; import type Datasource from '../../datasource'; diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx index 737053e81db..7fddd79ce0c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorHeader, InlineSelect } from '@grafana/experimental'; +import { EditorHeader, InlineSelect } from '@grafana/plugin-ui'; import { selectors } from '../../e2e/selectors'; import { AzureMonitorQuery, AzureQueryType } from '../../types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx index 12f985dbb92..15d4f7793ee 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx @@ -5,7 +5,7 @@ import { lastValueFrom } from 'rxjs'; import { CoreApp, DataFrame, getDefaultTimeRange, SelectableValue, TimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { HorizontalGroup, Select, diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx index 9c8c84d1bc6..8628766f6ba 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx @@ -2,7 +2,7 @@ import { uniq } from 'lodash'; import { useEffect, useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorList } from '@grafana/experimental'; +import { EditorList } from '@grafana/plugin-ui'; import { Field } from '@grafana/ui'; import { AzureQueryEditorFieldProps, AzureTracesFilter } from '../../types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx index 7ed37f8fa21..b550ecf09a8 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { usePrevious } from 'react-use'; import { TimeRange } from '@grafana/data'; -import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental'; +import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui'; import { Input } from '@grafana/ui'; import Datasource from '../../datasource'; diff --git a/public/app/plugins/datasource/azuremonitor/components/shared/Field.tsx b/public/app/plugins/datasource/azuremonitor/components/shared/Field.tsx index 2657c066115..3c5a9cf1123 100644 --- a/public/app/plugins/datasource/azuremonitor/components/shared/Field.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/shared/Field.tsx @@ -1,4 +1,4 @@ -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { InlineField } from '@grafana/ui'; import { Props as InlineFieldProps } from '@grafana/ui/src/components/Forms/InlineField'; diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 69ca46d32f5..38ed9e50920 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", - "@grafana/experimental": "2.1.6", + "@grafana/plugin-ui": "0.9.6", "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.tsx index 8d9c874bb38..2d5089f06fc 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import { getAggregationOptionsByMetric } from '../functions'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AliasBy.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AliasBy.tsx index a275b89e15f..6b92a061e66 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/AliasBy.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/AliasBy.tsx @@ -2,7 +2,7 @@ import { debounce } from 'lodash'; import { useState } from 'react'; import * as React from 'react'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Input } from '@grafana/ui'; export interface Props { diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx index c9a313e48ed..1a3d21c3a9d 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField, EditorFieldGroup } from '@grafana/experimental'; +import { EditorField, EditorFieldGroup } from '@grafana/plugin-ui'; import { ALIGNMENT_PERIODS } from '../constants'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx index 35f524cc556..6925e724d68 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { useDebounce } from 'react-use'; import { QueryEditorProps, getDefaultTimeRange, toOption } from '@grafana/data'; -import { EditorField, EditorRows } from '@grafana/experimental'; +import { EditorField, EditorRows } from '@grafana/plugin-ui'; import { Input } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx index 02943e876ef..9889693efae 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx @@ -1,8 +1,8 @@ import { PureComponent } from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; -import { ConfigSection, DataSourceDescription } from '@grafana/experimental'; import { ConnectionConfig } from '@grafana/google-sdk'; +import { ConfigSection, DataSourceDescription } from '@grafana/plugin-ui'; import { reportInteraction, config } from '@grafana/runtime'; import { Divider, SecureSocksProxySettings } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx index 13dd8e3d201..9e6c7aed0c3 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx @@ -1,5 +1,5 @@ import { SelectableValue } from '@grafana/data'; -import { EditorField, EditorRow } from '@grafana/experimental'; +import { EditorField, EditorRow } from '@grafana/plugin-ui'; import { HorizontalGroup, Switch } from '@grafana/ui'; import { GRAPH_PERIODS } from '../constants'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx index d9fd0e4c70c..356a81d5ade 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField, EditorFieldGroup } from '@grafana/experimental'; +import { EditorField, EditorFieldGroup } from '@grafana/plugin-ui'; import { MultiSelect } from '@grafana/ui'; import { SYSTEM_LABELS } from '../constants'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx index 102bc22ef2f..dca57374c91 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { SelectableValue, toOption } from '@grafana/data'; -import { AccessoryButton, EditorField, EditorList, EditorRow } from '@grafana/experimental'; +import { AccessoryButton, EditorField, EditorList, EditorRow } from '@grafana/plugin-ui'; import { HorizontalGroup, Select } from '@grafana/ui'; import { labelsToGroupedOptions, stringArrayToFilters } from '../functions'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/LookbackPeriodSelect.tsx b/public/app/plugins/datasource/cloud-monitoring/components/LookbackPeriodSelect.tsx index 0e7d991327a..92cce156c87 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/LookbackPeriodSelect.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/LookbackPeriodSelect.tsx @@ -1,5 +1,5 @@ import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import { LOOKBACK_PERIODS } from '../constants'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx index 8a245c46503..7cd44f61ffb 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect } from 'react'; import * as React from 'react'; import { SelectableValue, TimeRange } from '@grafana/data'; -import { EditorRows, Stack } from '@grafana/experimental'; +import { EditorRows } from '@grafana/plugin-ui'; +import { Stack } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; import { AlignmentTypes, CloudMonitoringQuery, QueryType, TimeSeriesList, TimeSeriesQuery } from '../types/query'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx index c7f6c66d355..3ca7d101942 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { RadioButtonGroup } from '@grafana/ui'; import { getAlignmentPickerData } from '../functions'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx index de42fbdd788..289b4745ba0 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Project.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/PromQLEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/PromQLEditor.tsx index 8e54fbe372c..deaa5e7eaf8 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/PromQLEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/PromQLEditor.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField, EditorRow } from '@grafana/experimental'; +import { EditorField, EditorRow } from '@grafana/plugin-ui'; import { TextArea, Input } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx index 9464e273ea2..7b701df34da 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx @@ -3,7 +3,7 @@ import { isEqual } from 'lodash'; import { useEffect, useState } from 'react'; import { QueryEditorProps, getDefaultTimeRange, toOption } from '@grafana/data'; -import { EditorRows } from '@grafana/experimental'; +import { EditorRows } from '@grafana/plugin-ui'; import { ConfirmModal } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx index 150bb3c1f1f..71431f0fc20 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx @@ -1,4 +1,4 @@ -import { EditorHeader, FlexItem, InlineSelect } from '@grafana/experimental'; +import { EditorHeader, FlexItem, InlineSelect } from '@grafana/plugin-ui'; import { QUERY_TYPES } from '../constants'; import { CloudMonitoringQuery } from '../types/query'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/SLO.tsx b/public/app/plugins/datasource/cloud-monitoring/components/SLO.tsx index 2e0117cfe70..be4606155d7 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/SLO.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/SLO.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx index 0d62b1cff1e..1a4ec805596 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import * as React from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; +import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/plugin-ui'; import { ALIGNMENT_PERIODS, SLO_BURN_RATE_SELECTOR_NAME } from '../constants'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Selector.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Selector.tsx index 690e03219b6..ea0e5903af5 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Selector.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Selector.tsx @@ -1,5 +1,5 @@ import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import { SELECTORS } from '../constants'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Service.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Service.tsx index 35093b92a7b..9425437d7b3 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Service.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Service.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { SelectableValue } from '@grafana/data'; -import { EditorField } from '@grafana/experimental'; +import { EditorField } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx index d50b6011e8e..56824f89358 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx @@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from 'react'; import * as React from 'react'; import { GrafanaTheme2, SelectableValue, TimeRange } from '@grafana/data'; -import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; +import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/plugin-ui'; import { reportInteraction } from '@grafana/runtime'; import { getSelectStyles, Select, AsyncSelect, useStyles2, useTheme2 } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 8aee980ffa1..32a61eb300d 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -6,8 +6,8 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", - "@grafana/experimental": "2.1.6", "@grafana/google-sdk": "0.1.2", + "@grafana/plugin-ui": "0.9.6", "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx index 458f2eb9176..153ec2aa039 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx @@ -1,4 +1,4 @@ -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { DEFAULT_POLICY } from '../../../../../types'; import { toSelectableValue } from '../utils/toSelectableValue'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx index 3bb380ee126..14f188bd683 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx @@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css'; import { Fragment, useMemo } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { useTheme2 } from '@grafana/ui'; import { toSelectableValue } from '../utils/toSelectableValue'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx index 9079bc056c7..3edc8320144 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx @@ -1,5 +1,5 @@ import { SelectableValue } from '@grafana/data'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { InfluxQueryTag } from '../../../../../types'; import { adjustOperatorIfNeeded, getCondition, getOperator } from '../utils/tagUtils'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx index 7d6b19d637f..910bb67fca5 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx @@ -38,13 +38,12 @@ jest.mock('../../../../../influxql_metadata_query', () => { }; }); -jest.mock('@grafana/runtime', () => { - return { - getTemplateSrv: jest.fn().mockReturnValueOnce({ - getVariables: jest.fn().mockReturnValueOnce([]), - }), - }; -}); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getTemplateSrv: jest.fn().mockReturnValueOnce({ + getVariables: jest.fn().mockReturnValueOnce([]), + }), +})); beforeEach(() => { (mockedMeta.getTagKeys as jest.Mock).mockClear(); diff --git a/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts b/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts index ce40ff3c53a..da0a3ed5f25 100644 --- a/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts +++ b/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import { DataSourceInstanceSettings, TimeRange } from '@grafana/data'; -import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental'; +import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/plugin-ui'; import { TemplateSrv, config, getTemplateSrv } from '@grafana/runtime'; import { COMMON_FNS, DB, FuncParameter, SQLQuery, SqlDatasource, formatSQL } from '@grafana/sql'; diff --git a/public/app/plugins/datasource/influxdb/fsql/sqlCompletionProvider.ts b/public/app/plugins/datasource/influxdb/fsql/sqlCompletionProvider.ts index b44b2475027..19fca7d6f56 100644 --- a/public/app/plugins/datasource/influxdb/fsql/sqlCompletionProvider.ts +++ b/public/app/plugins/datasource/influxdb/fsql/sqlCompletionProvider.ts @@ -11,7 +11,7 @@ import { TableDefinition, TableIdentifier, TokenType, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; interface CompletionProviderGetterArgs { getMeta: (t?: TableIdentifier) => Promise; diff --git a/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx index 607dc32f114..287257b4eaf 100644 --- a/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx +++ b/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx @@ -10,7 +10,7 @@ import { updateDatasourcePluginJsonDataOption, updateDatasourcePluginResetOption, } from '@grafana/data'; -import { ConfigSection, ConfigSubSection, DataSourceDescription } from '@grafana/experimental'; +import { ConfigSection, ConfigSubSection, DataSourceDescription } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { ConnectionLimits, useMigrateDatabaseFields } from '@grafana/sql'; import { NumberInput } from '@grafana/sql/src/components/configuration/NumberInput'; diff --git a/public/app/plugins/datasource/mssql/configuration/Kerberos.tsx b/public/app/plugins/datasource/mssql/configuration/Kerberos.tsx index f5dfc8759c3..d1166ba79e0 100644 --- a/public/app/plugins/datasource/mssql/configuration/Kerberos.tsx +++ b/public/app/plugins/datasource/mssql/configuration/Kerberos.tsx @@ -1,7 +1,7 @@ import { SyntheticEvent } from 'react'; import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data'; -import { ConfigSubSection } from '@grafana/experimental'; +import { ConfigSubSection } from '@grafana/plugin-ui'; import { FieldSet, Input, Field } from '@grafana/ui'; import { MSSQLAuthenticationType, MssqlOptions } from '../types'; diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index abe0ee75b13..b020d051975 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; -import { LanguageDefinition } from '@grafana/experimental'; +import { LanguageDefinition } from '@grafana/plugin-ui'; import { TemplateSrv, config } from '@grafana/runtime'; import { COMMON_FNS, diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index 1c422fdda68..7a54d042db0 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", - "@grafana/experimental": "2.1.6", + "@grafana/plugin-ui": "0.9.6", "@grafana/runtime": "11.5.0-pre", "@grafana/sql": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/mssql/sqlCompletionProvider.ts b/public/app/plugins/datasource/mssql/sqlCompletionProvider.ts index 871c2172dfc..72cc15f421d 100644 --- a/public/app/plugins/datasource/mssql/sqlCompletionProvider.ts +++ b/public/app/plugins/datasource/mssql/sqlCompletionProvider.ts @@ -6,7 +6,7 @@ import { TableDefinition, TableIdentifier, TokenType, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; import { DB, SQLQuery } from '@grafana/sql'; interface CompletionProviderGetterArgs { diff --git a/yarn.lock b/yarn.lock index 7b812284201..b6e5c37717e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2706,8 +2706,8 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/experimental": "npm:2.1.6" "@grafana/plugin-configs": "npm:11.5.0-pre" + "@grafana/plugin-ui": "npm:0.9.6" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/schema": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" @@ -2903,8 +2903,8 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/experimental": "npm:2.1.6" "@grafana/plugin-configs": "npm:11.5.0-pre" + "@grafana/plugin-ui": "npm:0.9.6" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/sql": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" @@ -2997,9 +2997,9 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/experimental": "npm:2.1.6" "@grafana/google-sdk": "npm:0.1.2" "@grafana/plugin-configs": "npm:11.5.0-pre" + "@grafana/plugin-ui": "npm:0.9.6" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/schema": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" From 1cc9f8f0c217ec59fc11472213a17c0199bfbaae Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:46:25 +0000 Subject: [PATCH 139/894] Release: update changelog for 11.2.6 (#99645) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5ca2310ca3..3b736827d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ + + +# 11.2.6 (2025-01-28) + +### Features and enhancements + +- **Azure Monitor:** Add a feature flag to toggle user auth for Azure Monitor only [#97565](https://github.com/grafana/grafana/pull/97565), [@adamyeats](https://github.com/adamyeats) +- **Security:** Update to Go 1.22.11 - Backport to v11.2.x [#99125](https://github.com/grafana/grafana/pull/99125), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.22.11 - Backport to v11.2.x (Enterprise) + +### Bug fixes + +- **Azure/GCM:** Improve error display [#97591](https://github.com/grafana/grafana/pull/97591), [@aangelisc](https://github.com/aangelisc) + + # 11.1.11 (2025-01-28) From f8e7e9e0244750a1527d35820dbb33e2bd1297ae Mon Sep 17 00:00:00 2001 From: Fayzal Ghantiwala <114010985+fayzal-g@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:54:11 +0000 Subject: [PATCH 140/894] Alerting: Make pagination token empty if an invalid token is passed (#99644) Reset token to empty if invalid --- pkg/services/ngalert/api/api_prometheus.go | 5 +++++ pkg/services/ngalert/api/api_prometheus_test.go | 13 ++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index 22d09a91668..364a7027253 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -334,6 +334,11 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager maxGroups := getInt64WithDefault(opts.Query, "group_limit", -1) nextToken := opts.Query.Get("group_next_token") + if nextToken != "" { + if _, err := base64.URLEncoding.DecodeString(nextToken); err != nil { + nextToken = "" + } + } groupedRules := getGroupedRules(log, ruleList, ruleNamesSet, opts.Namespaces) rulesTotals := make(map[string]int64, len(groupedRules)) diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 120755d2252..8ac41c3606e 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -790,8 +790,8 @@ func TestRouteGetRuleStatuses(t *testing.T) { } }) - t.Run("bad token should return no results", func(t *testing.T) { - r, err := http.NewRequest("GET", "/api/v1/rules?group_limit=10&group_next_token=foobar", nil) + t.Run("bad token should return first group_limit results", func(t *testing.T) { + r, err := http.NewRequest("GET", "/api/v1/rules?group_limit=1&group_next_token=foobar", nil) require.NoError(t, err) c.Context = &web.Context{Req: r} @@ -801,7 +801,14 @@ func TestRouteGetRuleStatuses(t *testing.T) { result := &apimodels.RuleResponse{} require.NoError(t, json.Unmarshal(resp.Body(), result)) - require.Len(t, result.Data.RuleGroups, 0) + require.Len(t, result.Data.RuleGroups, 1) + require.Len(t, result.Data.Totals, 0) + require.NotEmpty(t, result.Data.NextToken) + + folder, err := api.store.GetNamespaceByUID(context.Background(), "namespace_0", orgID, user) + require.NoError(t, err) + require.Equal(t, folder.Fullpath, result.Data.RuleGroups[0].File) + require.Equal(t, "rule_group_0", result.Data.RuleGroups[0].Name) }) t.Run("should return nothing when using group_limit=0", func(t *testing.T) { From b1f39f4b8ba01cc5d96fccbe16e0f9bed1016c6c Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:06:25 +0000 Subject: [PATCH 141/894] Release: update changelog for 11.3.3 (#99647) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b736827d26..9ab5bca9c62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ + + +# 11.3.3 (2025-01-28) + +### Features and enhancements + +- **Azure Monitor:** Add a feature flag to toggle user auth for Azure Monitor only [#97576](https://github.com/grafana/grafana/pull/97576), [@adamyeats](https://github.com/adamyeats) +- **Security:** Update to Go 1.23.5 - Backport to v11.3.x [#99124](https://github.com/grafana/grafana/pull/99124), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.23.5 - Backport to v11.3.x (Enterprise) + +### Bug fixes + +- **Alerting:** AlertingQueryRunner should skip descendant nodes of invalid queries [#97829](https://github.com/grafana/grafana/pull/97829), [@gillesdemey](https://github.com/gillesdemey) +- **Azure/GCM:** Improve error display [#97593](https://github.com/grafana/grafana/pull/97593), [@aangelisc](https://github.com/aangelisc) +- **Dashboard:** Fixes issue with compatability of old DashboardModel.annotations [#97467](https://github.com/grafana/grafana/pull/97467), [@torkelo](https://github.com/torkelo) +- **Dashboards:** Fix issue where filtered panels would not react to variable changes [#98733](https://github.com/grafana/grafana/pull/98733), [@oscarkilhed](https://github.com/oscarkilhed) +- **Dashboards:** Fixes issue with panel header showing even when hide time override was enabled [#97389](https://github.com/grafana/grafana/pull/97389), [@torkelo](https://github.com/torkelo) +- **Dashboards:** Fixes week relative time ranges when weekStart was changed [#98268](https://github.com/grafana/grafana/pull/98268), [@torkelo](https://github.com/torkelo) +- **DateTimePicker:** Fixes issue with date picker showing invalid date [#97970](https://github.com/grafana/grafana/pull/97970), [@torkelo](https://github.com/torkelo) +- **Fix:** Add support for datasource variable queries [#98118](https://github.com/grafana/grafana/pull/98118), [@sunker](https://github.com/sunker) +- **InfluxDB:** Adhoc filters can use template vars as values [#98785](https://github.com/grafana/grafana/pull/98785), [@bossinc](https://github.com/bossinc) +- **Unified Storage:** Use tls preferred when grafana db using ssl [#97379](https://github.com/grafana/grafana/pull/97379), [@owensmallwood](https://github.com/owensmallwood) + +### Plugin development fixes & changes + +- **Grafana UI:** Re-add react-router-dom as a dependency [#98421](https://github.com/grafana/grafana/pull/98421), [@leventebalogh](https://github.com/leventebalogh) + + # 11.2.6 (2025-01-28) From af2c7a19d1da2960be78c0864f4ba95c4356631f Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:29:59 +0000 Subject: [PATCH 142/894] Release: update changelog for 11.4.1 (#99650) Update changelog Co-authored-by: github-actions[bot] --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ab5bca9c62..c1c7060ba7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ + + +# 11.4.1 (2025-01-28) + +### Features and enhancements + +- **Security:** Update to Go 1.23.5 - Backport to v11.4.x [#99123](https://github.com/grafana/grafana/pull/99123), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.23.5 - Backport to v11.4.x (Enterprise) + +### Bug fixes + +- **Alerting:** AlertingQueryRunner should skip descendant nodes of invalid queries [#97830](https://github.com/grafana/grafana/pull/97830), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Fix alert rules unpausing after moving rule to different folder [#97583](https://github.com/grafana/grafana/pull/97583), [@santihernandezc](https://github.com/santihernandezc) +- **Alerting:** Fix label escaping in rule export [#98649](https://github.com/grafana/grafana/pull/98649), [@moustafab](https://github.com/moustafab) +- **Alerting:** Fix slack image uploading to use new api [#98066](https://github.com/grafana/grafana/pull/98066), [@moustafab](https://github.com/moustafab) +- **Azure/GCM:** Improve error display [#97594](https://github.com/grafana/grafana/pull/97594), [@aangelisc](https://github.com/aangelisc) +- **Dashboards:** Fix issue where filtered panels would not react to variable changes [#98734](https://github.com/grafana/grafana/pull/98734), [@oscarkilhed](https://github.com/oscarkilhed) +- **Dashboards:** Fixes issue with panel header showing even when hide time override was enabled [#98747](https://github.com/grafana/grafana/pull/98747), [@torkelo](https://github.com/torkelo) +- **Dashboards:** Fixes week relative time ranges when weekStart was changed [#98269](https://github.com/grafana/grafana/pull/98269), [@torkelo](https://github.com/torkelo) +- **Dashboards:** Panel react for `timeFrom` and `timeShift` changes using variables [#98659](https://github.com/grafana/grafana/pull/98659), [@Sergej-Vlasov](https://github.com/Sergej-Vlasov) +- **DateTimePicker:** Fixes issue with date picker showing invalid date [#97971](https://github.com/grafana/grafana/pull/97971), [@torkelo](https://github.com/torkelo) +- **Fix:** Add support for datasource variable queries [#98119](https://github.com/grafana/grafana/pull/98119), [@sunker](https://github.com/sunker) +- **InfluxDB:** Adhoc filters can use template vars as values [#98786](https://github.com/grafana/grafana/pull/98786), [@bossinc](https://github.com/bossinc) +- **LibraryPanel:** Fallback to panel title if library panel title is not set [#99410](https://github.com/grafana/grafana/pull/99410), [@ivanortegaalba](https://github.com/ivanortegaalba) + +### Plugin development fixes & changes + +- **Grafana UI:** Re-add react-router-dom as a dependency [#98422](https://github.com/grafana/grafana/pull/98422), [@leventebalogh](https://github.com/leventebalogh) + + # 11.3.3 (2025-01-28) From 9d635edd0e173f732f3ca8e2e0923d79b66a0ce7 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 28 Jan 2025 13:36:59 +0000 Subject: [PATCH 143/894] MultiCombobox: Async options (#99469) * remove managed isOpen state, add hook to abstract away options/async functionality * split useOptions into new file * refactor stories revert combobox stories to what's in main. I screwed up that rebase * change onChange type, clean up what calls onChange, add debounce and useLatestAsyncCall * tests (mid trying to figure out the act stuff) * tests * debounce-promise doesn't work with rollup? * just some minor code clean up * fix type import --- .../components/Combobox/Combobox.story.tsx | 10 +- .../Combobox/MultiCombobox.internal.story.tsx | 97 ++++++-- .../Combobox/MultiCombobox.test.tsx | 156 ++++++++++++- .../src/components/Combobox/MultiCombobox.tsx | 211 +++++++++--------- .../src/components/Combobox/useOptions.ts | 82 +++++++ 5 files changed, 428 insertions(+), 128 deletions(-) create mode 100644 packages/grafana-ui/src/components/Combobox/useOptions.ts diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx index 9d3bc5fbfb8..5575d21df0e 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx @@ -162,15 +162,15 @@ export const AsyncOptionsWithLabels: Story = { return ( { - onChangeAction(val); - setArgs({ value: val }); + onChange={(value: ComboboxOption | null) => { + onChangeAction(value); + setArgs({ value }); }} /> @@ -205,7 +205,7 @@ export const AsyncOptionsWithOnlyValues: Story = { {...dynamicArgs} onChange={(value: ComboboxOption | null) => { onChangeAction(value); - setArgs({ value: value }); + setArgs({ value }); }} /> diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx index 5d27d2ac594..36c368291ee 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx @@ -2,8 +2,10 @@ import { action } from '@storybook/addon-actions'; import { useArgs, useEffect, useState } from '@storybook/preview-api'; import type { Meta, StoryFn, StoryObj } from '@storybook/react'; +import { Field } from '../Forms/Field'; + import { MultiCombobox } from './MultiCombobox'; -import { generateOptions } from './storyUtils'; +import { generateOptions, fakeSearchAPI } from './storyUtils'; import { ComboboxOption } from './types'; const meta: Meta = { @@ -11,6 +13,9 @@ const meta: Meta = { component: MultiCombobox, }; +const loadOptionsAction = action('options called'); +const onChangeAction = action('onChange called'); + const commonArgs = { options: [ { label: 'wasd - 1', value: 'option1' }, @@ -40,7 +45,7 @@ export const Basic: Story = { {...args} value={value} onChange={(val) => { - action('onChange')(val); + onChangeAction(val); setArgs({ value: val }); }} /> @@ -67,17 +72,14 @@ export const AutoSize: Story = { }; const ManyOptionsStory: StoryFn = ({ numberOfOptions = 1e4, ...args }) => { - const [value, setValue] = useState([]); + const [dynamicArgs, setArgs] = useArgs(); + const [options, setOptions] = useState([]); - const [isLoading, setIsLoading] = useState(true); useEffect(() => { - setTimeout(() => { - generateOptions(numberOfOptions).then((options) => { - setIsLoading(false); - setOptions(options); - setValue([options[5].value]); - }); + setTimeout(async () => { + const options = await generateOptions(numberOfOptions); + setOptions(options); }, 1000); }, [numberOfOptions]); @@ -85,12 +87,11 @@ const ManyOptionsStory: StoryFn = ({ numberOfOptions = 1e4, ... return ( { - setValue(opts || []); - action('onChange')(opts); + setArgs({ value: opts }); + onChangeAction(opts); }} /> ); @@ -104,3 +105,71 @@ export const ManyOptions: StoryObj = { }, render: ManyOptionsStory, }; + +function loadOptionsWithLabels(inputValue: string) { + loadOptionsAction(inputValue); + return fakeSearchAPI(`http://example.com/search?errorOnQuery=break&query=${inputValue}`); +} + +export const AsyncOptionsWithLabels: Story = { + name: 'Async - options returns labels', + args: { + options: loadOptionsWithLabels, + value: [{ label: 'Option 69', value: '69' }], + placeholder: 'Select an option', + }, + render: (args) => { + const [dynamicArgs, setArgs] = useArgs(); + + return ( + + { + onChangeAction(val); + setArgs({ value: val }); + }} + /> + + ); + }, +}; + +function loadOptionsOnlyValues(inputValue: string) { + loadOptionsAction(inputValue); + return fakeSearchAPI(`http://example.com/search?errorOnQuery=break&query=${inputValue}`).then((options) => + options.map((opt) => ({ value: opt.label! })) + ); +} + +export const AsyncOptionsWithOnlyValues: Story = { + name: 'Async - options returns only values', + args: { + options: loadOptionsOnlyValues, + value: [{ value: 'Option 69' }], + placeholder: 'Select an option', + }, + render: (args) => { + const [dynamicArgs, setArgs] = useArgs(); + + return ( + + { + onChangeAction(val); + setArgs({ value: val }); + }} + /> + + ); + }, +}; diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx index 70be3c4148d..66444644ca4 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx @@ -1,8 +1,9 @@ -import { render, screen } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import userEvent, { UserEvent } from '@testing-library/user-event'; import React from 'react'; import { MultiCombobox, MultiComboboxProps } from './MultiCombobox'; +import { ComboboxOption } from './types'; describe('MultiCombobox', () => { beforeAll(() => { @@ -91,15 +92,18 @@ describe('MultiCombobox', () => { await user.click(input); await user.click(await screen.findByRole('option', { name: 'A' })); - //Second option + // Second option await user.click(screen.getByRole('option', { name: 'C' })); - //Deselect + // Deselect await user.click(screen.getByRole('option', { name: 'A' })); - expect(onChange).toHaveBeenNthCalledWith(1, [first]); - expect(onChange).toHaveBeenNthCalledWith(2, [first, third]); - expect(onChange).toHaveBeenNthCalledWith(3, [third]); + expect(onChange).toHaveBeenNthCalledWith(1, [{ label: 'A', value: first }]); + expect(onChange).toHaveBeenNthCalledWith(2, [ + { label: 'A', value: first }, + { label: 'C', value: third }, + ]); + expect(onChange).toHaveBeenNthCalledWith(3, [{ label: 'C', value: third }]); }); it('should be able to render a value that is not in the options', async () => { @@ -138,7 +142,11 @@ describe('MultiCombobox', () => { await user.click(input); await user.click(await screen.findByText('All')); - expect(onChange).toHaveBeenCalledWith(['a', 'b', 'c']); + expect(onChange).toHaveBeenCalledWith([ + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + ]); }); it('should deselect all option', async () => { @@ -157,4 +165,138 @@ describe('MultiCombobox', () => { expect(onChange).toHaveBeenCalledWith([]); }); }); + + describe('async', () => { + const onChangeHandler = jest.fn(); + let user: ReturnType; + + beforeAll(() => { + user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + afterEach(() => { + onChangeHandler.mockReset(); + }); + + // Assume that most apis only return with the value + const simpleAsyncOptions = [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }]; + + it('should allow async options', async () => { + const asyncOptions = jest.fn(() => Promise.resolve(simpleAsyncOptions)); + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + // Debounce + await act(async () => jest.advanceTimersByTime(200)); + + expect(asyncOptions).toHaveBeenCalled(); + }); + + it('should allow async options and select value', async () => { + const asyncOptions = jest.fn(() => Promise.resolve(simpleAsyncOptions)); + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + const item = await screen.findByRole('option', { name: 'Option 3' }); + await user.click(item); + + expect(onChangeHandler).toHaveBeenCalledWith([simpleAsyncOptions[2]]); + }); + + it('should retain values not returned by the async function', async () => { + const asyncOptions = jest.fn(() => Promise.resolve(simpleAsyncOptions)); + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + const item = await screen.findByRole('option', { name: 'Option 3' }); + await user.click(item); + + expect(onChangeHandler).toHaveBeenCalledWith([{ value: 'Option 69' }, { value: 'Option 3' }]); + }); + + it('should ignore late responses', async () => { + const asyncOptions = jest.fn(async (searchTerm: string) => { + if (searchTerm === 'a') { + return promiseResolvesWith([{ value: 'first' }], 1500); + } else if (searchTerm === 'ab') { + return promiseResolvesWith([{ value: 'second' }], 500); + } else if (searchTerm === 'abc') { + return promiseResolvesWith([{ value: 'third' }], 800); + } + + return Promise.resolve([]); + }); + + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + await user.keyboard('a'); + act(() => jest.advanceTimersByTime(200)); // Skip debounce + + await user.keyboard('b'); + act(() => jest.advanceTimersByTime(200)); // Skip debounce + + await user.keyboard('c'); + act(() => jest.advanceTimersByTime(500)); // Resolve the second request, should be ignored + + expect(screen.queryByRole('option', { name: 'first' })).not.toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'second' })).not.toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'third' })).not.toBeInTheDocument(); + + jest.advanceTimersByTime(800); // Resolve the third request, should be shown + expect(screen.queryByRole('option', { name: 'first' })).not.toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'second' })).not.toBeInTheDocument(); + expect(await screen.findByRole('option', { name: 'third' })).toBeInTheDocument(); + + jest.advanceTimersByTime(1500); // Resolve the first request, should be ignored + expect(screen.queryByRole('option', { name: 'first' })).not.toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'second' })).not.toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'third' })).toBeInTheDocument(); + + jest.clearAllTimers(); + }); + + it('should debounce requests', async () => { + const asyncOptions = jest.fn(async () => { + return promiseResolvesWith([{ value: 'Option 3' }], 1); + }); + + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + await user.keyboard('a'); + act(() => jest.advanceTimersByTime(10)); + + await user.keyboard('b'); + act(() => jest.advanceTimersByTime(10)); + + await user.keyboard('c'); + act(() => jest.advanceTimersByTime(200)); + + const item = await screen.findByRole('option', { name: 'Option 3' }); + expect(item).toBeInTheDocument(); + + expect(asyncOptions).toHaveBeenCalledTimes(1); + expect(asyncOptions).toHaveBeenCalledWith('abc'); + }); + }); }); + +function promiseResolvesWith(value: ComboboxOption[], timeout = 0) { + return new Promise((resolve) => setTimeout(() => resolve(value), timeout)); +} diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 4b819ab4a28..104f638de60 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -18,46 +18,25 @@ import { NotFoundError } from './MessageRows'; import { OptionListItem } from './OptionListItem'; import { SuffixIcon } from './SuffixIcon'; import { ValuePill } from './ValuePill'; -import { itemFilter, itemToString } from './filter'; +import { itemToString } from './filter'; import { getComboboxStyles, MENU_OPTION_HEIGHT, MENU_OPTION_HEIGHT_DESCRIPTION } from './getComboboxStyles'; import { getMultiComboboxStyles } from './getMultiComboboxStyles'; import { ALL_OPTION_VALUE, ComboboxOption } from './types'; import { useComboboxFloat } from './useComboboxFloat'; import { MAX_SHOWN_ITEMS, useMeasureMulti } from './useMeasureMulti'; import { useMultiInputAutoSize } from './useMultiInputAutoSize'; +import { useOptions } from './useOptions'; interface MultiComboboxBaseProps extends Omit, 'value' | 'onChange'> { value?: T[] | Array>; - onChange: (items?: T[]) => void; + onChange: (option: Array>) => void; enableAllOption?: boolean; } export type MultiComboboxProps = MultiComboboxBaseProps & AutoSizeConditionals; export const MultiCombobox = (props: MultiComboboxProps) => { - const { - options, - placeholder, - onChange, - value, - width, - enableAllOption, - invalid, - loading, - disabled, - minWidth, - maxWidth, - } = props; - const isAsync = typeof options === 'function'; - - const selectedItems = useMemo(() => { - if (!value || isAsync) { - //TODO handle async - return []; - } - - return getSelectedItemsFromValue(value, options); - }, [value, options, isAsync]); + const { placeholder, onChange, value, width, enableAllOption, invalid, disabled, minWidth, maxWidth } = props; const styles = useStyles2(getComboboxStyles); const [inputValue, setInputValue] = useState(''); @@ -73,19 +52,22 @@ export const MultiCombobox = (props: MultiComboboxPro } as ComboboxOption; }, [inputValue]); - const baseItems = useMemo(() => { - return isAsync ? [] : enableAllOption ? [allOptionItem, ...options] : options; - }, [options, enableAllOption, allOptionItem, isAsync]); + // Handle async options and the 'All' option + const { options: baseOptions, updateOptions, asyncLoading } = useOptions(props.options); + const options = useMemo(() => { + // Only add the 'All' option if there's more than 1 option + const addAllOption = enableAllOption && baseOptions.length > 1; + return addAllOption ? [allOptionItem, ...baseOptions] : baseOptions; + }, [baseOptions, enableAllOption, allOptionItem]); + const loading = props.loading || asyncLoading; - const items = useMemo(() => { - const newItems = baseItems.filter(itemFilter(inputValue)); - - if (enableAllOption && newItems.length === 1 && newItems[0] === allOptionItem) { + const selectedItems = useMemo(() => { + if (!value) { return []; } - return newItems; - }, [baseItems, inputValue, enableAllOption, allOptionItem]); + return getSelectedItemsFromValue(value, baseOptions); + }, [value, baseOptions]); const { measureRef, counterMeasureRef, suffixMeasureRef, shownItems } = useMeasureMulti( selectedItems, @@ -98,48 +80,50 @@ export const MultiCombobox = (props: MultiComboboxPro [selectedItems] ); - const { getSelectedItemProps, getDropdownProps, removeSelectedItem } = useMultipleSelection({ - selectedItems, //initally selected items, - onStateChange: ({ type, selectedItems: newSelectedItems }) => { - switch (type) { - case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace: - case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete: - case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace: - case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem: - if (newSelectedItems) { - onChange(getComboboxOptionsValues(newSelectedItems)); - } - break; + const { getSelectedItemProps, getDropdownProps, setSelectedItems, addSelectedItem, removeSelectedItem } = + useMultipleSelection({ + selectedItems, // initally selected items, + onStateChange: ({ type, selectedItems: newSelectedItems }) => { + switch (type) { + case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace: + case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete: + case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace: + case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem: + case useMultipleSelection.stateChangeTypes.FunctionAddSelectedItem: + case useMultipleSelection.stateChangeTypes.FunctionSetSelectedItems: + // Unclear why newSelectedItems would be undefined, but this seems logical + onChange(newSelectedItems ?? []); + break; - default: - break; - } - }, - stateReducer: (state, actionAndChanges) => { - const { changes } = actionAndChanges; - return { - ...changes, + default: + break; + } + }, + stateReducer: (state, actionAndChanges) => { + const { changes } = actionAndChanges; + return { + ...changes, - /** - * TODO: Fix Hack! - * This prevents the menu from closing when the user unselects an item in the dropdown at the expense - * of breaking keyboard navigation. - * - * Downshift isn't really designed to keep selected items in the dropdown menu, so when you unselect an item - * in a multiselect, the stateReducer tries to move focus onto another item which causes the menu to be closed. - * This only seems to happen when you deselect the last item in the selectedItems list. - * - * Check out: - * - FunctionRemoveSelectedItem in the useMultipleSelection reducer https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/reducer.js#L75 - * - The activeIndex useEffect in useMultipleSelection https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/index.js#L68-L72 - * - * Forcing the activeIndex to -999 both prevents the useEffect that changes the focus from triggering (value never changes) - * and prevents the if statement in useMultipleSelection from focusing anything. - */ - activeIndex: -999, - }; - }, - }); + /** + * TODO: Fix Hack! + * This prevents the menu from closing when the user unselects an item in the dropdown at the expense + * of breaking keyboard navigation. + * + * Downshift isn't really designed to keep selected items in the dropdown menu, so when you unselect an item + * in a multiselect, the stateReducer tries to move focus onto another item which causes the menu to be closed. + * This only seems to happen when you deselect the last item in the selectedItems list. + * + * Check out: + * - FunctionRemoveSelectedItem in the useMultipleSelection reducer https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/reducer.js#L75 + * - The activeIndex useEffect in useMultipleSelection https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/index.js#L68-L72 + * + * Forcing the activeIndex to -999 both prevents the useEffect that changes the focus from triggering (value never changes) + * and prevents the if statement in useMultipleSelection from focusing anything. + */ + activeIndex: -999, + }; + }, + }); const { getToggleButtonProps, @@ -150,12 +134,25 @@ export const MultiCombobox = (props: MultiComboboxPro getInputProps, getItemProps, } = useCombobox({ - items, + items: options, itemToString, inputValue, selectedItem: null, stateReducer: (state, actionAndChanges) => { - const { changes, type } = actionAndChanges; + const { type } = actionAndChanges; + let { changes } = actionAndChanges; + const menuBeingOpened = state.isOpen === false && changes.isOpen === true; + + // Reset the input value when the menu is opened. If the menu is opened due to an input change + // then make sure we keep that. + // This will trigger onInputValueChange to load async options + if (menuBeingOpened && changes.inputValue === state.inputValue) { + changes = { + ...changes, + inputValue: '', + }; + } + switch (type) { case useCombobox.stateChangeTypes.InputKeyDownEnter: case useCombobox.stateChangeTypes.ItemClick: @@ -171,39 +168,50 @@ export const MultiCombobox = (props: MultiComboboxPro } }, + onIsOpenChange: ({ isOpen, inputValue }) => { + if (isOpen && inputValue === '') { + updateOptions(inputValue); + } + }, + onStateChange: ({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) => { switch (type) { case useCombobox.stateChangeTypes.InputKeyDownEnter: case useCombobox.stateChangeTypes.ItemClick: // Handle All functionality if (newSelectedItem?.value === ALL_OPTION_VALUE) { - const allFilteredSelected = selectedItems.length === items.length - 1; - let newSelectedItems = allFilteredSelected && inputValue === '' ? [] : baseItems.slice(1); + // TODO: fix bug where if the search filtered items list is the + // same length, but different, than the selected items (ask tobias) + const isAllFilteredSelected = selectedItems.length === options.length - 1; - if (!allFilteredSelected && inputValue !== '') { + // if every option is already selected, clear the selection. + // otherwise, select all the options (excluding the first ALL_OTION) + const realOptions = options.slice(1); + let newSelectedItems = isAllFilteredSelected && inputValue === '' ? [] : realOptions; + + if (!isAllFilteredSelected && inputValue !== '') { // Select all currently filtered items and deduplicate - newSelectedItems = [...new Set([...selectedItems, ...items.slice(1)])]; + newSelectedItems = [...new Set([...selectedItems, ...realOptions])]; } - if (allFilteredSelected && inputValue !== '') { + if (isAllFilteredSelected && inputValue !== '') { // Deselect all currently filtered items - const filteredSet = new Set(items.slice(1).map((item) => item.value)); + const filteredSet = new Set(realOptions.map((item) => item.value)); newSelectedItems = selectedItems.filter((item) => !filteredSet.has(item.value)); } - onChange(getComboboxOptionsValues(newSelectedItems)); - break; - } - if (newSelectedItem) { - if (!isOptionSelected(newSelectedItem)) { - onChange(getComboboxOptionsValues([...selectedItems, newSelectedItem])); - break; - } - removeSelectedItem(newSelectedItem); // onChange is handled by multiselect here + setSelectedItems(newSelectedItems); + } else if (newSelectedItem && isOptionSelected(newSelectedItem)) { + removeSelectedItem(newSelectedItem); + } else if (newSelectedItem) { + addSelectedItem(newSelectedItem); } + break; case useCombobox.stateChangeTypes.InputChange: setInputValue(newInputValue ?? ''); + updateOptions(newInputValue ?? ''); + break; default: break; @@ -211,14 +219,14 @@ export const MultiCombobox = (props: MultiComboboxPro }, }); - const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, isOpen); + const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(options, isOpen); const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled, width, minWidth, maxWidth); const virtualizerOptions = { - count: items.length, + count: options.length, getScrollElement: () => scrollRef.current, estimateSize: (index: number) => - 'description' in items[index] ? MENU_OPTION_HEIGHT_DESCRIPTION : MENU_OPTION_HEIGHT, + 'description' in options[index] ? MENU_OPTION_HEIGHT_DESCRIPTION : MENU_OPTION_HEIGHT, overscan: VIRTUAL_OVERSCAN_ITEMS, }; @@ -291,13 +299,16 @@ export const MultiCombobox = (props: MultiComboboxPro
    {rowVirtualizer.getVirtualItems().map((virtualRow) => { const index = virtualRow.index; - const item = items[index]; + const item = options[index]; const itemProps = getItemProps({ item, index }); const isSelected = isOptionSelected(item); const id = 'multicombobox-option-' + item.value.toString(); const isAll = item.value === ALL_OPTION_VALUE; + + // TODO: fix bug where if the search filtered items list is the + // same length, but different, than the selected items (ask tobias) const allItemsSelected = - items[0]?.value === ALL_OPTION_VALUE && selectedItems.length === items.length - 1; + options[0]?.value === ALL_OPTION_VALUE && selectedItems.length === options.length - 1; return (
  • (props: MultiComboboxPro label={ isAll ? (item.label ?? item.value.toString()) + - (isAll && inputValue !== '' ? ` (${items.length - 1})` : '') + (isAll && inputValue !== '' ? ` (${options.length - 1})` : '') : (item.label ?? item.value.toString()) } description={item?.description} @@ -332,7 +343,7 @@ export const MultiCombobox = (props: MultiComboboxPro ); })}
-
{items.length === 0 && }
+
{options.length === 0 && }
)}
@@ -375,7 +386,3 @@ function isComboboxOptions( ): value is Array> { return typeof value[0] === 'object'; } - -function getComboboxOptionsValues(optionArray: Array>) { - return optionArray.map((option) => option.value); -} diff --git a/packages/grafana-ui/src/components/Combobox/useOptions.ts b/packages/grafana-ui/src/components/Combobox/useOptions.ts new file mode 100644 index 00000000000..504f3584e1d --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/useOptions.ts @@ -0,0 +1,82 @@ +import { debounce } from 'lodash'; +import { useState, useCallback, useMemo } from 'react'; + +import { itemFilter } from './filter'; +import { ComboboxOption } from './types'; +import { StaleResultError, useLatestAsyncCall } from './useLatestAsyncCall'; + +type AsyncOptions = + | Array> + | ((inputValue: string) => Promise>>); + +const asyncNoop = () => Promise.resolve([]); + +/** + * Abstracts away sync/async options for MultiCombobox (and later Combobox). + * It also filters options based on the user's input. + * + * Returns: + * - options either filtered by user's input, or from async options fn + * - function to call when user types (to filter, or call async fn) + * - loading and error states + */ +export function useOptions(rawOptions: AsyncOptions) { + const isAsync = typeof rawOptions === 'function'; + + const loadOptions = useLatestAsyncCall(isAsync ? rawOptions : asyncNoop); + + const debouncedLoadOptions = useMemo( + () => + debounce((searchTerm: string) => { + return loadOptions(searchTerm) + .then((options) => { + setAsyncOptions(options); + setAsyncLoading(false); + setAsyncError(false); + }) + .catch((error) => { + if (!(error instanceof StaleResultError)) { + setAsyncError(true); + setAsyncLoading(false); + + if (error) { + console.error('Error loading async options for Combobox', error); + } + } + }); + }, 200), + [loadOptions] + ); + + const [asyncOptions, setAsyncOptions] = useState>>([]); + const [asyncLoading, setAsyncLoading] = useState(false); + const [asyncError, setAsyncError] = useState(false); + + // This hook keeps its own inputValue state (rather than accepting it as an arg) because it needs to be + // told it for async options loading anyway. + const [userTypedSearch, setUserTypedSearch] = useState(''); + + const updateOptions = useCallback( + (inputValue: string) => { + if (!isAsync) { + setUserTypedSearch(inputValue); + return; + } + + setAsyncLoading(true); + + debouncedLoadOptions(inputValue); + }, + [debouncedLoadOptions, isAsync] + ); + + const finalOptions = useMemo(() => { + if (isAsync) { + return asyncOptions; + } else { + return rawOptions.filter(itemFilter(userTypedSearch)); + } + }, [rawOptions, asyncOptions, isAsync, userTypedSearch]); + + return { options: finalOptions, updateOptions, asyncLoading, asyncError }; +} From 8b9d4d13586b55509e7f0b26daff2a1ede6e41c4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:37:10 +0000 Subject: [PATCH 144/894] Update dependency @types/node to v22.12.0 (#99638) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-icons/package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/mssql/package.json | 2 +- .../app/plugins/datasource/mysql/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 48 +++++++++---------- 21 files changed, 44 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index 54a32f06f62..92651a8dbd1 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "@types/lodash": "4.17.15", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4", "@types/pluralize": "^0.0.33", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 8ba4574e8df..ff3a9928802 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -65,7 +65,7 @@ "@rollup/plugin-node-resolve": "16.0.0", "@types/history": "4.7.11", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/papaparse": "5.3.15", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index f6da48b7630..5eaf8277ad1 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@rollup/plugin-node-resolve": "16.0.0", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/semver": "7.5.8", "esbuild": "0.24.2", "rimraf": "6.0.1", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 66de5e2225d..e48e6113c6b 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -68,7 +68,7 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index 5e4ea1556f8..a39c28777a5 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -45,7 +45,7 @@ "@svgr/plugin-prettier": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "@types/babel__core": "^7", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "esbuild": "0.24.2", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 4da97db322a..a668fa0280b 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -36,7 +36,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/systemjs": "6.15.1", "jest": "^29.6.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 740e9309832..3f3060e08cc 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -93,7 +93,7 @@ "@types/jest": "29.5.14", "@types/jquery": "3.5.32", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index a5093fc6aee..6bafb8a31ed 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -41,7 +41,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-virtualized-auto-sizer": "1.0.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index a8eb8f1be2b..aa162a98040 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -145,7 +145,7 @@ "@types/is-hotkey": "0.1.10", "@types/jest": "29.5.14", "@types/mock-raf": "1.0.6", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-color": "3.0.13", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 38ed9e50920..0f8b460420e 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -33,7 +33,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 32a61eb300d..b281d43e7a3 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -35,7 +35,7 @@ "@types/debounce-promise": "3.1.9", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index 4094ca3e08b..2f85083d376 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index bba725e85e2..ca1682f1758 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -27,7 +27,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index a1666e38d59..47acd088aeb 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -30,7 +30,7 @@ "@types/d3-random": "^3.0.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/uuid": "10.0.0", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index 5f1fd12a7ad..a882e2093de 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -31,7 +31,7 @@ "@types/jest": "29.5.14", "@types/lodash": "4.17.15", "@types/logfmt": "^1.2.3", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-window": "1.8.8", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index 7a54d042db0..008012c3d27 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index b1a791dd64b..2cbed20190a 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index becc966e421..cf4dcec1268 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -23,7 +23,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 9f6e20f1d5b..55a87c3c5f3 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -46,7 +46,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index f9f8a40298c..efddda4e217 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -26,7 +26,7 @@ "@testing-library/react": "16.1.0", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", - "@types/node": "22.10.10", + "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "ts-node": "10.9.2", diff --git a/yarn.lock b/yarn.lock index b6e5c37717e..2714fa57eea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2718,7 +2718,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2760,7 +2760,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2790,7 +2790,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2832,7 +2832,7 @@ __metadata: "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/uuid": "npm:10.0.0" @@ -2873,7 +2873,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" "@types/logfmt": "npm:^1.2.3" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/react-window": "npm:1.8.8" @@ -2913,7 +2913,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2944,7 +2944,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2972,7 +2972,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" lodash: "npm:4.17.21" @@ -3010,7 +3010,7 @@ __metadata: "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3063,7 +3063,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3113,7 +3113,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" lodash: "npm:4.17.21" @@ -3206,7 +3206,7 @@ __metadata: "@types/d3-interpolate": "npm:^3.0.0" "@types/history": "npm:4.7.11" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/papaparse": "npm:5.3.15" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3264,7 +3264,7 @@ __metadata: dependencies: "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:16.0.0" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/semver": "npm:7.5.8" esbuild: "npm:0.24.2" rimraf: "npm:6.0.1" @@ -3399,7 +3399,7 @@ __metadata: "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/tinycolor2": "npm:1.4.6" @@ -3507,7 +3507,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/systemjs": "npm:6.15.1" jest: "npm:^29.6.4" @@ -3628,7 +3628,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/jquery": "npm:3.5.32" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -3779,7 +3779,7 @@ __metadata: "@svgr/plugin-prettier": "npm:^8.1.0" "@svgr/plugin-svgo": "npm:^8.1.0" "@types/babel__core": "npm:^7" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" esbuild: "npm:0.24.2" @@ -3886,7 +3886,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.15" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/react-virtualized-auto-sizer": "npm:1.0.4" @@ -4050,7 +4050,7 @@ __metadata: "@types/jquery": "npm:3.5.32" "@types/lodash": "npm:4.17.15" "@types/mock-raf": "npm:1.0.6" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-color": "npm:3.0.13" @@ -9745,12 +9745,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:22.10.10, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4, @types/node@npm:^22.0.0": - version: 22.10.10 - resolution: "@types/node@npm:22.10.10" +"@types/node@npm:*, @types/node@npm:22.12.0, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4, @types/node@npm:^22.0.0": + version: 22.12.0 + resolution: "@types/node@npm:22.12.0" dependencies: undici-types: "npm:~6.20.0" - checksum: 10/61559b62bc7e62b947876e097c99472dd01317dd3b5916b538e2c40db8d68a1fc23c8de48149ef04cd2790a97bf2c005ea60cbb067e55ddbefd013d73da7a147 + checksum: 10/aac2b6f6a845ec3540c3d979b3150efe3162165bfda953af10b579df2d1cc4f5c48506922bf6bf661a2e5a7ebb571c5729bf1f9f12488a810bb1a5fa9522ef9d languageName: node linkType: hard @@ -17822,7 +17822,7 @@ __metadata: "@types/lodash": "npm:4.17.15" "@types/logfmt": "npm:^1.2.3" "@types/lucene": "npm:^2" - "@types/node": "npm:22.10.10" + "@types/node": "npm:22.12.0" "@types/node-forge": "npm:^1" "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4" "@types/pluralize": "npm:^0.0.33" From 0bf31c14a7e295af089ecc682d7eedb434349d41 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Tue, 28 Jan 2025 14:49:47 +0100 Subject: [PATCH 145/894] Alerting: Improve default form values handling (#97564) This PR refactors how rule form state is managed and relies less on prop drilling and removes dependency on redux store. --- .betterer.results | 41 ++- public/app/features/alerting/routes.tsx | 4 +- .../alerting/unified/api/alertRuleApi.ts | 1 + .../components/export/GrafanaModifyExport.tsx | 2 +- .../ExpressionStatusIndicator.test.tsx | 2 +- .../rule-editor/AnnotationsStep.test.tsx | 2 +- .../rule-editor/GrafanaEvaluationBehavior.tsx | 180 ++++----- .../alert-rule-form/AlertRuleForm.tsx | 155 ++------ .../alert-rule-form/ModifyExportRuleForm.tsx | 17 +- .../getPayloadToExport.test.ts | 2 +- .../SimplifiedRuleEditor.test.tsx | 10 +- .../rule-editor/labels/LabelsField.test.tsx | 1 + .../QueryAndExpressionsStep.tsx | 63 +--- ...riesTransformableToSimpleCondition.test.ts | 3 +- .../useAdvancedMode.ts | 5 +- .../unified/components/rule-editor/util.ts | 14 - .../rule-viewer/RuleViewer.test.tsx | 12 +- .../rules/EditRuleGroupModal.test.tsx | 206 ++++++----- .../components/rules/EditRuleGroupModal.tsx | 346 ++++++++++-------- .../components/rules/RulesGroup.test.tsx | 52 +-- .../unified/components/rules/RulesGroup.tsx | 24 +- .../unified/components/rules/RulesTable.tsx | 17 +- .../hooks/ruleGroup/useProduceNewRuleGroup.ts | 2 +- .../alerting/unified/hooks/useHasRuler.ts | 7 +- .../hooks/useUnifiedAlertingSelector.ts | 3 + public/app/features/alerting/unified/mocks.ts | 9 +- .../unified/mocks/server/configure.ts | 9 +- .../alerting/unified/mocks/server/db.ts | 62 +++- .../__snapshots__/ruleGroups.test.ts.snap | 4 - .../CloneRuleEditor.test.tsx | 32 +- .../{ => rule-editor}/CloneRuleEditor.tsx | 23 +- .../{ => rule-editor}/ExistingRuleEditor.tsx | 15 +- .../unified/{ => rule-editor}/RuleEditor.tsx | 55 ++- .../RuleEditorCloudOnlyAllowed.test.tsx | 26 +- .../RuleEditorCloudRules.test.tsx | 16 +- .../RuleEditorExisting.test.tsx | 12 +- .../RuleEditorGrafanaRules.test.tsx | 8 +- .../RuleEditorRecordingRule.test.tsx | 14 +- .../RuleEditorCloudRules.test.tsx.snap | 0 .../RuleEditorGrafanaRules.test.tsx.snap | 0 .../RuleEditorRecordingRule.test.tsx.snap | 0 .../unified/rule-editor/formDefaults.test.ts | 196 ++++++++++ .../unified/rule-editor/formDefaults.ts | 157 ++++++++ .../unified/rule-editor/formProcessing.ts | 150 ++++++++ .../unified/rule-list/FilterView.test.tsx | 6 +- .../unified/rule-list/GroupedView.test.tsx | 6 +- .../alerting/unified/test/test-utils.ts | 14 + .../alerting/unified/utils/rule-form.test.ts | 35 +- .../alerting/unified/utils/rule-form.ts | 116 +----- public/test/helpers/alertingRuleEditor.tsx | 3 +- 50 files changed, 1203 insertions(+), 936 deletions(-) rename public/app/features/alerting/unified/{ => rule-editor}/CloneRuleEditor.test.tsx (93%) rename public/app/features/alerting/unified/{ => rule-editor}/CloneRuleEditor.tsx (72%) rename public/app/features/alerting/unified/{ => rule-editor}/ExistingRuleEditor.tsx (71%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditor.tsx (69%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditorCloudOnlyAllowed.test.tsx (88%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditorCloudRules.test.tsx (85%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditorExisting.test.tsx (95%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditorGrafanaRules.test.tsx (94%) rename public/app/features/alerting/unified/{ => rule-editor}/RuleEditorRecordingRule.test.tsx (88%) rename public/app/features/alerting/unified/{ => rule-editor}/__snapshots__/RuleEditorCloudRules.test.tsx.snap (100%) rename public/app/features/alerting/unified/{ => rule-editor}/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap (100%) rename public/app/features/alerting/unified/{ => rule-editor}/__snapshots__/RuleEditorRecordingRule.test.tsx.snap (100%) create mode 100644 public/app/features/alerting/unified/rule-editor/formDefaults.test.ts create mode 100644 public/app/features/alerting/unified/rule-editor/formDefaults.ts create mode 100644 public/app/features/alerting/unified/rule-editor/formProcessing.ts diff --git a/.betterer.results b/.betterer.results index 3d15c748b2e..c08d07d5459 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1555,19 +1555,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] ], - "public/app/features/alerting/unified/CloneRuleEditor.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"] - ], - "public/app/features/alerting/unified/ExistingRuleEditor.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. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] - ], "public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/datetime/rangeutil\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], @@ -1606,12 +1593,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/RuleEditor.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. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] - ], "public/app/features/alerting/unified/RuleViewer.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], @@ -2563,7 +2544,8 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "10"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "11"] ], "public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -2925,6 +2907,25 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/plugins/PluginOriginBadge.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/Icon/utils\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] ], + "public/app/features/alerting/unified/rule-editor/CloneRuleEditor.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"] + ], + "public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.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. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] + ], + "public/app/features/alerting/unified/rule-editor/RuleEditor.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. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] + ], "public/app/features/alerting/unified/rule-list/FilterView.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 839d5db06c4..490d264c5fa 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -214,7 +214,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { pageClass: 'page-alerting', roles: evaluateAccess([AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite]), component: importAlertingComponent( - () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') + () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/rule-editor/RuleEditor') ), }, { @@ -222,7 +222,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { pageClass: 'page-alerting', roles: evaluateAccess([AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleExternalWrite]), component: importAlertingComponent( - () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') + () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/rule-editor/RuleEditor') ), }, { diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts index f483df956e7..806b782a2bf 100644 --- a/public/app/features/alerting/unified/api/alertRuleApi.ts +++ b/public/app/features/alerting/unified/api/alertRuleApi.ts @@ -248,6 +248,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({ const { path, params } = rulerUrlBuilder(rulerConfig).namespace(namespace); return { url: path, params }; }, + providesTags: (_result, _error, { namespace }) => [{ type: 'RuleNamespace', id: namespace }], }), // TODO This should be probably a separate ruler API file diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx index 108532e5e41..5aca1d77824 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx @@ -7,8 +7,8 @@ import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { RuleIdentifier } from '../../../../../types/unified-alerting'; import { useRuleWithLocation } from '../../hooks/useCombinedRule'; +import { formValuesFromExistingRule } from '../../rule-editor/formDefaults'; import { stringifyErrorLike } from '../../utils/misc'; -import { formValuesFromExistingRule } from '../../utils/rule-form'; import * as ruleId from '../../utils/rule-id'; import { isGrafanaRulerRule } from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; diff --git a/public/app/features/alerting/unified/components/expressions/ExpressionStatusIndicator.test.tsx b/public/app/features/alerting/unified/components/expressions/ExpressionStatusIndicator.test.tsx index 3b76b141690..c82097c0df0 100644 --- a/public/app/features/alerting/unified/components/expressions/ExpressionStatusIndicator.test.tsx +++ b/public/app/features/alerting/unified/components/expressions/ExpressionStatusIndicator.test.tsx @@ -1,8 +1,8 @@ import { render, screen } from '@testing-library/react'; import { FormProvider, useForm } from 'react-hook-form'; +import { getDefaultFormValues } from '../../rule-editor/formDefaults'; import { RuleFormValues } from '../../types/rule-form'; -import { getDefaultFormValues } from '../../utils/rule-form'; import { ExpressionStatusIndicator } from './ExpressionStatusIndicator'; diff --git a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx index df1ee304e11..9f1067761ca 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx @@ -6,9 +6,9 @@ import { byRole, byTestId } from 'testing-library-selector'; import { DashboardSearchItemType } from '../../../../search/types'; import { mockDashboardApi, setupMswServer } from '../../mockApi'; import { mockDashboardDto, mockDashboardSearchItem } from '../../mocks'; +import { getDefaultFormValues } from '../../rule-editor/formDefaults'; import { RuleFormValues } from '../../types/rule-form'; import { Annotation } from '../../utils/constants'; -import { getDefaultFormValues } from '../../utils/rule-form'; import AnnotationsStep from './AnnotationsStep'; diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx index 3a918eb6369..6104c3b2fa6 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -1,12 +1,11 @@ import { css } from '@emotion/css'; -import { debounce, take, uniqueId } from 'lodash'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { uniqueId } from 'lodash'; +import { useEffect, useMemo, useState } from 'react'; import { Controller, FormProvider, RegisterOptions, useForm, useFormContext } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { - AsyncSelect, Box, Button, Field, @@ -15,6 +14,7 @@ import { Input, Label, Modal, + Select, Stack, Switch, Text, @@ -22,17 +22,13 @@ import { useStyles2, } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; -import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; import { RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; -import { LogMessages, logInfo } from '../../Analytics'; import { alertRuleApi } from '../../api/alertRuleApi'; import { GRAFANA_RULER_CONFIG } from '../../api/featureDiscoveryApi'; -import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces'; -import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; +import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../rule-editor/formDefaults'; import { RuleFormValues } from '../../types/rule-form'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; -import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form'; import { isGrafanaAlertingRuleByType, isGrafanaManagedRuleByType, @@ -53,48 +49,38 @@ import { RuleEditorSection } from './RuleEditorSection'; export const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds export const MAX_GROUP_RESULTS = 1000; -export const useFolderGroupOptions = (folderUid: string, enableProvisionedGroups: boolean) => { +const useFetchGroupsForFolder = (folderUid: string) => { // fetch the ruler rules from the database so we can figure out what other "groups" are already defined // for our folders - const { isLoading: isLoadingRulerNamespace, currentData: rulerNamespace } = - alertRuleApi.endpoints.rulerNamespace.useQuery( - { - namespace: folderUid, - rulerConfig: GRAFANA_RULER_CONFIG, - }, - { - skip: !folderUid, - refetchOnMountOrArgChange: true, - } - ); - - // There should be only one entry in the rulerNamespace object - // However it uses folder name as key, so to avoid fetching folder name, we use Object.values - const groupOptions = useMemo(() => { - if (!rulerNamespace) { - // still waiting for namespace information to be fetched - return []; + return alertRuleApi.endpoints.rulerNamespace.useQuery( + { + namespace: folderUid, + rulerConfig: GRAFANA_RULER_CONFIG, + }, + { + refetchOnMountOrArgChange: true, + skip: !folderUid, } + ); +}; - const folderGroups = Object.values(rulerNamespace).flat() ?? []; +const namespaceToGroupOptions = (rulerNamespace: RulerRulesConfigDTO, enableProvisionedGroups: boolean) => { + const folderGroups = Object.values(rulerNamespace).flat(); - return folderGroups - .map>((group) => { - const isProvisioned = isProvisionedGroup(group); - return { - label: group.name, - value: group.name, - description: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL, - // we include provisioned folders, but disable the option to select them - isDisabled: !enableProvisionedGroups ? isProvisioned : false, - isProvisioned: isProvisioned, - }; - }) + return folderGroups + .map>((group) => { + const isProvisioned = isProvisionedGroup(group); + return { + label: group.name, + value: group.name, + description: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL, + // we include provisioned folders, but disable the option to select them + isDisabled: !enableProvisionedGroups ? isProvisioned : false, + isProvisioned: isProvisioned, + }; + }) - .sort(sortByLabel); - }, [rulerNamespace, enableProvisionedGroups]); - - return { groupOptions, loading: isLoadingRulerNamespace }; + .sort(sortByLabel); }; const isProvisionedGroup = (group: RulerRuleGroupDTO) => { @@ -105,10 +91,6 @@ const sortByLabel = (a: SelectableValue, b: SelectableValue) => return a.label?.localeCompare(b.label ?? '') || 0; }; -const findGroupMatchingLabel = (group: SelectableValue, query: string) => { - return group.label?.toLowerCase().includes(query.toLowerCase()); -}; - const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluateFor: string }> => ({ required: { value: true, @@ -149,24 +131,10 @@ const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluate }, }); -const useIsNewGroup = (folder: string, group: string) => { - const { groupOptions } = useFolderGroupOptions(folder, false); - - const groupIsInGroupOptions = useCallback( - (group_: string) => groupOptions.some((groupInList: SelectableValue) => groupInList.label === group_), - [groupOptions] - ); - return !groupIsInGroupOptions(group); -}; - export function GrafanaEvaluationBehaviorStep({ - evaluateEvery, - setEvaluateEvery, existing, enableProvisionedGroups, }: { - evaluateEvery: string; - setEvaluateEvery: (value: string) => void; existing: boolean; enableProvisionedGroups: boolean; }) { @@ -181,51 +149,39 @@ export function GrafanaEvaluationBehaviorStep({ control, } = useFormContext(); - const [folder, group, type, isPaused, folderUid, folderName] = watch([ - 'folder', + const [group, type, isPaused, folder, evaluateEvery] = watch([ 'group', 'type', 'isPaused', - 'folder.uid', - 'folder.title', + 'folder', + 'evaluateEvery', ]); const isGrafanaAlertingRule = isGrafanaAlertingRuleByType(type); const isGrafanaRecordingRule = isGrafanaRecordingRuleByType(type); - const { groupOptions, loading } = useFolderGroupOptions(folder?.uid ?? '', enableProvisionedGroups); + const { currentData: rulerNamespace, isLoading: loadingGroups } = useFetchGroupsForFolder(folder?.uid ?? ''); const [isEditingGroup, setIsEditingGroup] = useState(false); - const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules); - const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME]; + const groupOptions = useMemo(() => { + return rulerNamespace ? namespaceToGroupOptions(rulerNamespace, enableProvisionedGroups) : []; + }, [enableProvisionedGroups, rulerNamespace]); - const grafanaNamespaces = useCombinedRuleNamespaces(GRAFANA_RULES_SOURCE_NAME); - const existingNamespace = grafanaNamespaces.find((ns) => ns.uid === folderUid); - const existingGroup = existingNamespace?.groups.find((g) => g.name === group); - - const isNewGroup = useIsNewGroup(folderUid ?? '', group); + const existingGroup = Object.values(rulerNamespace ?? {}) + .flat() + .find((ruleGroup) => ruleGroup.name === group); + const isNewGroup = !existingGroup && !loadingGroups; + // synchronize the evaluation interval with the group name when it's an existing group useEffect(() => { - if (!isNewGroup && existingGroup?.interval) { - setEvaluateEvery(existingGroup.interval); + if (existingGroup) { + setValue('evaluateEvery', existingGroup.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL); } - }, [setEvaluateEvery, isNewGroup, setValue, existingGroup]); - - const closeEditGroupModal = (saved = false) => { - if (!saved) { - logInfo(LogMessages.leavingRuleGroupEdit); - } - setIsEditingGroup(false); - }; + }, [existingGroup, setValue]); + const closeEditGroupModal = () => setIsEditingGroup(false); const onOpenEditGroupModal = () => setIsEditingGroup(true); - const editGroupDisabled = groupfoldersForGrafana?.loading || isNewGroup || !folderUid || !group; - const emptyNamespace: CombinedRuleNamespace = { - name: folderName, - rulesSource: GRAFANA_RULES_SOURCE_NAME, - groups: [], - }; - const emptyGroup: CombinedRuleGroup = { name: group, interval: evaluateEvery, rules: [], totals: {} }; + const editGroupDisabled = loadingGroups || isNewGroup || !folder?.uid || !group; const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false); @@ -235,18 +191,6 @@ export function GrafanaEvaluationBehaviorStep({ setIsCreatingEvaluationGroup(false); }; - const getOptions = useCallback( - async (query: string) => { - const results = query ? groupOptions.filter((group) => findGroupMatchingLabel(group, query)) : groupOptions; - return take(results, MAX_GROUP_RESULTS); - }, - [groupOptions] - ); - - const debouncedSearch = useMemo(() => { - return debounce(getOptions, 300, { leading: true }); - }, [getOptions]); - const defaultGroupValue = group ? { value: group, label: group } : undefined; const pauseContentText = isGrafanaRecordingRule @@ -257,7 +201,7 @@ export function GrafanaEvaluationBehaviorStep({ const step = isGrafanaManagedRuleByType(type) ? 4 : 3; const label = - isGrafanaManagedRuleByType(type) && !folder + isGrafanaManagedRuleByType(type) && !folder?.uid ? t( 'alerting.rule-form.evaluation.select-folder-before', 'Select a folder before setting evaluation group and interval' @@ -284,21 +228,20 @@ export function GrafanaEvaluationBehaviorStep({ > ( - { field.onChange(group.label ?? ''); }} - isLoading={loading} - invalid={Boolean(folder) && !group && Boolean(fieldState.error)} - loadOptions={debouncedSearch} + isLoading={loadingGroups} + invalid={Boolean(folder?.uid) && !group && Boolean(fieldState.error)} cacheOptions loadingMessage={'Loading groups...'} defaultValue={defaultGroupValue} - defaultOptions={groupOptions} + options={groupOptions} getOptionLabel={(option: SelectableValue) => (
{option.label} @@ -329,7 +272,7 @@ export function GrafanaEvaluationBehaviorStep({ icon="plus" fill="outline" variant="secondary" - disabled={!folder} + disabled={!folder?.uid} data-testid={selectors.components.AlertRules.newEvaluationGroupButton} > New evaluation group @@ -339,22 +282,25 @@ export function GrafanaEvaluationBehaviorStep({ setIsCreatingEvaluationGroup(false)} - groupfoldersForGrafana={groupfoldersForGrafana?.result} + groupfoldersForGrafana={rulerNamespace} /> )} - {folderName && isEditingGroup && ( + {folder?.uid && isEditingGroup && ( closeEditGroupModal()} intervalEditOnly hideFolder={true} /> )} - {folderName && group && ( + {folder?.title && group && (
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 54d22db90ac..3d46e0873e7 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-hook-form'; import { useParams } from 'react-router-dom-v5-compat'; @@ -22,7 +22,6 @@ import { isGrafanaRulerRulePaused, isRecordingRuleByType, } from 'app/features/alerting/unified/utils/rules'; -import { isExpressionQuery } from 'app/features/expressions/guards'; import { RuleGroupIdentifier, RuleIdentifier, RuleWithLocation } from 'app/types/unified-alerting'; import { PostableRuleGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; @@ -40,24 +39,21 @@ import { shouldUsePrometheusRulesPrimary } from '../../../featureToggles'; import { useDeleteRuleFromGroup } from '../../../hooks/ruleGroup/useDeleteRuleFromGroup'; import { useAddRuleToRuleGroup, useUpdateRuleInRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup'; import { useReturnTo } from '../../../hooks/useReturnTo'; -import { useURLSearchParams } from '../../../hooks/useURLSearchParams'; -import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; -import { DataSourceType } from '../../../utils/datasource'; import { - DEFAULT_GROUP_EVALUATION_INTERVAL, + defaultFormValuesForRuleType, + formValuesFromExistingRule, + formValuesFromPrefill, + translateRouteParamToRuleType, +} from '../../../rule-editor/formDefaults'; +import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; +import { MANUAL_ROUTING_KEY, SIMPLIFIED_QUERY_EDITOR_KEY, - formValuesFromExistingRule, formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, - getDefaultFormValues, - getDefaultQueries, - ignoreHiddenQueries, - normalizeDefaultAnnotations, } from '../../../utils/rule-form'; import * as ruleId from '../../../utils/rule-id'; import { fromRulerRule, fromRulerRuleAndRuleGroupIdentifier, stringifyIdentifier } from '../../../utils/rule-id'; -import { isGrafanaRecordingRuleByType } from '../../../utils/rules'; import { createRelativeUrl } from '../../../utils/url'; import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter'; import { AlertRuleNameAndMetric } from '../AlertRuleNameInput'; @@ -68,12 +64,7 @@ import { GrafanaFolderAndLabelsStep } from '../GrafanaFolderAndLabelsStep'; import { NotificationsStep } from '../NotificationsStep'; import { RecordingRulesNameSpaceAndGroupStep } from '../RecordingRulesNameSpaceAndGroupStep'; import { RuleInspector } from '../RuleInspector'; -import { - QueryAndExpressionsStep, - areQueriesTransformableToSimpleCondition, - isExpressionQueryInAlert, -} from '../query-and-alert-condition/QueryAndExpressionsStep'; -import { translateRouteParamToRuleType } from '../util'; +import { QueryAndExpressionsStep } from '../query-and-alert-condition/QueryAndExpressionsStep'; type Props = { existing?: RuleWithLocation; @@ -85,9 +76,7 @@ const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary(); export const AlertRuleForm = ({ existing, prefill }: Props) => { const styles = useStyles2(getStyles); const notifyApp = useAppNotification(); - const [queryParams] = useURLSearchParams(); const [showEditYaml, setShowEditYaml] = useState(false); - const [evaluateEvery, setEvaluateEvery] = useState(existing?.group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL); const [deleteRuleFromGroup] = useDeleteRuleFromGroup(); const [addRuleToRuleGroup] = useAddRuleToRuleGroup(); @@ -110,19 +99,10 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { return formValuesFromPrefill(prefill); } - if (queryParams.has('defaults')) { - return formValuesFromQueryParams(queryParams.get('defaults') ?? '', ruleType); - } const defaultRuleType = ruleType || RuleFormType.grafana; - return { - ...getDefaultFormValues(), - condition: 'C', - queries: getDefaultQueries(isGrafanaRecordingRuleByType(defaultRuleType)), - type: defaultRuleType, - evaluateEvery: evaluateEvery, - }; - }, [existing, prefill, queryParams, evaluateEvery, ruleType]); + return defaultFormValuesForRuleType(defaultRuleType); + }, [existing, prefill, ruleType]); const formAPI = useForm({ mode: 'onSubmit', @@ -151,6 +131,8 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { // @todo why is error not propagated to form? const submit = async (values: RuleFormValues, exitOnSave: boolean) => { + const { type, evaluateEvery } = values; + if (conditionErrorMsg !== '') { notifyApp.error(conditionErrorMsg); if (!existing && grafanaTypeRule) { @@ -160,7 +142,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { return; } - trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type }); + trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: type }); const ruleDefinition = grafanaTypeRule ? formValuesToRulerGrafanaRuleDTO(values) : formValuesToRulerRuleDTO(values); @@ -206,12 +188,11 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { const deleteRule = async () => { if (existing) { - const returnTo = queryParams.get('returnTo') || '/alerting/list'; const ruleGroupIdentifier = getRuleGroupLocationFromRuleWithLocation(existing); const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule); await deleteRuleFromGroup.execute(ruleGroupIdentifier, ruleIdentifier); - locationService.replace(returnTo); + locationService.replace(returnTo ?? '/alerting/list'); } }; @@ -236,9 +217,6 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { locationService.getHistory().goBack(); }; - const evaluateEveryInForm = watch('evaluateEvery'); - useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]); - const actionButtons = ( {existing && ( @@ -314,12 +292,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { {/* Step 4 & 5 & 6*/} {isGrafanaManagedRuleByType(type) && ( - + )} {/* Notifications step*/} @@ -379,101 +352,17 @@ const isCortexLokiOrRecordingRule = (watch: UseFormWatch) => { return (ruleType === RuleFormType.cloudAlerting || ruleType === RuleFormType.cloudRecording) && dataSourceName !== ''; }; -function formValuesFromQueryParams(ruleDefinition: string, type: RuleFormType): RuleFormValues { - let ruleFromQueryParams: Partial; - - try { - ruleFromQueryParams = JSON.parse(ruleDefinition); - } catch (err) { - return { - ...getDefaultFormValues(), - queries: getDefaultQueries(), - }; - } - - return setQueryEditorSettings( - setInstantOrRange( - ignoreHiddenQueries({ - ...getDefaultFormValues(), - ...ruleFromQueryParams, - annotations: normalizeDefaultAnnotations(ruleFromQueryParams.annotations ?? []), - queries: ruleFromQueryParams.queries ?? getDefaultQueries(), - type: type || RuleFormType.grafana, - evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, - }) - ) - ); -} - -function formValuesFromPrefill(rule: Partial): RuleFormValues { - return ignoreHiddenQueries({ - ...getDefaultFormValues(), - ...rule, - }); -} - -function setQueryEditorSettings(values: RuleFormValues): RuleFormValues { - const isQuerySwitchModeEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; - - if (!isQuerySwitchModeEnabled) { - return { - ...values, - editorSettings: { - simplifiedQueryEditor: false, - simplifiedNotificationEditor: true, // actually it doesn't matter in this case - }, - }; - } - - // data queries only - const dataQueries = values.queries.filter((query) => !isExpressionQuery(query.model)); - - // expression queries only - const expressionQueries = values.queries.filter((query) => isExpressionQueryInAlert(query)); - - const queryParamsAreTransformable = areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries); - return { - ...values, - editorSettings: { - simplifiedQueryEditor: queryParamsAreTransformable, - simplifiedNotificationEditor: true, - }, - }; -} - -function setInstantOrRange(values: RuleFormValues): RuleFormValues { - return { - ...values, - queries: values.queries?.map((query) => { - if (isExpressionQuery(query.model)) { - return query; - } - // data query - const defaultToInstant = - query.model.datasource?.type === DataSourceType.Loki || - query.model.datasource?.type === DataSourceType.Prometheus; - const isInstant = - 'instant' in query.model && query.model.instant !== undefined ? query.model.instant : defaultToInstant; - return { - ...query, - model: { - ...query.model, - instant: isInstant, - range: !isInstant, // we cannot have both instant and range queries in alerting - }, - }; - }), - }; -} - function storeInLocalStorageValues(values: RuleFormValues) { - if (values.manualRouting) { + const { manualRouting, editorSettings } = values; + + if (manualRouting) { localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); } else { localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); } - if (values.editorSettings) { - if (values.editorSettings.simplifiedQueryEditor) { + + if (editorSettings) { + if (editorSettings.simplifiedQueryEditor) { localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'true'); } else { localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'false'); diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx index 38598f08784..72093ecc490 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx @@ -16,14 +16,10 @@ import { alertRuleApi } from '../../../api/alertRuleApi'; import { fetchRulerRulesGroup } from '../../../api/ruler'; import { useDataSourceFeatures } from '../../../hooks/useCombinedRule'; import { useReturnTo } from '../../../hooks/useReturnTo'; +import { DEFAULT_GROUP_EVALUATION_INTERVAL, getDefaultFormValues } from '../../../rule-editor/formDefaults'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource'; -import { - DEFAULT_GROUP_EVALUATION_INTERVAL, - formValuesToRulerGrafanaRuleDTO, - getDefaultFormValues, - getDefaultQueries, -} from '../../../utils/rule-form'; +import { formValuesToRulerGrafanaRuleDTO, getDefaultQueries } from '../../../utils/rule-form'; import { isGrafanaRulerRule } from '../../../utils/rules'; import { FileExportPreview } from '../../export/FileExportPreview'; import { GrafanaExportDrawer } from '../../export/GrafanaExportDrawer'; @@ -64,9 +60,7 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor const { returnTo } = useReturnTo('/alerting/list'); const [exportData, setExportData] = useState(undefined); - const [conditionErrorMsg, setConditionErrorMsg] = useState(''); - const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? DEFAULT_GROUP_EVALUATION_INTERVAL); const onInvalid = (): void => { notifyApp.error('There are errors in the form. Please correct them and try again!'); @@ -112,12 +106,7 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor {/* Step 4 & 5 */} - + {/* Notifications step*/} {/* Annotations only for cloud and Grafana */} diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts index 6fc5ba37a35..2a1e67855e3 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts @@ -1,9 +1,9 @@ import { RulerRuleDTO, RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { mockRulerGrafanaRecordingRule, mockRulerGrafanaRule } from '../../../mocks'; +import { getDefaultFormValues } from '../../../rule-editor/formDefaults'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { Annotation } from '../../../utils/constants'; -import { getDefaultFormValues } from '../../../utils/rule-form'; import { getPayloadToExport } from './ModifyExportRuleForm'; diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx index 11b3a0f5dfc..116374475da 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx @@ -106,8 +106,10 @@ describe('Can create a new grafana managed alert using simplified routing', () = it('simplified routing is not available when Grafana AM is not enabled', async () => { setAlertmanagerChoices(AlertmanagerChoice.External, 1); - renderRuleEditor(); + const { user } = renderRuleEditor(); + // Just to make sure all dropdowns have been loaded + await selectFolderAndGroup(user); await waitFor(() => expect(ui.inputs.simplifiedRouting.contactPointRouting.query()).not.toBeInTheDocument()); }); @@ -147,6 +149,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = expect(await screen.findByText('Email')).toBeInTheDocument(); }); }); + describe('switch modes enabled', () => { testWithFeatureToggles(['alertingQueryAndExpressionsStepMode', 'alertingNotificationsStepMode']); @@ -168,6 +171,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = const serializedRequests = await serializeRequests(requests); expect(serializedRequests).toMatchSnapshot(); }); + it('can create the new grafana-managed rule with advanced modes', async () => { const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/')); @@ -185,6 +189,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = const serializedRequests = await serializeRequests(requests); expect(serializedRequests).toMatchSnapshot(); }); + it('can create the new grafana-managed rule with only notifications step advanced mode', async () => { const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/')); @@ -202,6 +207,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = const serializedRequests = await serializeRequests(requests); expect(serializedRequests).toMatchSnapshot(); }); + it('can create the new grafana-managed rule with only query step advanced mode', async () => { const contactPointName = 'lotsa-emails'; const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/')); @@ -221,6 +227,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = const serializedRequests = await serializeRequests(requests); expect(serializedRequests).toMatchSnapshot(); }); + it('switch modes are intiallized depending on the local storage - 1', async () => { localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'false'); localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); @@ -231,6 +238,7 @@ describe('Can create a new grafana managed alert using simplified routing', () = expect(ui.inputs.switchModeAdvanced(GrafanaRuleFormStep.Query).get()).toBeInTheDocument(); expect(ui.inputs.switchModeBasic(GrafanaRuleFormStep.Notification).get()).toBeInTheDocument(); }); + it('switch modes are intiallized depending on the local storage - 2', async () => { localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'true'); localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx index 752d9e71101..a0983cba07f 100644 --- a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.test.tsx @@ -136,6 +136,7 @@ describe('LabelsField with suggestions', () => { expect(screen.getByTestId('labelsInSubform-key-2')).toHaveTextContent('key3'); expect(screen.getByTestId('labelsInSubform-value-2')).toHaveTextContent('value3'); }); + it('Should be able to write new keys and values using the dropdowns, case sensitive', async () => { const { user } = await renderLabelsWithSuggestions(); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index f43e3a14b67..2ecd8131b3b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -23,16 +23,14 @@ import { import { Text } from '@grafana/ui/src/components/Text/Text'; import { Trans, t } from 'app/core/internationalization'; import { isExpressionQuery } from 'app/features/expressions/guards'; -import { - ExpressionDatasourceUID, - ExpressionQuery, - ExpressionQueryType, - ReducerMode, - expressionTypes, -} from 'app/features/expressions/types'; -import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; +import { ExpressionDatasourceUID, ExpressionQueryType, expressionTypes } from 'app/features/expressions/types'; +import { AlertQuery } from 'app/types/unified-alerting-dto'; import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler'; +import { + areQueriesTransformableToSimpleCondition, + isExpressionQueryInAlert, +} from '../../../rule-editor/formProcessing'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource'; import { PromOrLokiQuery, isPromOrLokiQuery } from '../../../utils/rule-form'; @@ -76,49 +74,6 @@ import { import { useAdvancedMode } from './useAdvancedMode'; import { useAlertQueryRunner } from './useAlertQueryRunner'; -export function areQueriesTransformableToSimpleCondition( - dataQueries: Array>, - expressionQueries: Array> -) { - if (dataQueries.length !== 1) { - return false; - } - const singleReduceExpressionInInstantQuery = - 'instant' in dataQueries[0].model && dataQueries[0].model.instant && expressionQueries.length === 1; - - if (expressionQueries.length !== 2 && !singleReduceExpressionInInstantQuery) { - return false; - } - - const query = dataQueries[0]; - - if (query.refId !== SimpleConditionIdentifier.queryId) { - return false; - } - - const reduceExpressionIndex = expressionQueries.findIndex( - (query) => query.model.type === ExpressionQueryType.reduce && query.refId === SimpleConditionIdentifier.reducerId - ); - const reduceExpression = expressionQueries.at(reduceExpressionIndex); - const reduceOk = - reduceExpression && - reduceExpressionIndex === 0 && - (reduceExpression.model.settings?.mode === ReducerMode.Strict || - reduceExpression.model.settings?.mode === undefined); - - const thresholdExpressionIndex = expressionQueries.findIndex( - (query) => - query.model.type === ExpressionQueryType.threshold && query.refId === SimpleConditionIdentifier.thresholdId - ); - const thresholdExpression = expressionQueries.at(thresholdExpressionIndex); - const conditions = thresholdExpression?.model.conditions ?? []; - const thresholdIndexOk = singleReduceExpressionInInstantQuery - ? thresholdExpressionIndex === 0 - : thresholdExpressionIndex === 1; - const thresholdOk = thresholdExpression && thresholdIndexOk && conditions[0]?.unloadEvaluator === undefined; - return (Boolean(reduceOk) || Boolean(singleReduceExpressionInInstantQuery)) && Boolean(thresholdOk); -} - interface Props { editingExistingRule: boolean; onDataChange: (error: string) => void; @@ -777,9 +732,3 @@ const useSetExpressionAndDataSource = () => { } }; }; - -export function isExpressionQueryInAlert( - query: AlertQuery -): query is AlertQuery { - return isExpressionQuery(query.model); -} diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts index 208342735e6..2a9294ada7d 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts @@ -2,11 +2,10 @@ import { produce } from 'immer'; import { EvalFunction } from 'app/features/alerting/state/alertDef'; import { dataQuery, reduceExpression, thresholdExpression } from 'app/features/alerting/unified/mocks'; +import { areQueriesTransformableToSimpleCondition } from 'app/features/alerting/unified/rule-editor/formProcessing'; import { ExpressionQuery, ReducerMode } from 'app/features/expressions/types'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; -import { areQueriesTransformableToSimpleCondition } from '../QueryAndExpressionsStep'; - const expressionQueries: Array> = [reduceExpression, thresholdExpression]; describe('areQueriesTransformableToSimpleCondition', () => { diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/useAdvancedMode.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/useAdvancedMode.ts index 0c793dfdaac..08e9d889bbf 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/useAdvancedMode.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/useAdvancedMode.ts @@ -5,7 +5,8 @@ import { EvalFunction } from 'app/features/alerting/state/alertDef'; import { ExpressionQuery } from 'app/features/expressions/types'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; -import { areQueriesTransformableToSimpleCondition } from './QueryAndExpressionsStep'; +import { areQueriesTransformableToSimpleCondition } from '../../../rule-editor/formProcessing'; + import { SimpleCondition, getSimpleConditionFromExpressions } from './SimpleCondition'; function initializeSimpleCondition( @@ -30,7 +31,7 @@ export function determineAdvancedMode(simplifiedQueryEditor: boolean | undefined } /* - This hook is used mantain the state of the advanced mode, and the simple condition, + This hook is used mantain the state of the advanced mode, and the simple condition, depending on the editor settings, the alert type, and the queries. */ export const useAdvancedMode = ( diff --git a/public/app/features/alerting/unified/components/rule-editor/util.ts b/public/app/features/alerting/unified/components/rule-editor/util.ts index 1c432344b1c..3933de4f48e 100644 --- a/public/app/features/alerting/unified/components/rule-editor/util.ts +++ b/public/app/features/alerting/unified/components/rule-editor/util.ts @@ -15,8 +15,6 @@ import { isExpressionQuery } from 'app/features/expressions/guards'; import { ClassicCondition, ExpressionQueryType } from 'app/features/expressions/types'; import { AlertQuery } from 'app/types/unified-alerting-dto'; -import { RuleFormType } from '../../types/rule-form'; - import { createDagFromQueries, getOriginOfRefId } from './dag'; export function queriesWithUpdatedReferences( @@ -312,18 +310,6 @@ export function getStatusMessage(data: PanelData): string | undefined { return data.error?.message ?? genericErrorMessage; } -export function translateRouteParamToRuleType(param = ''): RuleFormType { - if (param === 'recording') { - return RuleFormType.cloudRecording; - } - - if (param === 'grafana-recording') { - return RuleFormType.grafanaRecording; - } - - return RuleFormType.grafana; -} - /** * This function finds what refIds have been updated given the previous Array of queries and an Array of updated data queries. * All expression queries are discarded from the arrays, since we have separate handlers for those (see "onUpdateRefId") of the ExpressionEditor diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx index be7153bc131..7f41a873797 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx @@ -4,7 +4,6 @@ import { byRole, byText } from 'testing-library-selector'; import { setPluginLinksHook } from '@grafana/runtime'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; -import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure'; import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting'; @@ -20,6 +19,7 @@ import { mockPromAlertingRule, } from '../../mocks'; import { grafanaRulerRule } from '../../mocks/grafanaRulerApi'; +import { grantPermissionsHelper } from '../../test/test-utils'; import { setupDataSources } from '../../testSetup/datasources'; import { Annotation } from '../../utils/constants'; import { DataSourceType } from '../../utils/datasource'; @@ -76,16 +76,6 @@ setPluginLinksHook(() => ({ isLoading: false, })); -/** - * "Grants" permissions via contextSrv mock, and additionally sets folder access control - * API response to match - */ -const grantPermissionsHelper = (permissions: AccessControlAction[]) => { - const permissionsHash = permissions.reduce((hash, permission) => ({ ...hash, [permission]: true }), {}); - grantUserPermissions(permissions); - setFolderAccessControl(permissionsHash); -}; - const openSilenceDrawer = async () => { const user = userEvent.setup(); await user.click(ELEMENTS.actions.more.button.get()); diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx index dcbc646ae03..f755d6ff9e3 100644 --- a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx +++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx @@ -1,17 +1,17 @@ +import { HttpResponse } from 'msw'; import { render } from 'test/test-utils'; import { byLabelText, byTestId, byText, byTitle } from 'testing-library-selector'; -import { CombinedRuleNamespace } from 'app/types/unified-alerting'; +import { AccessControlAction } from 'app/types'; +import { RuleGroupIdentifier } from 'app/types/unified-alerting'; -import { - mockCombinedRule, - mockCombinedRuleNamespace, - mockDataSource, - mockPromAlertingRule, - mockPromRecordingRule, - mockRulerAlertingRule, - mockRulerRecordingRule, -} from '../../mocks'; +import { GRAFANA_RULER_CONFIG } from '../../api/featureDiscoveryApi'; +import server, { setupMswServer } from '../../mockApi'; +import { mimirDataSource } from '../../mocks/server/configure'; +import { alertingFactory } from '../../mocks/server/db'; +import { rulerRuleGroupHandler as grafanaRulerRuleGroupHandler } from '../../mocks/server/handlers/grafanaRuler'; +import { rulerRuleGroupHandler } from '../../mocks/server/handlers/mimirRuler'; +import { grantPermissionsHelper } from '../../test/test-utils'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { EditRuleGroupModal } from './EditRuleGroupModal'; @@ -29,133 +29,165 @@ const ui = { }; const noop = () => jest.fn(); +setupMswServer(); -describe('EditGroupModal', () => { +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + useReturnToPrevious: jest.fn(), +})); + +describe('EditGroupModal component on cloud alert rules', () => { it('Should disable all inputs but interval when intervalEditOnly is set', async () => { - const namespace = mockCombinedRuleNamespace({ - name: 'my-alerts', - rulesSource: mockDataSource(), - groups: [{ name: 'default-group', interval: '90s', rules: [], totals: {} }], + const { rulerConfig } = mimirDataSource(); + + const group = alertingFactory.ruler.group.build({ + rules: [alertingFactory.ruler.alertingRule.build(), alertingFactory.ruler.recordingRule.build()], }); - const group = namespace.groups[0]; + // @TODO need to simplify this a bit I think, ideally there would be a higher-level function that simply sets up a few rules + // and attaches the ruler and prometheus endpoint(s) – including the namespaces and group endpoints. + server.use( + rulerRuleGroupHandler({ + response: HttpResponse.json(group), + }) + ); - render(); + const rulerGroupIdentifier: RuleGroupIdentifier = { + dataSourceName: rulerConfig.dataSourceName, + groupName: 'default-group', + namespaceName: 'my-namespace', + }; + + render( + + ); expect(await ui.input.namespace.find()).toHaveAttribute('readonly'); expect(ui.input.group.get()).toHaveAttribute('readonly'); expect(ui.input.interval.get()).not.toHaveAttribute('readonly'); }); -}); - -describe('EditGroupModal component on cloud alert rules', () => { - const promDsSettings = mockDataSource({ name: 'Prometheus-1', uid: 'Prometheus-1' }); - - const alertingRule = mockCombinedRule({ - namespace: undefined, - promRule: mockPromAlertingRule({ name: 'alerting-rule-cpu' }), - rulerRule: mockRulerAlertingRule({ alert: 'alerting-rule-cpu' }), - }); - - const recordingRule1 = mockCombinedRule({ - namespace: undefined, - promRule: mockPromRecordingRule({ name: 'recording-rule-memory' }), - rulerRule: mockRulerRecordingRule({ record: 'recording-rule-memory' }), - }); - - const recordingRule2 = mockCombinedRule({ - namespace: undefined, - promRule: mockPromRecordingRule({ name: 'recording-rule-cpu' }), - rulerRule: mockRulerRecordingRule({ record: 'recording-rule-cpu' }), - }); it('Should show alert table in case of having some non-recording rules in the group', async () => { - const promNs = mockCombinedRuleNamespace({ - name: 'prometheus-ns', - rulesSource: promDsSettings, - groups: [ - { name: 'default-group', interval: '90s', rules: [alertingRule, recordingRule1, recordingRule2], totals: {} }, - ], + const { dataSource, rulerConfig } = mimirDataSource(); + + const group = alertingFactory.ruler.group.build({ + rules: [alertingFactory.ruler.alertingRule.build(), alertingFactory.ruler.recordingRule.build()], }); - const group = promNs.groups[0]; + // @TODO need to simplify this a bit I think, ideally there would be a higher-level function that simply sets up a few rules + // and attaches the ruler and prometheus endpoint(s) – including the namespaces and group endpoints. + server.use( + rulerRuleGroupHandler({ + response: HttpResponse.json(group), + }) + ); - render(); + const ruleGroupIdentifier: RuleGroupIdentifier = { + dataSourceName: dataSource.name, + groupName: group.name, + namespaceName: 'ns1', + }; - expect(await ui.input.namespace.find()).toHaveValue('prometheus-ns'); + render(); + + expect(await ui.input.namespace.find()).toHaveValue('ns1'); expect(ui.input.namespace.get()).not.toHaveAttribute('readonly'); - expect(ui.input.group.get()).toHaveValue('default-group'); + expect(ui.input.group.get()).toHaveValue(group.name); + + // @ts-ignore + const ruleName = group.rules.at(0).alert; expect(ui.tableRows.getAll()).toHaveLength(1); // Only one rule is non-recording - expect(ui.tableRows.getAll()[0]).toHaveTextContent('alerting-rule-cpu'); + expect(ui.tableRows.getAll().at(0)).toHaveTextContent(ruleName); }); it('Should not show alert table in case of having exclusively recording rules in the group', async () => { - const promNs = mockCombinedRuleNamespace({ - name: 'prometheus-ns', - rulesSource: promDsSettings, - groups: [{ name: 'default-group', interval: '90s', rules: [recordingRule1, recordingRule2], totals: {} }], + const { dataSource, rulerConfig } = mimirDataSource(); + + const group = alertingFactory.ruler.group.build({ + rules: [alertingFactory.ruler.recordingRule.build(), alertingFactory.ruler.recordingRule.build()], }); - const group = promNs.groups[0]; + // @TODO need to simplify this a bit I think + server.use( + rulerRuleGroupHandler({ + response: HttpResponse.json(group), + }) + ); - render(); + const ruleGroupIdentifier: RuleGroupIdentifier = { + dataSourceName: dataSource.name, + groupName: group.name, + namespaceName: 'ns1', + }; + + render(); expect(ui.table.query()).not.toBeInTheDocument(); expect(await ui.noRulesText.find()).toBeInTheDocument(); }); }); describe('EditGroupModal component on grafana-managed alert rules', () => { - const grafanaNamespace: CombinedRuleNamespace = { - name: 'namespace1', - rulesSource: GRAFANA_RULES_SOURCE_NAME, - groups: [ - { - name: 'grafanaGroup1', - interval: '30s', - rules: [ - mockCombinedRule({ - namespace: undefined, - promRule: mockPromAlertingRule({ name: 'high-cpu-1' }), - rulerRule: mockRulerAlertingRule({ alert: 'high-cpu-1' }), - }), - mockCombinedRule({ - namespace: undefined, - promRule: mockPromAlertingRule({ name: 'high-memory' }), - rulerRule: mockRulerAlertingRule({ alert: 'high-memory' }), - }), - ], - totals: {}, - }, - ], + // @TODO simplify folder stuff, should also have a higher-level function to set these up + const folder = alertingFactory.folder.build(); + const NAMESPACE_UID = folder.uid; + + const group = alertingFactory.ruler.group.build({ + rules: [alertingFactory.ruler.alertingRule.build(), alertingFactory.ruler.alertingRule.build()], + }); + + const ruleGroupIdentifier: RuleGroupIdentifier = { + dataSourceName: GRAFANA_RULES_SOURCE_NAME, + groupName: group.name, + namespaceName: NAMESPACE_UID, }; - const grafanaGroup1 = grafanaNamespace.groups[0]; + beforeEach(() => { + grantPermissionsHelper([ + AccessControlAction.AlertingRuleCreate, + AccessControlAction.AlertingRuleRead, + AccessControlAction.AlertingRuleUpdate, + ]); + + server.use( + grafanaRulerRuleGroupHandler({ + response: HttpResponse.json(group), + }) + ); + }); const renderWithGrafanaGroup = () => - render(); + render( + + ); it('Should show alert table', async () => { renderWithGrafanaGroup(); - expect(await ui.input.namespace.find()).toHaveValue('namespace1'); - expect(ui.input.group.get()).toHaveValue('grafanaGroup1'); - expect(ui.input.interval.get()).toHaveValue('30s'); + expect(await ui.input.namespace.find()).toHaveValue(NAMESPACE_UID); + expect(ui.input.group.get()).toHaveValue(group.name); + expect(ui.input.interval.get()).toHaveValue(group.interval); expect(ui.tableRows.getAll()).toHaveLength(2); - expect(ui.tableRows.getAll()[0]).toHaveTextContent('high-cpu-1'); - expect(ui.tableRows.getAll()[1]).toHaveTextContent('high-memory'); + // @ts-ignore + expect(ui.tableRows.getAll().at(0)).toHaveTextContent(group.rules.at(0).alert); + // @ts-ignore + expect(ui.tableRows.getAll().at(1)).toHaveTextContent(group.rules.at(1).alert); }); it('Should have folder input in readonly mode', async () => { renderWithGrafanaGroup(); - expect(await ui.input.namespace.find()).toHaveAttribute('readonly'); }); it('Should not display folder link if no folderUrl provided', async () => { renderWithGrafanaGroup(); - expect(await ui.input.namespace.find()).toHaveValue('namespace1'); + expect(await ui.input.namespace.find()).toHaveValue(NAMESPACE_UID); expect(ui.folderLink.query()).not.toBeInTheDocument(); }); }); diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx index 76427e7d5fa..828920a7e52 100644 --- a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx +++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx @@ -4,32 +4,46 @@ import { useMemo } from 'react'; import { FieldValues, FormProvider, RegisterOptions, useForm, useFormContext } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; -import { Alert, Badge, Button, Field, Input, Label, LinkButton, Modal, Stack, useStyles2 } from '@grafana/ui'; +import { + Alert, + Badge, + Button, + Field, + Input, + Label, + LinkButton, + LoadingPlaceholder, + Modal, + Stack, + useStyles2, +} from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { Trans } from 'app/core/internationalization'; +import { Trans, t } from 'app/core/internationalization'; import { dispatch } from 'app/store/store'; -import { CombinedRuleGroup, CombinedRuleNamespace, RuleGroupIdentifier } from 'app/types/unified-alerting'; -import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; +import { RuleGroupIdentifier, RulerDataSourceConfig } from 'app/types/unified-alerting'; +import { RulerRuleDTO, RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; +import { alertRuleApi } from '../../api/alertRuleApi'; import { useMoveRuleGroup, useRenameRuleGroup, useUpdateRuleGroupConfiguration, } from '../../hooks/ruleGroup/useUpdateRuleGroup'; import { anyOfRequestState } from '../../hooks/useAsync'; +import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../rule-editor/formDefaults'; import { fetchRulerRulesAction, rulesInSameGroupHaveInvalidFor } from '../../state/actions'; import { checkEvaluationIntervalGlobalLimit } from '../../utils/config'; -import { GRAFANA_RULES_SOURCE_NAME, getRulesSourceName } from '../../utils/datasource'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { stringifyErrorLike } from '../../utils/misc'; -import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form'; import { AlertInfo, getAlertInfo, isGrafanaOrDataSourceRecordingRule } from '../../utils/rules'; import { formatPrometheusDuration, parsePrometheusDuration, safeParsePrometheusDuration } from '../../utils/time'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning'; -import { decodeGrafanaNamespace, encodeGrafanaNamespace } from '../expressions/util'; import { EvaluationGroupQuickPick } from '../rule-editor/EvaluationGroupQuickPick'; import { MIN_TIME_RANGE_STEP_S } from '../rule-editor/GrafanaEvaluationBehavior'; +const useRuleGroupDefinition = alertRuleApi.endpoints.getRuleGroupForNamespace.useQuery; + const ITEMS_PER_PAGE = 10; function ForBadge({ message, error }: { message: string; error?: boolean }) { @@ -170,17 +184,59 @@ export const evaluateEveryValidationOptions = (rules: Rul }); export interface ModalProps { - namespace: CombinedRuleNamespace; - group: CombinedRuleGroup; + ruleGroupIdentifier: RuleGroupIdentifier; + folderTitle?: string; + rulerConfig: RulerDataSourceConfig; onClose: (saved?: boolean) => void; intervalEditOnly?: boolean; folderUrl?: string; - folderUid?: string; hideFolder?: boolean; } -export function EditRuleGroupModal(props: ModalProps): React.ReactElement { - const { namespace, group, onClose, intervalEditOnly, folderUid } = props; +export interface ModalFormProps { + ruleGroupIdentifier: RuleGroupIdentifier; + folderTitle?: string; // used to display the GMA folder title + ruleGroup: RulerRuleGroupDTO; + onClose: (saved?: boolean) => void; + intervalEditOnly?: boolean; + folderUrl?: string; + hideFolder?: boolean; +} + +// this component just wraps the modal with some loading state for grabbing rules and such +export function EditRuleGroupModal(props: ModalProps) { + const { ruleGroupIdentifier, rulerConfig, intervalEditOnly, onClose } = props; + const rulesSourceName = ruleGroupIdentifier.dataSourceName; + const isGrafanaManagedGroup = rulesSourceName === GRAFANA_RULES_SOURCE_NAME; + + const modalTitle = + intervalEditOnly || isGrafanaManagedGroup ? 'Edit evaluation group' : 'Edit namespace or evaluation group'; + + const styles = useStyles2(getStyles); + + const { + data: ruleGroup, + error, + isLoading, + } = useRuleGroupDefinition({ + group: ruleGroupIdentifier.groupName, + namespace: ruleGroupIdentifier.namespaceName, + rulerConfig, + }); + + const loadingText = t('alerting.common.loading', 'Loading...'); + + return ( + + {isLoading && } + {error ? stringifyErrorLike(error) : null} + {ruleGroup && } + + ); +} + +export function EditRuleGroupModalForm(props: ModalFormProps): React.ReactElement { + const { ruleGroup, ruleGroupIdentifier, folderTitle, onClose, intervalEditOnly } = props; const styles = useStyles2(getStyles); const notifyApp = useAppNotification(); @@ -200,32 +256,21 @@ export function EditRuleGroupModal(props: ModalProps): React.ReactElement { const defaultValues = useMemo( (): FormValues => ({ - namespaceName: decodeGrafanaNamespace(namespace).name, - groupName: group.name, - groupInterval: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL, + namespaceName: ruleGroupIdentifier.namespaceName, + groupName: ruleGroupIdentifier.groupName, + groupInterval: ruleGroup?.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL, }), - [namespace, group.name, group.interval] + [ruleGroup?.interval, ruleGroupIdentifier.groupName, ruleGroupIdentifier.namespaceName] ); - const rulesSourceName = getRulesSourceName(namespace.rulesSource); + const rulesSourceName = ruleGroupIdentifier.dataSourceName; const isGrafanaManagedGroup = rulesSourceName === GRAFANA_RULES_SOURCE_NAME; - // parse any parent folders the alert rule might be stored in - const nestedFolderParents = decodeGrafanaNamespace(namespace).parents; - const nameSpaceLabel = isGrafanaManagedGroup ? 'Folder' : 'Namespace'; const onSubmit = async (values: FormValues) => { - const ruleGroupIdentifier: RuleGroupIdentifier = { - dataSourceName: rulesSourceName, - groupName: group.name, - namespaceName: isGrafanaManagedGroup ? folderUid! : namespace.name, - }; - // make sure that when dealing with a nested folder for Grafana managed rules we encode the folder properly - const updatedNamespaceName = isGrafanaManagedGroup - ? encodeGrafanaNamespace(values.namespaceName, nestedFolderParents) - : values.namespaceName; + const updatedNamespaceName = values.namespaceName; const updatedGroupName = values.groupName; const updatedInterval = values.groupInterval; @@ -266,136 +311,133 @@ export function EditRuleGroupModal(props: ModalProps): React.ReactElement { }; const rulesWithoutRecordingRules = compact( - group.rules.map((r) => r.rulerRule).filter((rule) => !isGrafanaOrDataSourceRecordingRule(rule)) + ruleGroup?.rules.filter((rule) => !isGrafanaOrDataSourceRecordingRule(rule)) ); const hasSomeNoRecordingRules = rulesWithoutRecordingRules.length > 0; - const modalTitle = - intervalEditOnly || isGrafanaManagedGroup ? 'Edit evaluation group' : 'Edit namespace or evaluation group'; return ( - - -
- <> - {!props.hideFolder && ( - - - {nameSpaceLabel} - - } - invalid={Boolean(errors.namespaceName) ? true : undefined} - error={errors.namespaceName?.message} - > - - - {isGrafanaManagedGroup && props.folderUrl && ( - - )} - - )} - - Evaluation group - - } - invalid={!!errors.groupName} - error={errors.groupName?.message} - > - - - - Evaluation interval - - } - invalid={Boolean(errors.groupInterval) ? true : undefined} - error={errors.groupInterval?.message} - > - + + + <> + {!props.hideFolder && ( + + + {nameSpaceLabel} + + } + invalid={Boolean(errors.namespaceName) ? true : undefined} + error={errors.namespaceName?.message} + > - setValue('groupInterval', value, { shouldValidate: true, shouldDirty: true })} - /> - - - - {/* if we're dealing with a Grafana-managed group, check if the evaluation interval is valid / permitted */} - {isGrafanaManagedGroup && checkEvaluationIntervalGlobalLimit(watch('groupInterval')).exceedsLimit && ( - - )} - - {!hasSomeNoRecordingRules &&
This group does not contain alert rules.
} - {hasSomeNoRecordingRules && ( - <> -
List of rules that belong to this group
-
- #Eval column represents the number of evaluations needed before alert starts firing. -
- - - )} - {error && {stringifyErrorLike(error)}} -
- - - - -
- - -
-
+ icon="folder-open" + target="_blank" + /> + )} +
+ )} + + Evaluation group + + } + invalid={!!errors.groupName} + error={errors.groupName?.message} + > + + + + Evaluation interval + + } + invalid={Boolean(errors.groupInterval) ? true : undefined} + error={errors.groupInterval?.message} + > + + + setValue('groupInterval', value, { shouldValidate: true, shouldDirty: true })} + /> + + + + {/* if we're dealing with a Grafana-managed group, check if the evaluation interval is valid / permitted */} + {isGrafanaManagedGroup && checkEvaluationIntervalGlobalLimit(watch('groupInterval')).exceedsLimit && ( + + )} + + {!hasSomeNoRecordingRules &&
This group does not contain alert rules.
} + {hasSomeNoRecordingRules && ( + <> +
List of rules that belong to this group
+
+ #Eval column represents the number of evaluations needed before alert starts firing. +
+ + + )} + {error && {stringifyErrorLike(error)}} +
+ + + + +
+ + + ); } diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx index d79e630ffb5..9baab7600cd 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx @@ -7,12 +7,14 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; import { AccessControlAction } from 'app/types'; -import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; +import { CombinedRuleGroup, CombinedRuleNamespace, RulerDataSourceConfig } from 'app/types/unified-alerting'; import * as analytics from '../../Analytics'; +import { GRAFANA_RULER_CONFIG } from '../../api/featureDiscoveryApi'; import { useHasRuler } from '../../hooks/useHasRuler'; import { mockExportApi, mockFolderApi, setupMswServer } from '../../mockApi'; import { grantUserPermissions, mockCombinedRule, mockDataSource, mockFolder, mockGrafanaRulerRule } from '../../mocks'; +import { mimirDataSource } from '../../mocks/server/configure'; import { RulesGroup } from './RulesGroup'; @@ -38,10 +40,10 @@ const mocks = { useHasRuler: jest.mocked(useHasRuler), }; -function mockUseHasRuler(hasRuler: boolean, rulerRulesLoaded: boolean) { +function mockUseHasRuler(hasRuler: boolean, rulerConfig: RulerDataSourceConfig) { mocks.useHasRuler.mockReturnValue({ hasRuler, - rulerRulesLoaded, + rulerConfig, }); } @@ -107,7 +109,7 @@ describe('Rules group tests', () => { it('Should hide delete and edit group buttons', async () => { // Act - mockUseHasRuler(true, true); + mockUseHasRuler(true, GRAFANA_RULER_CONFIG); mockFolderApi(server).folder('cpu-usage', mockFolder({ uid: 'cpu-usage', canSave: false })); renderRulesGroup(namespace, group); expect(await screen.findByTestId('rule-group')).toBeInTheDocument(); @@ -119,7 +121,7 @@ describe('Rules group tests', () => { it('Should allow exporting rules group', async () => { // Arrange - mockUseHasRuler(true, true); + mockUseHasRuler(true, GRAFANA_RULER_CONFIG); mockFolderApi(server).folder('cpu-usage', mockFolder({ uid: 'cpu-usage' })); mockExportApi(server).exportRulesGroup('cpu-usage', 'TestGroup', { yaml: 'Yaml Export Content', @@ -151,6 +153,8 @@ describe('Rules group tests', () => { }); describe('Cloud rules', () => { + const { rulerConfig } = mimirDataSource(); + beforeEach(() => { contextSrv.isEditor = true; }); @@ -169,7 +173,7 @@ describe('Rules group tests', () => { it('When ruler enabled should display delete and edit group buttons', () => { // Arrange - mockUseHasRuler(true, true); + mockUseHasRuler(true, rulerConfig); // Act renderRulesGroup(namespace, group); @@ -182,7 +186,7 @@ describe('Rules group tests', () => { it('When ruler disabled should hide delete and edit group buttons', () => { // Arrange - mockUseHasRuler(false, false); + mockUseHasRuler(false, rulerConfig); // Act renderRulesGroup(namespace, group); @@ -195,7 +199,7 @@ describe('Rules group tests', () => { it('Delete button click should display confirmation modal', async () => { // Arrange - mockUseHasRuler(true, true); + mockUseHasRuler(true, rulerConfig); // Act renderRulesGroup(namespace, group); @@ -206,36 +210,4 @@ describe('Rules group tests', () => { expect(ui.confirmDeleteModal.confirmButton.get()).toBeInTheDocument(); }); }); - - describe('Analytics', () => { - beforeEach(() => { - contextSrv.isEditor = true; - }); - - const group: CombinedRuleGroup = { - name: 'TestGroup', - rules: [mockCombinedRule()], - totals: {}, - }; - - const namespace: CombinedRuleNamespace = { - name: 'TestNamespace', - rulesSource: mockDataSource(), - groups: [group], - }; - - it('Should log info when closing the edit group rule modal without saving', async () => { - mockUseHasRuler(true, true); - renderRulesGroup(namespace, group); - - await userEvent.click(ui.editGroupButton.get()); - - expect(screen.getByText('Cancel')).toBeInTheDocument(); - - await userEvent.click(screen.getByText('Cancel')); - - expect(screen.queryByText('Cancel')).not.toBeInTheDocument(); - expect(analytics.logInfo).toHaveBeenCalledWith(analytics.LogMessages.leavingRuleGroupEdit); - }); - }); }); diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index b1a5bddd43c..ccf725b5f99 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import pluralize from 'pluralize'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -26,7 +26,7 @@ import { ActionIcon } from './ActionIcon'; import { EditRuleGroupModal } from './EditRuleGroupModal'; import { ReorderCloudGroupModal } from './ReorderRuleGroupModal'; import { RuleGroupStats } from './RuleStats'; -import { RulesTable } from './RulesTable'; +import { RulesTable, useIsRulesLoading } from './RulesTable'; type ViewMode = 'grouped' | 'list'; @@ -42,6 +42,7 @@ const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi; export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: Props) => { const { rulesSource } = namespace; const rulesSourceName = getRulesSourceName(rulesSource); + const rulerRulesLoaded = useIsRulesLoading(rulesSource); const [deleteRuleGroup] = useDeleteRuleGroup(); const styles = useStyles2(getStyles); @@ -58,7 +59,7 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: setIsCollapsed(!expandAll); }, [expandAll]); - const { hasRuler, rulerRulesLoaded } = useHasRuler(namespace.rulesSource); + const { hasRuler, rulerConfig } = useHasRuler(namespace.rulesSource); const { currentData: dsFeatures } = useDiscoverDsFeaturesQuery({ rulesSourceName }); const rulerRule = group.rules[0]?.rulerRule; @@ -78,12 +79,15 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: const isListView = viewMode === 'list'; const isGroupView = viewMode === 'grouped'; - const deleteGroup = async () => { - const namespaceName = decodeGrafanaNamespace(namespace).name; + const ruleGroupIdentifier = useMemo(() => { + const namespaceName = namespace.uid ?? namespace.name; const groupName = group.name; const dataSourceName = getRulesSourceName(namespace.rulesSource); - const ruleGroupIdentifier: RuleGroupIdentifier = { namespaceName, groupName, dataSourceName }; + return { namespaceName, groupName, dataSourceName }; + }, [namespace, group.name]); + + const deleteGroup = async () => { await deleteRuleGroup.execute(ruleGroupIdentifier); setIsDeletingGroup(false); }; @@ -274,13 +278,13 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: rules={group.rules} /> )} - {isEditingGroup && ( + {isEditingGroup && rulerConfig && ( closeEditModal()} folderUrl={folder?.canEdit ? makeFolderSettingsLink(folder.uid) : undefined} - folderUid={folderUID} /> )} {isReorderingGroup && dsFeatures?.rulerConfig && ( diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx index 38eb1045e40..1f7c6c44b11 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx @@ -4,7 +4,7 @@ import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2 } from '@grafana/data'; import { Pagination, Tooltip, useStyles2 } from '@grafana/ui'; -import { CombinedRule } from 'app/types/unified-alerting'; +import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants'; import { alertRuleApi } from '../../api/alertRuleApi'; @@ -14,6 +14,7 @@ import { useAsync } from '../../hooks/useAsync'; import { attachRulerRuleToCombinedRule } from '../../hooks/useCombinedRuleNamespaces'; import { useHasRuler } from '../../hooks/useHasRuler'; import { usePagination } from '../../hooks/usePagination'; +import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { PluginOriginBadge } from '../../plugins/PluginOriginBadge'; import { calculateNextEvaluationEstimate } from '../../rule-list/components/util'; import { Annotation } from '../../utils/constants'; @@ -328,8 +329,20 @@ function RuleActionsCell({ rule, isLoadingRuler }: { rule: CombinedRule; isLoadi ); } +export function useIsRulesLoading(rulesSource: RulesSource) { + const rulerRules = useUnifiedAlertingSelector((state) => state.rulerRules); + const rulesSourceName = getRulesSourceName(rulesSource); + + const rulerRulesLoaded = Boolean(rulerRules[rulesSourceName]?.result); + return rulerRulesLoaded; +} + function useRuleStatus(rule: CombinedRule) { - const { hasRuler, rulerRulesLoaded } = useHasRuler(rule.namespace.rulesSource); + const rulesSource = rule.namespace.rulesSource; + + const rulerRulesLoaded = useIsRulesLoading(rulesSource); + const { hasRuler } = useHasRuler(rulesSource); + const { promRule, rulerRule } = rule; // If prometheusRulesPrimary is enabled, we don't fetch rules from the Ruler API (except for Grafana managed rules) diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts b/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts index f7c54c1121b..6d8d890e737 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts +++ b/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts @@ -7,7 +7,7 @@ import { alertRuleApi } from '../../api/alertRuleApi'; import { featureDiscoveryApi } from '../../api/featureDiscoveryApi'; import { notFoundToNullOrThrow } from '../../api/util'; import { ruleGroupReducer } from '../../reducers/ruler/ruleGroups'; -import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form'; +import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../rule-editor/formDefaults'; const { useLazyGetRuleGroupForNamespaceQuery } = alertRuleApi; const { useLazyDiscoverDsFeaturesQuery } = featureDiscoveryApi; diff --git a/public/app/features/alerting/unified/hooks/useHasRuler.ts b/public/app/features/alerting/unified/hooks/useHasRuler.ts index db2e85f09ee..399aca6081f 100644 --- a/public/app/features/alerting/unified/hooks/useHasRuler.ts +++ b/public/app/features/alerting/unified/hooks/useHasRuler.ts @@ -3,19 +3,14 @@ import { RulesSource } from 'app/types/unified-alerting'; import { featureDiscoveryApi } from '../api/featureDiscoveryApi'; import { getRulesSourceName } from '../utils/datasource'; -import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector'; - const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi; // datasource has ruler if the discovery api returns a rulerConfig export function useHasRuler(rulesSource: RulesSource) { - const rulerRules = useUnifiedAlertingSelector((state) => state.rulerRules); const rulesSourceName = getRulesSourceName(rulesSource); const { currentData: dsFeatures } = useDiscoverDsFeaturesQuery({ rulesSourceName }); - const hasRuler = Boolean(dsFeatures?.rulerConfig); - const rulerRulesLoaded = Boolean(rulerRules[rulesSourceName]?.result); - return { hasRuler, rulerRulesLoaded }; + return { hasRuler, rulerConfig: dsFeatures?.rulerConfig }; } diff --git a/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts b/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts index 7b50599934c..5e6daa81e8b 100644 --- a/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts +++ b/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts @@ -4,6 +4,9 @@ import { StoreState, useSelector } from 'app/types'; import { UnifiedAlertingState } from '../state/reducers'; +/** + * @deprecated: DO NOT USE THIS; when using this you are INCORRECTLY assuming that we already have dispatched an action to populate the redux store values + */ export function useUnifiedAlertingSelector( selector: (state: UnifiedAlertingState) => TSelected, equalityFn?: (left: TSelected, right: TSelected) => boolean diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 5676dbb9a04..278178b395e 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -196,15 +196,12 @@ export const mockRulerAlertingRule = (partial: Partial = { ...partial, }); -export const mockRulerRecordingRule = (partial: Partial = {}): RulerAlertingRuleDTO => ({ - alert: 'alert1', +export const mockRulerRecordingRule = (partial: Partial = {}): RulerRecordingRuleDTO => ({ + record: 'alert1', expr: 'up = 1', labels: { severity: 'warning', }, - annotations: { - summary: 'test alert', - }, ...partial, }); @@ -735,7 +732,7 @@ export function mockStore(recipe: (state: StoreState) => void) { return configureStore(produce(defaultState, recipe)); } -export function mockAlertQuery(query: Partial): AlertQuery { +export function mockAlertQuery(query: Partial = {}): AlertQuery { return { datasourceUid: '--uid--', refId: 'A', diff --git a/public/app/features/alerting/unified/mocks/server/configure.ts b/public/app/features/alerting/unified/mocks/server/configure.ts index 063a070a10f..bc638438e14 100644 --- a/public/app/features/alerting/unified/mocks/server/configure.ts +++ b/public/app/features/alerting/unified/mocks/server/configure.ts @@ -20,6 +20,7 @@ import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridg import { clearPluginSettingsCache } from 'app/features/plugins/pluginSettings'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; import { FolderDTO } from 'app/types'; +import { RulerDataSourceConfig } from 'app/types/unified-alerting'; import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { setupDataSources } from '../../testSetup/datasources'; @@ -108,9 +109,15 @@ export function mimirDataSource() { { alerting: true, module: 'core:plugin/prometheus' } ); + const rulerConfig: RulerDataSourceConfig = { + apiVersion: 'config', + dataSourceUid: dataSource.uid, + dataSourceName: dataSource.name, + }; + setupDataSources(dataSource); - return { dataSource }; + return { dataSource, rulerConfig }; } export function setPrometheusRules(ds: DataSourceInstanceSettings, groups: PromRuleGroupDTO[]) { diff --git a/public/app/features/alerting/unified/mocks/server/db.ts b/public/app/features/alerting/unified/mocks/server/db.ts index 55e76f78208..ba78b0be31a 100644 --- a/public/app/features/alerting/unified/mocks/server/db.ts +++ b/public/app/features/alerting/unified/mocks/server/db.ts @@ -1,18 +1,23 @@ import { Factory } from 'fishery'; +import { uniqueId } from 'lodash'; import { DataSourceInstanceSettings, PluginType } from '@grafana/data'; import { config, setDataSourceSrv } from '@grafana/runtime'; +import { FolderDTO } from 'app/types'; import { PromAlertingRuleDTO, PromAlertingRuleState, PromRuleGroupDTO, PromRuleType, + RulerAlertingRuleDTO, + RulerRecordingRuleDTO, + RulerRuleGroupDTO, } from 'app/types/unified-alerting-dto'; import { MockDataSourceSrv } from '../../mocks'; import { DataSourceType } from '../../utils/datasource'; -const ruleFactory = Factory.define(({ sequence }) => ({ +const prometheusRuleFactory = Factory.define(({ sequence }) => ({ name: `test-rule-${sequence}`, query: 'test-query', state: PromAlertingRuleState.Inactive, @@ -21,15 +26,35 @@ const ruleFactory = Factory.define(({ sequence }) => ({ labels: { team: 'infra' }, })); -const groupFactory = Factory.define(({ sequence }) => { +const rulerAlertingRuleFactory = Factory.define(({ sequence }) => ({ + alert: `ruler-alerting-rule-${sequence}`, + expr: 'vector(0)', + annotations: { 'annotation-key-1': 'annotation-value-1' }, + labels: { 'label-key-1': 'label-value-1' }, + for: '5m', +})); + +const rulerRecordingRuleFactory = Factory.define(({ sequence }) => ({ + record: `ruler-recording-rule-${sequence}`, + expr: 'vector(0)', + labels: { 'label-key-1': 'label-value-1' }, +})); + +const rulerRuleGroupFactory = Factory.define(({ sequence }) => ({ + name: `ruler-rule-group-${sequence}`, + rules: [], + interval: '1m', +})); + +const prometheusRuleGroupFactory = Factory.define(({ sequence }) => { const group = { name: `test-group-${sequence}`, file: `test-namespace`, interval: 10, - rules: ruleFactory.buildList(10), + rules: prometheusRuleFactory.buildList(10), }; - ruleFactory.rewindSequence(); + prometheusRuleFactory.rewindSequence(); return group; }); @@ -72,8 +97,33 @@ const dataSourceFactory = Factory.define(({ sequence }; }); +const grafanaFolderFactory = Factory.define(({ sequence }) => ({ + id: sequence, + uid: uniqueId(), + title: `Mock Folder ${sequence}`, + version: 1, + url: '', + canAdmin: true, + canDelete: true, + canEdit: true, + canSave: true, + created: '', + createdBy: '', + hasAcl: false, + updated: '', + updatedBy: '', +})); + export const alertingFactory = { - group: groupFactory, - rule: ruleFactory, + folder: grafanaFolderFactory, + prometheus: { + group: prometheusRuleGroupFactory, + rule: prometheusRuleFactory, + }, + ruler: { + group: rulerRuleGroupFactory, + alertingRule: rulerAlertingRuleFactory, + recordingRule: rulerRecordingRuleFactory, + }, dataSource: dataSourceFactory, }; diff --git a/public/app/features/alerting/unified/reducers/ruler/__snapshots__/ruleGroups.test.ts.snap b/public/app/features/alerting/unified/reducers/ruler/__snapshots__/ruleGroups.test.ts.snap index 95329f16627..42aac7884e8 100644 --- a/public/app/features/alerting/unified/reducers/ruler/__snapshots__/ruleGroups.test.ts.snap +++ b/public/app/features/alerting/unified/reducers/ruler/__snapshots__/ruleGroups.test.ts.snap @@ -104,10 +104,6 @@ exports[`removing a rule should remove a Data source managed ruler rule without }, }, { - "alert": "alert1", - "annotations": { - "summary": "test alert", - }, "expr": "up = 1", "labels": { "severity": "warning", diff --git a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx similarity index 93% rename from public/app/features/alerting/unified/CloneRuleEditor.test.tsx rename to public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx index 7399bcae26b..298be9b1529 100644 --- a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx @@ -4,36 +4,36 @@ import { getWrapper, render, waitFor, waitForElementToBeRemoved, within } from ' import { byRole, byTestId, byText } from 'testing-library-selector'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; +import { AccessControlAction } from 'app/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; - -import { AccessControlAction } from '../../../types'; import { RulerAlertingRuleDTO, RulerGrafanaRuleDTO, RulerRecordingRuleDTO, RulerRuleDTO, -} from '../../../types/unified-alerting-dto'; +} from 'app/types/unified-alerting-dto'; -import { CloneRuleEditor, cloneRuleDefinition } from './CloneRuleEditor'; -import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; -import { setupMswServer } from './mockApi'; +import { ExpressionEditorProps } from '../components/rule-editor/ExpressionEditor'; +import { setupMswServer } from '../mockApi'; import { grantUserPermissions, mockDataSource, mockRulerAlertingRule, mockRulerGrafanaRule, mockRulerRuleGroup, -} from './mocks'; -import { grafanaRulerRule } from './mocks/grafanaRulerApi'; -import { mockRulerRulesApiResponse, mockRulerRulesGroupApiResponse } from './mocks/rulerApi'; -import { AlertingQueryRunner } from './state/AlertingQueryRunner'; -import { setupDataSources } from './testSetup/datasources'; -import { RuleFormValues } from './types/rule-form'; -import { Annotation } from './utils/constants'; -import { getDefaultFormValues } from './utils/rule-form'; -import { hashRulerRule } from './utils/rule-id'; +} from '../mocks'; +import { grafanaRulerRule } from '../mocks/grafanaRulerApi'; +import { mockRulerRulesApiResponse, mockRulerRulesGroupApiResponse } from '../mocks/rulerApi'; +import { AlertingQueryRunner } from '../state/AlertingQueryRunner'; +import { setupDataSources } from '../testSetup/datasources'; +import { RuleFormValues } from '../types/rule-form'; +import { Annotation } from '../utils/constants'; +import { hashRulerRule } from '../utils/rule-id'; -jest.mock('./components/rule-editor/ExpressionEditor', () => ({ +import { CloneRuleEditor, cloneRuleDefinition } from './CloneRuleEditor'; +import { getDefaultFormValues } from './formDefaults'; + +jest.mock('../components/rule-editor/ExpressionEditor', () => ({ // eslint-disable-next-line react/display-name ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( onChange(e.target.value)} /> diff --git a/public/app/features/alerting/unified/CloneRuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.tsx similarity index 72% rename from public/app/features/alerting/unified/CloneRuleEditor.tsx rename to public/app/features/alerting/unified/rule-editor/CloneRuleEditor.tsx index 92745f20bde..e92ad4b46e6 100644 --- a/public/app/features/alerting/unified/CloneRuleEditor.tsx +++ b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.tsx @@ -1,18 +1,17 @@ import { cloneDeep } from 'lodash'; -import { locationService } from '@grafana/runtime/src'; -import { Alert, LoadingPlaceholder } from '@grafana/ui/src'; +import { locationService } from '@grafana/runtime'; +import { Alert, LoadingPlaceholder } from '@grafana/ui'; +import { RuleIdentifier, RuleWithLocation } from 'app/types/unified-alerting'; +import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; -import { RuleIdentifier, RuleWithLocation } from '../../../types/unified-alerting'; -import { RulerRuleDTO } from '../../../types/unified-alerting-dto'; - -import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm'; -import { useRuleWithLocation } from './hooks/useCombinedRule'; -import { generateCopiedName } from './utils/duplicate'; -import { stringifyErrorLike } from './utils/misc'; -import { rulerRuleToFormValues } from './utils/rule-form'; -import { getRuleName, isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './utils/rules'; -import { createRelativeUrl } from './utils/url'; +import { AlertRuleForm } from '../components/rule-editor/alert-rule-form/AlertRuleForm'; +import { useRuleWithLocation } from '../hooks/useCombinedRule'; +import { generateCopiedName } from '../utils/duplicate'; +import { stringifyErrorLike } from '../utils/misc'; +import { rulerRuleToFormValues } from '../utils/rule-form'; +import { getRuleName, isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from '../utils/rules'; +import { createRelativeUrl } from '../utils/url'; export function CloneRuleEditor({ sourceRuleId }: { sourceRuleId: RuleIdentifier }) { const { loading, result: rule, error } = useRuleWithLocation({ ruleIdentifier: sourceRuleId }); diff --git a/public/app/features/alerting/unified/ExistingRuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx similarity index 71% rename from public/app/features/alerting/unified/ExistingRuleEditor.tsx rename to public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx index 17dc0d74753..7d1b9d92fcf 100644 --- a/public/app/features/alerting/unified/ExistingRuleEditor.tsx +++ b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx @@ -1,19 +1,18 @@ import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { RuleIdentifier } from 'app/types/unified-alerting'; -import { AlertWarning } from './AlertWarning'; -import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm'; -import { useRuleWithLocation } from './hooks/useCombinedRule'; -import { useIsRuleEditable } from './hooks/useIsRuleEditable'; -import { stringifyErrorLike } from './utils/misc'; -import * as ruleId from './utils/rule-id'; +import { AlertWarning } from '../AlertWarning'; +import { AlertRuleForm } from '../components/rule-editor/alert-rule-form/AlertRuleForm'; +import { useRuleWithLocation } from '../hooks/useCombinedRule'; +import { useIsRuleEditable } from '../hooks/useIsRuleEditable'; +import { stringifyErrorLike } from '../utils/misc'; +import * as ruleId from '../utils/rule-id'; interface ExistingRuleEditorProps { identifier: RuleIdentifier; - id?: string; } -export function ExistingRuleEditor({ identifier, id }: ExistingRuleEditorProps) { +export function ExistingRuleEditor({ identifier }: ExistingRuleEditorProps) { const { loading: loadingAlertRule, result: ruleWithLocation, diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx similarity index 69% rename from public/app/features/alerting/unified/RuleEditor.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditor.tsx index bdda3e5d28b..b5adba57cf2 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx @@ -5,14 +5,16 @@ import { NavModelItem } from '@grafana/data'; import { withErrorBoundary } from '@grafana/ui'; import { RuleIdentifier } from 'app/types/unified-alerting'; -import { AlertWarning } from './AlertWarning'; +import { AlertWarning } from '../AlertWarning'; +import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; +import { AlertRuleForm } from '../components/rule-editor/alert-rule-form/AlertRuleForm'; +import { useURLSearchParams } from '../hooks/useURLSearchParams'; +import { useRulesAccess } from '../utils/accessControlHooks'; +import * as ruleId from '../utils/rule-id'; + import { CloneRuleEditor } from './CloneRuleEditor'; import { ExistingRuleEditor } from './ExistingRuleEditor'; -import { AlertingPageWrapper } from './components/AlertingPageWrapper'; -import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm'; -import { useURLSearchParams } from './hooks/useURLSearchParams'; -import { useRulesAccess } from './utils/accessControlHooks'; -import * as ruleId from './utils/rule-id'; +import { formValuesFromQueryParams, translateRouteParamToRuleType } from './formDefaults'; type RuleEditorPathParams = { id?: string; @@ -44,14 +46,8 @@ const getPageNav = (identifier?: RuleIdentifier, type?: RuleEditorPathParams['ty }; const RuleEditor = () => { - const [searchParams] = useURLSearchParams(); - const params = useParams(); - const { type } = params; - const id = ruleId.getRuleIdFromPathname(params); - const identifier = ruleId.tryParse(id, true); - - const copyFromId = searchParams.get('copyFrom') ?? undefined; - const copyFromIdentifier = ruleId.tryParse(copyFromId); + const { identifier, type } = useRuleEditorPathParams(); + const { copyFromIdentifier, queryDefaults } = useRuleEditorQueryParams(); const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess(); @@ -65,15 +61,15 @@ const RuleEditor = () => { } if (identifier) { - return ; + return ; } if (copyFromIdentifier) { return ; } // new alert rule - return ; - }, [canCreateCloudRules, canCreateGrafanaRules, canEditRules, copyFromIdentifier, id, identifier]); + return ; + }, [canCreateCloudRules, canCreateGrafanaRules, canEditRules, copyFromIdentifier, identifier, queryDefaults]); return ( @@ -83,3 +79,28 @@ const RuleEditor = () => { }; export default withErrorBoundary(RuleEditor, { style: 'page' }); + +function useRuleEditorPathParams() { + const params = useParams(); + const { type } = params; + const id = ruleId.getRuleIdFromPathname(params); + const identifier = ruleId.tryParse(id, true); + + return { identifier, type }; +} + +function useRuleEditorQueryParams() { + const { type } = useParams(); + + const [searchParams] = useURLSearchParams(); + const copyFromId = searchParams.get('copyFrom') ?? undefined; + const copyFromIdentifier = ruleId.tryParse(copyFromId); + + const ruleType = translateRouteParamToRuleType(type); + + const queryDefaults = searchParams.has('defaults') + ? formValuesFromQueryParams(searchParams.get('defaults') ?? '', ruleType) + : undefined; + + return { copyFromIdentifier, queryDefaults }; +} diff --git a/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx similarity index 88% rename from public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx index e99c56856d2..3671d13d232 100644 --- a/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx @@ -6,24 +6,24 @@ import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types'; import { PromApiFeatures, PromApplication } from 'app/types/unified-alerting-dto'; -import { discoverFeaturesByUid } from './api/buildInfo'; -import { fetchRulerRulesGroup } from './api/ruler'; -import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; -import { setupMswServer } from './mockApi'; -import { grantUserPermissions, mockDataSource } from './mocks'; -import { setupDataSources } from './testSetup/datasources'; -import { DataSourceType, GRAFANA_DATASOURCE_NAME, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; +import { discoverFeaturesByUid } from '../api/buildInfo'; +import { fetchRulerRulesGroup } from '../api/ruler'; +import { ExpressionEditorProps } from '../components/rule-editor/ExpressionEditor'; +import { setupMswServer } from '../mockApi'; +import { grantUserPermissions, mockDataSource } from '../mocks'; +import { setupDataSources } from '../testSetup/datasources'; +import { DataSourceType, GRAFANA_DATASOURCE_NAME, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; -jest.mock('./components/rule-editor/ExpressionEditor', () => ({ +jest.mock('../components/rule-editor/ExpressionEditor', () => ({ // eslint-disable-next-line react/display-name ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( onChange(e.target.value)} /> ), })); -jest.mock('./api/buildInfo'); -jest.mock('./api/ruler', () => ({ - rulerUrlBuilder: jest.requireActual('./api/ruler').rulerUrlBuilder, +jest.mock('../api/buildInfo'); +jest.mock('../api/ruler', () => ({ + rulerUrlBuilder: jest.requireActual('../api/ruler').rulerUrlBuilder, fetchRulerRules: jest.fn(), fetchRulerRulesGroup: jest.fn(), fetchRulerRulesNamespace: jest.fn(), @@ -36,8 +36,8 @@ jest.mock('app/features/query/components/QueryEditorRow', () => ({ QueryEditorRow: () =>

hi

, })); -jest.mock('./components/rule-editor/util', () => { - const originalModule = jest.requireActual('./components/rule-editor/util'); +jest.mock('../components/rule-editor/util', () => { + const originalModule = jest.requireActual('../components/rule-editor/util'); return { ...originalModule, getThresholdsForQueries: jest.fn(() => ({})), diff --git a/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx similarity index 85% rename from public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx index 21ba09cb12f..d70177fc7a9 100644 --- a/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx @@ -5,15 +5,15 @@ import { screen } from 'test/test-utils'; import { selectors } from '@grafana/e2e-selectors'; import { AccessControlAction } from 'app/types'; -import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; -import { setupMswServer } from './mockApi'; -import { grantUserPermissions } from './mocks'; -import { GROUP_3, NAMESPACE_2 } from './mocks/mimirRulerApi'; -import { mimirDataSource } from './mocks/server/configure'; -import { MIMIR_DATASOURCE_UID } from './mocks/server/constants'; -import { captureRequests, serializeRequests } from './mocks/server/events'; +import { ExpressionEditorProps } from '../components/rule-editor/ExpressionEditor'; +import { setupMswServer } from '../mockApi'; +import { grantUserPermissions } from '../mocks'; +import { GROUP_3, NAMESPACE_2 } from '../mocks/mimirRulerApi'; +import { mimirDataSource } from '../mocks/server/configure'; +import { MIMIR_DATASOURCE_UID } from '../mocks/server/constants'; +import { captureRequests, serializeRequests } from '../mocks/server/events'; -jest.mock('./components/rule-editor/ExpressionEditor', () => ({ +jest.mock('../components/rule-editor/ExpressionEditor', () => ({ // eslint-disable-next-line react/display-name ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( onChange(e.target.value)} /> diff --git a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx similarity index 95% rename from public/app/features/alerting/unified/RuleEditorExisting.test.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx index 15ecae4e3ec..1781ab75d2b 100644 --- a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx @@ -7,15 +7,15 @@ import { setFolderResponse } from 'app/features/alerting/unified/mocks/server/co import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; import { DashboardSearchItemType } from 'app/features/search/types'; +import { AccessControlAction } from 'app/types'; -import { AccessControlAction } from '../../../types'; +import { setupMswServer } from '../mockApi'; +import { grantUserPermissions, mockDataSource, mockFolder } from '../mocks'; +import { grafanaRulerRule } from '../mocks/grafanaRulerApi'; +import { setupDataSources } from '../testSetup/datasources'; +import { Annotation } from '../utils/constants'; import RuleEditor from './RuleEditor'; -import { setupMswServer } from './mockApi'; -import { grantUserPermissions, mockDataSource, mockFolder } from './mocks'; -import { grafanaRulerRule } from './mocks/grafanaRulerApi'; -import { setupDataSources } from './testSetup/datasources'; -import { Annotation } from './utils/constants'; jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) =>
{actions}
, diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx similarity index 94% rename from public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx index e07f62cd7c1..94888d4c9b1 100644 --- a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx @@ -9,10 +9,10 @@ import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { PROMETHEUS_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { AccessControlAction } from 'app/types'; -import { grantUserPermissions, mockDataSource } from './mocks'; -import { grafanaRulerGroup } from './mocks/grafanaRulerApi'; -import { captureRequests, serializeRequests } from './mocks/server/events'; -import { setupDataSources } from './testSetup/datasources'; +import { grantUserPermissions, mockDataSource } from '../mocks'; +import { grafanaRulerGroup } from '../mocks/grafanaRulerApi'; +import { captureRequests, serializeRequests } from '../mocks/server/events'; +import { setupDataSources } from '../testSetup/datasources'; jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) =>
{actions}
, diff --git a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx similarity index 88% rename from public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx rename to public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx index 766253febf1..8e979828aa9 100644 --- a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx @@ -7,14 +7,14 @@ import { byText } from 'testing-library-selector'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { AccessControlAction } from 'app/types'; -import { RecordingRuleEditorProps } from './components/rule-editor/RecordingRuleEditor'; -import { grantUserPermissions } from './mocks'; -import { GROUP_3, NAMESPACE_2 } from './mocks/mimirRulerApi'; -import { mimirDataSource } from './mocks/server/configure'; -import { MIMIR_DATASOURCE_UID } from './mocks/server/constants'; -import { captureRequests, serializeRequests } from './mocks/server/events'; +import { RecordingRuleEditorProps } from '../components/rule-editor/RecordingRuleEditor'; +import { grantUserPermissions } from '../mocks'; +import { GROUP_3, NAMESPACE_2 } from '../mocks/mimirRulerApi'; +import { mimirDataSource } from '../mocks/server/configure'; +import { MIMIR_DATASOURCE_UID } from '../mocks/server/constants'; +import { captureRequests, serializeRequests } from '../mocks/server/events'; -jest.mock('./components/rule-editor/RecordingRuleEditor', () => ({ +jest.mock('../components/rule-editor/RecordingRuleEditor', () => ({ RecordingRuleEditor: ({ queries, onChangeQuery }: Pick) => { const onChange = (expr: string) => { const query = queries[0]; diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorCloudRules.test.tsx.snap similarity index 100% rename from public/app/features/alerting/unified/__snapshots__/RuleEditorCloudRules.test.tsx.snap rename to public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorCloudRules.test.tsx.snap diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap similarity index 100% rename from public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap rename to public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorRecordingRule.test.tsx.snap similarity index 100% rename from public/app/features/alerting/unified/__snapshots__/RuleEditorRecordingRule.test.tsx.snap rename to public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorRecordingRule.test.tsx.snap diff --git a/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts b/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts new file mode 100644 index 00000000000..800b95a6193 --- /dev/null +++ b/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts @@ -0,0 +1,196 @@ +import { config } from '@grafana/runtime'; + +import { mockAlertQuery, mockDataSource, reduceExpression, thresholdExpression } from '../mocks'; +import { testWithFeatureToggles } from '../test/test-utils'; +import { RuleFormType } from '../types/rule-form'; +import { Annotation } from '../utils/constants'; +import { DataSourceType, getDefaultOrFirstCompatibleDataSource } from '../utils/datasource'; +import { MANUAL_ROUTING_KEY, getDefaultQueries } from '../utils/rule-form'; + +import { formValuesFromQueryParams, getDefaultFormValues, getDefautManualRouting } from './formDefaults'; +import { isAlertQueryOfAlertData } from './formProcessing'; + +jest.mock('../utils/datasource'); + +const mocks = { + getDefaultOrFirstCompatibleDataSource: jest.mocked(getDefaultOrFirstCompatibleDataSource), +}; + +// Setup mock implementation +mocks.getDefaultOrFirstCompatibleDataSource.mockReturnValue( + mockDataSource({ + type: DataSourceType.Prometheus, + }) +); + +// TODO Not sure why queries are an empty array in the default form values +const defaultFormValues = { + ...getDefaultFormValues(), + queries: getDefaultQueries(), +}; + +describe('formValuesFromQueryParams', () => { + it('should return default values when given invalid JSON', () => { + const result = formValuesFromQueryParams('invalid json', RuleFormType.grafana); + + expect(result).toEqual(defaultFormValues); + }); + + it('should normalize annotations', () => { + const ruleDefinition = JSON.stringify({ + annotations: [ + { key: 'custom', value: 'my custom annotation' }, + { key: Annotation.runbookURL, value: 'runbook annotation' }, + { key: 'custom-2', value: 'custom annotation v2' }, + { key: Annotation.summary, value: 'summary annotation' }, + { key: 'custom-3', value: 'custom annotation v3' }, + { key: Annotation.description, value: 'description annotation' }, + ], + }); + + const result = formValuesFromQueryParams(ruleDefinition, RuleFormType.grafana); + + const [summary, description, runbookURL, ...rest] = result.annotations; + + expect(summary).toEqual({ key: Annotation.summary, value: 'summary annotation' }); + expect(description).toEqual({ key: Annotation.description, value: 'description annotation' }); + expect(runbookURL).toEqual({ key: Annotation.runbookURL, value: 'runbook annotation' }); + expect(rest).toContainEqual({ key: 'custom', value: 'my custom annotation' }); + expect(rest).toContainEqual({ key: 'custom-2', value: 'custom annotation v2' }); + expect(rest).toContainEqual({ key: 'custom-3', value: 'custom annotation v3' }); + }); + + it('should disable simplified query editor when query switch mode is disabled', () => { + const result = formValuesFromQueryParams(JSON.stringify({}), RuleFormType.grafana); + + expect(result.editorSettings).toBeDefined(); + expect(result.editorSettings!.simplifiedQueryEditor).toBe(false); + }); + + describe('when simplified query editor is enabled', () => { + testWithFeatureToggles(['alertingQueryAndExpressionsStepMode']); + + it('should enable simplified query editor if queries are transformable to simple condition', () => { + const result = formValuesFromQueryParams( + JSON.stringify({ + queries: [mockAlertQuery(), reduceExpression, thresholdExpression], + }), + RuleFormType.grafana + ); + + expect(result.editorSettings).toBeDefined(); + expect(result.editorSettings!.simplifiedQueryEditor).toBe(true); + }); + + it('should disable simplified query editor if queries are not transformable to simple condition', () => { + const result = formValuesFromQueryParams( + JSON.stringify({ + queries: [mockAlertQuery(), mockAlertQuery(), thresholdExpression], + }), + RuleFormType.grafana + ); + + expect(result.editorSettings).toBeDefined(); + expect(result.editorSettings!.simplifiedQueryEditor).toBe(false); + }); + }); + + it('should default to instant queries for loki and prometheus if not specified', () => { + const result = formValuesFromQueryParams( + JSON.stringify({ + queries: [ + mockAlertQuery({ datasourceUid: 'loki', model: { refId: 'A', datasource: { type: DataSourceType.Loki } } }), + mockAlertQuery({ + datasourceUid: 'prometheus', + model: { refId: 'B', datasource: { type: DataSourceType.Prometheus } }, + }), + ], + }), + RuleFormType.grafana + ); + + const [lokiQuery, prometheusQuery] = result.queries.filter(isAlertQueryOfAlertData); + + expect(lokiQuery.model.instant).toBe(true); + expect(lokiQuery.model.range).toBe(false); + expect(prometheusQuery.model.instant).toBe(true); + expect(prometheusQuery.model.range).toBe(false); + }); + + it('should preserver instant and range values if specified', () => { + const result = formValuesFromQueryParams( + JSON.stringify({ + queries: [ + mockAlertQuery({ + datasourceUid: 'loki', + model: { refId: 'A', datasource: { type: DataSourceType.Loki }, instant: true, range: false }, + }), + mockAlertQuery({ + datasourceUid: 'prometheus', + model: { refId: 'B', datasource: { type: DataSourceType.Prometheus }, instant: false, range: true }, + }), + ], + }), + RuleFormType.grafana + ); + + const [lokiQuery, prometheusQuery] = result.queries.filter(isAlertQueryOfAlertData); + + expect(lokiQuery.model.instant).toBe(true); + expect(lokiQuery.model.range).toBe(false); + expect(prometheusQuery.model.range).toBe(true); + expect(prometheusQuery.model.instant).toBe(false); + }); + + it('should reveal hidden queries', () => { + const ruleDefinition = JSON.stringify({ + queries: [ + { refId: 'A', model: { refId: 'A', hide: true } }, + { refId: 'B', model: { refId: 'B', hide: false } }, + { refId: 'C', model: { refId: 'C' } }, + ], + }); + + const result = formValuesFromQueryParams(ruleDefinition, RuleFormType.grafana); + + expect(result.queries.length).toBe(3); + + const [q1, q2, q3] = result.queries; + expect(q1.refId).toBe('A'); + expect(q2.refId).toBe('B'); + expect(q3.refId).toBe('C'); + expect(q1.model).not.toHaveProperty('hide'); + expect(q2.model).not.toHaveProperty('hide'); + expect(q3.model).not.toHaveProperty('hide'); + }); +}); + +describe('getDefaultManualRouting', () => { + afterEach(() => { + window.localStorage.clear(); + }); + + it('returns false if the feature toggle is not enabled', () => { + config.featureToggles.alertingSimplifiedRouting = false; + expect(getDefautManualRouting()).toBe(false); + }); + + it('returns true if the feature toggle is enabled and localStorage is not set', () => { + config.featureToggles.alertingSimplifiedRouting = true; + expect(getDefautManualRouting()).toBe(true); + }); + + it('returns false if the feature toggle is enabled and localStorage is set to "false"', () => { + config.featureToggles.alertingSimplifiedRouting = true; + localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); + expect(getDefautManualRouting()).toBe(false); + }); + + it('returns true if the feature toggle is enabled and localStorage is set to any value other than "false"', () => { + config.featureToggles.alertingSimplifiedRouting = true; + localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); + expect(getDefautManualRouting()).toBe(true); + localStorage.removeItem(MANUAL_ROUTING_KEY); + expect(getDefautManualRouting()).toBe(true); + }); +}); diff --git a/public/app/features/alerting/unified/rule-editor/formDefaults.ts b/public/app/features/alerting/unified/rule-editor/formDefaults.ts new file mode 100644 index 00000000000..7c6a0c824c0 --- /dev/null +++ b/public/app/features/alerting/unified/rule-editor/formDefaults.ts @@ -0,0 +1,157 @@ +import { clamp } from 'lodash'; + +import { config } from '@grafana/runtime'; +import { RuleWithLocation } from 'app/types/unified-alerting'; +import { GrafanaAlertStateDecision, RulerRuleDTO } from 'app/types/unified-alerting-dto'; + +import { RuleFormType, RuleFormValues } from '../types/rule-form'; +// TODO Ideally all of these should be moved here +import { getRulesAccess } from '../utils/access-control'; +import { defaultAnnotations } from '../utils/constants'; +import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import { + MANUAL_ROUTING_KEY, + SIMPLIFIED_QUERY_EDITOR_KEY, + getDefaultQueries, + rulerRuleToFormValues, +} from '../utils/rule-form'; +import { isGrafanaRecordingRuleByType } from '../utils/rules'; +import { formatPrometheusDuration, safeParsePrometheusDuration } from '../utils/time'; + +import { + normalizeDefaultAnnotations, + revealHiddenQueries, + setInstantOrRange, + setQueryEditorSettings, +} from './formProcessing'; + +// even if the min interval is < 1m we should default to 1m, but allow arbitrary values for minInterval > 1m +const GROUP_EVALUATION_MIN_INTERVAL_MS = safeParsePrometheusDuration(config.unifiedAlerting?.minInterval ?? '10s'); +const GROUP_EVALUATION_INTERVAL_LOWER_BOUND = safeParsePrometheusDuration('1m'); +const GROUP_EVALUATION_INTERVAL_UPPER_BOUND = Infinity; + +export const DEFAULT_GROUP_EVALUATION_INTERVAL = formatPrometheusDuration( + clamp(GROUP_EVALUATION_MIN_INTERVAL_MS, GROUP_EVALUATION_INTERVAL_LOWER_BOUND, GROUP_EVALUATION_INTERVAL_UPPER_BOUND) +); +export const getDefaultFormValues = (): RuleFormValues => { + const { canCreateGrafanaRules, canCreateCloudRules } = getRulesAccess(); + + return Object.freeze({ + name: '', + uid: '', + labels: [{ key: '', value: '' }], + annotations: defaultAnnotations, + dataSourceName: GRAFANA_RULES_SOURCE_NAME, // let's use Grafana-managed alert rule by default + type: canCreateGrafanaRules ? RuleFormType.grafana : canCreateCloudRules ? RuleFormType.cloudAlerting : undefined, // viewers can't create prom alerts + group: '', + + // grafana + folder: undefined, + queries: [], + recordingRulesQueries: [], + condition: '', + noDataState: GrafanaAlertStateDecision.NoData, + execErrState: GrafanaAlertStateDecision.Error, + evaluateFor: DEFAULT_GROUP_EVALUATION_INTERVAL, + evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, + manualRouting: getDefautManualRouting(), // we default to true if the feature toggle is enabled and the user hasn't set local storage to false + contactPoints: {}, + overrideGrouping: false, + overrideTimings: false, + muteTimeIntervals: [], + editorSettings: getDefaultEditorSettings(), + + // cortex / loki + namespace: '', + expression: '', + forTime: 1, + forTimeUnit: 'm', + }); +}; + +export const getDefautManualRouting = () => { + // first check if feature toggle for simplified routing is enabled + const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false; + if (!simplifiedRoutingToggleEnabled) { + return false; + } + //then, check in local storage if the user has enabled simplified routing + // if it's not set, we'll default to true + const manualRouting = localStorage.getItem(MANUAL_ROUTING_KEY); + return manualRouting !== 'false'; +}; + +function getDefaultEditorSettings() { + const editorSettingsEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; + if (!editorSettingsEnabled) { + return undefined; + } + //then, check in local storage if the user has saved last rule with sections simplified + const queryEditorSettings = localStorage.getItem(SIMPLIFIED_QUERY_EDITOR_KEY); + const notificationStepSettings = localStorage.getItem(MANUAL_ROUTING_KEY); + return { + simplifiedQueryEditor: queryEditorSettings !== 'false', + simplifiedNotificationEditor: notificationStepSettings !== 'false', + }; +} + +export function formValuesFromQueryParams(ruleDefinition: string, type: RuleFormType): RuleFormValues { + let ruleFromQueryParams: Partial; + + try { + ruleFromQueryParams = JSON.parse(ruleDefinition); + } catch (err) { + return { + ...getDefaultFormValues(), + queries: getDefaultQueries(), + }; + } + + return setQueryEditorSettings( + setInstantOrRange( + revealHiddenQueries({ + ...getDefaultFormValues(), + ...ruleFromQueryParams, + annotations: normalizeDefaultAnnotations(ruleFromQueryParams.annotations ?? []), + queries: ruleFromQueryParams.queries ?? getDefaultQueries(), + type: type || RuleFormType.grafana, + evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, + }) + ) + ); +} + +export function formValuesFromPrefill(rule: Partial): RuleFormValues { + return revealHiddenQueries({ + ...getDefaultFormValues(), + ...rule, + }); +} + +export function formValuesFromExistingRule(rule: RuleWithLocation) { + return revealHiddenQueries(rulerRuleToFormValues(rule)); +} + +export function defaultFormValuesForRuleType(ruleType: RuleFormType): RuleFormValues { + return { + ...getDefaultFormValues(), + condition: 'C', + queries: getDefaultQueries(isGrafanaRecordingRuleByType(ruleType)), + type: ruleType, + evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, + }; +} + +// TODO This function is not 100% valid. There is no support for cloud form type because +// it's not valid from the path param point of view. +export function translateRouteParamToRuleType(param = ''): RuleFormType { + if (param === 'recording') { + return RuleFormType.cloudRecording; + } + + if (param === 'grafana-recording') { + return RuleFormType.grafanaRecording; + } + + return RuleFormType.grafana; +} diff --git a/public/app/features/alerting/unified/rule-editor/formProcessing.ts b/public/app/features/alerting/unified/rule-editor/formProcessing.ts new file mode 100644 index 00000000000..c9fa1951417 --- /dev/null +++ b/public/app/features/alerting/unified/rule-editor/formProcessing.ts @@ -0,0 +1,150 @@ +import { omit } from 'lodash'; + +import { config } from '@grafana/runtime'; +import { isExpressionQuery } from 'app/features/expressions/guards'; +import { ExpressionQuery, ExpressionQueryType, ReducerMode } from 'app/features/expressions/types'; +import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; + +import { SimpleConditionIdentifier } from '../components/rule-editor/query-and-alert-condition/SimpleCondition'; +import { KVObject, RuleFormValues } from '../types/rule-form'; +import { defaultAnnotations } from '../utils/constants'; +import { DataSourceType } from '../utils/datasource'; + +export function setQueryEditorSettings(values: RuleFormValues): RuleFormValues { + const isQuerySwitchModeEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; + + if (!isQuerySwitchModeEnabled) { + return { + ...values, + editorSettings: { + simplifiedQueryEditor: false, + simplifiedNotificationEditor: true, // actually it doesn't matter in this case + }, + }; + } + + // data queries only + const dataQueries = values.queries.filter((query) => !isExpressionQuery(query.model)); + + // expression queries only + const expressionQueries = values.queries.filter((query) => isExpressionQueryInAlert(query)); + + const queryParamsAreTransformable = areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries); + return { + ...values, + editorSettings: { + simplifiedQueryEditor: queryParamsAreTransformable, + simplifiedNotificationEditor: true, + }, + }; +} + +export function setInstantOrRange(values: RuleFormValues): RuleFormValues { + return { + ...values, + queries: values.queries?.map((query) => { + if (isExpressionQuery(query.model)) { + return query; + } + // data query + const defaultToInstant = + query.model.datasource?.type === DataSourceType.Loki || + query.model.datasource?.type === DataSourceType.Prometheus; + const isInstant = + 'instant' in query.model && query.model.instant !== undefined ? query.model.instant : defaultToInstant; + return { + ...query, + model: { + ...query.model, + instant: isInstant, + range: !isInstant, // we cannot have both instant and range queries in alerting + }, + }; + }), + }; +} + +export function areQueriesTransformableToSimpleCondition( + dataQueries: Array>, + expressionQueries: Array> +) { + if (dataQueries.length !== 1) { + return false; + } + const singleReduceExpressionInInstantQuery = + 'instant' in dataQueries[0].model && dataQueries[0].model.instant && expressionQueries.length === 1; + + if (expressionQueries.length !== 2 && !singleReduceExpressionInInstantQuery) { + return false; + } + + const query = dataQueries[0]; + + if (query.refId !== SimpleConditionIdentifier.queryId) { + return false; + } + + const reduceExpressionIndex = expressionQueries.findIndex( + (query) => query.model.type === ExpressionQueryType.reduce && query.refId === SimpleConditionIdentifier.reducerId + ); + const reduceExpression = expressionQueries.at(reduceExpressionIndex); + const reduceOk = + reduceExpression && + reduceExpressionIndex === 0 && + (reduceExpression.model.settings?.mode === ReducerMode.Strict || + reduceExpression.model.settings?.mode === undefined); + + const thresholdExpressionIndex = expressionQueries.findIndex( + (query) => + query.model.type === ExpressionQueryType.threshold && query.refId === SimpleConditionIdentifier.thresholdId + ); + const thresholdExpression = expressionQueries.at(thresholdExpressionIndex); + const conditions = thresholdExpression?.model.conditions ?? []; + const thresholdIndexOk = singleReduceExpressionInInstantQuery + ? thresholdExpressionIndex === 0 + : thresholdExpressionIndex === 1; + const thresholdOk = thresholdExpression && thresholdIndexOk && conditions[0]?.unloadEvaluator === undefined; + return (Boolean(reduceOk) || Boolean(singleReduceExpressionInInstantQuery)) && Boolean(thresholdOk); +} + +export function isExpressionQueryInAlert( + query: AlertQuery +): query is AlertQuery { + return isExpressionQuery(query.model); +} + +export function isAlertQueryOfAlertData( + query: AlertQuery +): query is AlertQuery { + return !isExpressionQuery(query.model); +} + +// the backend will always execute "hidden" queries, so we have no choice but to remove the property in the front-end +// to avoid confusion. The query editor shows them as "disabled" and that's a different semantic meaning. +// furthermore the "AlertingQueryRunner" calls `filterQuery` on each data source and those will skip running queries that are "hidden"." +// It seems like we have no choice but to act like "hidden" queries don't exist in alerting. +export const revealHiddenQueries = (ruleDefinition: RuleFormValues): RuleFormValues => { + return { + ...ruleDefinition, + queries: ruleDefinition.queries?.map((query) => omit(query, 'model.hide')), + }; +}; + +export function normalizeDefaultAnnotations(annotations: KVObject[]) { + const orderedAnnotations = [...annotations]; + const defaultAnnotationKeys = defaultAnnotations.map((annotation) => annotation.key); + + defaultAnnotationKeys.forEach((defaultAnnotationKey, index) => { + const fieldIndex = orderedAnnotations.findIndex((field) => field.key === defaultAnnotationKey); + + if (fieldIndex === -1) { + //add the default annotation if abstent + const emptyValue = { key: defaultAnnotationKey, value: '' }; + orderedAnnotations.splice(index, 0, emptyValue); + } else if (fieldIndex !== index) { + //move it to the correct position if present + orderedAnnotations.splice(index, 0, orderedAnnotations.splice(fieldIndex, 1)[0]); + } + }); + return orderedAnnotations; +} diff --git a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx index b436e84941a..de51e6dde8b 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx @@ -19,9 +19,9 @@ grantUserPermissions([AccessControlAction.AlertingRuleExternalRead]); setupMswServer(); -const mimirGroups = alertingFactory.group.buildList(5000, { file: 'test-mimir-namespace' }); -alertingFactory.group.rewindSequence(); -const prometheusGroups = alertingFactory.group.buildList(200, { file: 'test-prometheus-namespace' }); +const mimirGroups = alertingFactory.prometheus.group.buildList(5000, { file: 'test-mimir-namespace' }); +alertingFactory.prometheus.group.rewindSequence(); +const prometheusGroups = alertingFactory.prometheus.group.buildList(200, { file: 'test-prometheus-namespace' }); const mimirDs = alertingFactory.dataSource.build({ name: 'Mimir', uid: 'mimir' }); const prometheusDs = alertingFactory.dataSource.build({ name: 'Prometheus', uid: 'prometheus' }); diff --git a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx index adb8b83c077..953bce50ead 100644 --- a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx @@ -19,9 +19,9 @@ grantUserPermissions([AccessControlAction.AlertingRuleExternalRead]); setupMswServer(); -const mimirGroups = alertingFactory.group.buildList(500, { file: 'test-mimir-namespace' }); -alertingFactory.group.rewindSequence(); -const prometheusGroups = alertingFactory.group.buildList(130, { file: 'test-prometheus-namespace' }); +const mimirGroups = alertingFactory.prometheus.group.buildList(500, { file: 'test-mimir-namespace' }); +alertingFactory.prometheus.group.rewindSequence(); +const prometheusGroups = alertingFactory.prometheus.group.buildList(130, { file: 'test-prometheus-namespace' }); const mimirDs = alertingFactory.dataSource.build({ name: 'Mimir', uid: 'mimir' }); const prometheusDs = alertingFactory.dataSource.build({ name: 'Prometheus', uid: 'prometheus' }); diff --git a/public/app/features/alerting/unified/test/test-utils.ts b/public/app/features/alerting/unified/test/test-utils.ts index 88de7e27984..66598dea083 100644 --- a/public/app/features/alerting/unified/test/test-utils.ts +++ b/public/app/features/alerting/unified/test/test-utils.ts @@ -2,6 +2,10 @@ import { act } from '@testing-library/react'; import { FeatureToggles } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { AccessControlAction } from 'app/types'; + +import { grantUserPermissions } from '../mocks'; +import { setFolderAccessControl } from '../mocks/server/configure'; /** * Flushes out microtasks so we don't get warnings from `@floating-ui/react` @@ -47,3 +51,13 @@ export const testWithLicenseFeatures = (features: string[]) => { config.licenseInfo.enabledFeatures = originalFeatures; }); }; + +/** + * "Grants" permissions via contextSrv mock, and additionally sets folder access control + * API response to match + */ +export const grantPermissionsHelper = (permissions: AccessControlAction[]) => { + const permissionsHash = permissions.reduce((hash, permission) => ({ ...hash, [permission]: true }), {}); + grantUserPermissions(permissions); + setFolderAccessControl(permissionsHash); +}; diff --git a/public/app/features/alerting/unified/utils/rule-form.test.ts b/public/app/features/alerting/unified/utils/rule-form.test.ts index bfc7f86ea5c..80442e87984 100644 --- a/public/app/features/alerting/unified/utils/rule-form.test.ts +++ b/public/app/features/alerting/unified/utils/rule-form.test.ts @@ -1,20 +1,17 @@ import { PromQuery } from '@grafana/prometheus'; -import { config } from '@grafana/runtime'; import { GrafanaAlertStateDecision, GrafanaRuleDefinition, RulerAlertingRuleDTO } from 'app/types/unified-alerting-dto'; +import { getDefaultFormValues } from '../rule-editor/formDefaults'; import { AlertManagerManualRouting, RuleFormType, RuleFormValues } from '../types/rule-form'; import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; import { - MANUAL_ROUTING_KEY, alertingRulerRuleToRuleForm, cleanAnnotations, cleanLabels, formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, getContactPointsFromDTO, - getDefaultFormValues, - getDefautManualRouting, getNotificationSettingsForDTO, } from './rule-form'; @@ -227,36 +224,6 @@ describe('getNotificationSettingsForDTO', () => { }); }); -describe('getDefautManualRouting', () => { - afterEach(() => { - window.localStorage.clear(); - }); - - it('returns false if the feature toggle is not enabled', () => { - config.featureToggles.alertingSimplifiedRouting = false; - expect(getDefautManualRouting()).toBe(false); - }); - - it('returns true if the feature toggle is enabled and localStorage is not set', () => { - config.featureToggles.alertingSimplifiedRouting = true; - expect(getDefautManualRouting()).toBe(true); - }); - - it('returns false if the feature toggle is enabled and localStorage is set to "false"', () => { - config.featureToggles.alertingSimplifiedRouting = true; - localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); - expect(getDefautManualRouting()).toBe(false); - }); - - it('returns true if the feature toggle is enabled and localStorage is set to any value other than "false"', () => { - config.featureToggles.alertingSimplifiedRouting = true; - localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); - expect(getDefautManualRouting()).toBe(true); - localStorage.removeItem(MANUAL_ROUTING_KEY); - expect(getDefautManualRouting()).toBe(true); - }); -}); - describe('cleanAnnotations', () => { it('should remove falsy KVs', () => { const output = cleanAnnotations([{ key: '', value: '' }]); diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index de5ee071296..ecea384f9e7 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -1,5 +1,3 @@ -import { clamp, omit } from 'lodash'; - import { DataQuery, DataSourceInstanceSettings, @@ -31,7 +29,6 @@ import { AlertDataQuery, AlertQuery, Annotations, - GrafanaAlertStateDecision, GrafanaNotificationSettings, GrafanaRuleDefinition, Labels, @@ -42,6 +39,8 @@ import { } from 'app/types/unified-alerting-dto'; import { EvalFunction } from '../../state/alertDef'; +import { getDefaultFormValues } from '../rule-editor/formDefaults'; +import { normalizeDefaultAnnotations } from '../rule-editor/formProcessing'; import { AlertManagerManualRouting, ContactPoint, @@ -51,8 +50,7 @@ import { SimplifiedEditor, } from '../types/rule-form'; -import { getRulesAccess } from './access-control'; -import { Annotation, defaultAnnotations } from './constants'; +import { Annotation } from './constants'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME, @@ -68,84 +66,13 @@ import { isGrafanaRulerRule, isRecordingRulerRule, } from './rules'; -import { formatPrometheusDuration, parseInterval, safeParsePrometheusDuration } from './time'; +import { parseInterval } from './time'; export type PromOrLokiQuery = PromQuery | LokiQuery; export const MANUAL_ROUTING_KEY = 'grafana.alerting.manualRouting'; export const SIMPLIFIED_QUERY_EDITOR_KEY = 'grafana.alerting.simplifiedQueryEditor'; -// even if the min interval is < 1m we should default to 1m, but allow arbitrary values for minInterval > 1m -const GROUP_EVALUATION_MIN_INTERVAL_MS = safeParsePrometheusDuration(config.unifiedAlerting?.minInterval ?? '10s'); -const GROUP_EVALUATION_INTERVAL_LOWER_BOUND = safeParsePrometheusDuration('1m'); -const GROUP_EVALUATION_INTERVAL_UPPER_BOUND = Infinity; - -export const DEFAULT_GROUP_EVALUATION_INTERVAL = formatPrometheusDuration( - clamp(GROUP_EVALUATION_MIN_INTERVAL_MS, GROUP_EVALUATION_INTERVAL_LOWER_BOUND, GROUP_EVALUATION_INTERVAL_UPPER_BOUND) -); - -export const getDefaultFormValues = (): RuleFormValues => { - const { canCreateGrafanaRules, canCreateCloudRules } = getRulesAccess(); - - return Object.freeze({ - name: '', - uid: '', - labels: [{ key: '', value: '' }], - annotations: defaultAnnotations, - dataSourceName: GRAFANA_RULES_SOURCE_NAME, // let's use Grafana-managed alert rule by default - type: canCreateGrafanaRules ? RuleFormType.grafana : canCreateCloudRules ? RuleFormType.cloudAlerting : undefined, // viewers can't create prom alerts - group: '', - - // grafana - folder: undefined, - queries: [], - recordingRulesQueries: [], - condition: '', - noDataState: GrafanaAlertStateDecision.NoData, - execErrState: GrafanaAlertStateDecision.Error, - evaluateFor: DEFAULT_GROUP_EVALUATION_INTERVAL, - evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, - manualRouting: getDefautManualRouting(), // we default to true if the feature toggle is enabled and the user hasn't set local storage to false - contactPoints: {}, - overrideGrouping: false, - overrideTimings: false, - muteTimeIntervals: [], - editorSettings: getDefaultEditorSettings(), - - // cortex / loki - namespace: '', - expression: '', - forTime: 1, - forTimeUnit: 'm', - }); -}; - -export const getDefautManualRouting = () => { - // first check if feature toggle for simplified routing is enabled - const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false; - if (!simplifiedRoutingToggleEnabled) { - return false; - } - //then, check in local storage if the user has enabled simplified routing - // if it's not set, we'll default to true - const manualRouting = localStorage.getItem(MANUAL_ROUTING_KEY); - return manualRouting !== 'false'; -}; - -function getDefaultEditorSettings() { - const editorSettingsEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; - if (!editorSettingsEnabled) { - return undefined; - } - //then, check in local storage if the user has saved last rule with sections simplified - const queryEditorSettings = localStorage.getItem(SIMPLIFIED_QUERY_EDITOR_KEY); - const notificationStepSettings = localStorage.getItem(MANUAL_ROUTING_KEY); - return { - simplifiedQueryEditor: queryEditorSettings !== 'false', - simplifiedNotificationEditor: notificationStepSettings !== 'false', - }; -} - export function formValuesToRulerRuleDTO(values: RuleFormValues): RulerRuleDTO { const { name, expression, forTime, forTimeUnit, keepFiringForTime, keepFiringForTimeUnit, type } = values; @@ -184,26 +111,6 @@ export function listifyLabelsOrAnnotations(item: Labels | Annotations | undefine return list; } -//make sure default annotations are always shown in order even if empty -export function normalizeDefaultAnnotations(annotations: KVObject[]) { - const orderedAnnotations = [...annotations]; - const defaultAnnotationKeys = defaultAnnotations.map((annotation) => annotation.key); - - defaultAnnotationKeys.forEach((defaultAnnotationKey, index) => { - const fieldIndex = orderedAnnotations.findIndex((field) => field.key === defaultAnnotationKey); - - if (fieldIndex === -1) { - //add the default annotation if abstent - const emptyValue = { key: defaultAnnotationKey, value: '' }; - orderedAnnotations.splice(index, 0, emptyValue); - } else if (fieldIndex !== index) { - //move it to the correct position if present - orderedAnnotations.splice(index, 0, orderedAnnotations.splice(fieldIndex, 1)[0]); - } - }); - return orderedAnnotations; -} - export function getNotificationSettingsForDTO( manualRouting: boolean, contactPoints?: AlertManagerManualRouting @@ -902,21 +809,6 @@ export function isPromOrLokiQuery(model: AlertDataQuery): model is PromOrLokiQue return 'expr' in model; } -// the backend will always execute "hidden" queries, so we have no choice but to remove the property in the front-end -// to avoid confusion. The query editor shows them as "disabled" and that's a different semantic meaning. -// furthermore the "AlertingQueryRunner" calls `filterQuery` on each data source and those will skip running queries that are "hidden"." -// It seems like we have no choice but to act like "hidden" queries don't exist in alerting. -export const ignoreHiddenQueries = (ruleDefinition: RuleFormValues): RuleFormValues => { - return { - ...ruleDefinition, - queries: ruleDefinition.queries?.map((query) => omit(query, 'model.hide')), - }; -}; - -export function formValuesFromExistingRule(rule: RuleWithLocation) { - return ignoreHiddenQueries(rulerRuleToFormValues(rule)); -} - export function getInstantFromDataQuery(model: AlertDataQuery, type: string): boolean | undefined { // if the datasource is not prometheus or loki, instant is defined in the model or defaults to undefined if (type !== DataSourceType.Prometheus && type !== DataSourceType.Loki) { diff --git a/public/test/helpers/alertingRuleEditor.tsx b/public/test/helpers/alertingRuleEditor.tsx index 1f1dc3552f2..4b50dce768e 100644 --- a/public/test/helpers/alertingRuleEditor.tsx +++ b/public/test/helpers/alertingRuleEditor.tsx @@ -4,7 +4,8 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { selectors } from '@grafana/e2e-selectors'; import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; -import RuleEditor from 'app/features/alerting/unified/RuleEditor'; +import RuleEditor from 'app/features/alerting/unified/rule-editor/RuleEditor'; + export enum GrafanaRuleFormStep { Query = 2, Notification = 5, From 52a6c27d99e5d984032cc36802e7cc4dcd2bf79d Mon Sep 17 00:00:00 2001 From: Kristina Date: Tue, 28 Jan 2025 08:00:15 -0600 Subject: [PATCH 146/894] VizTooltip: Check useragent to perform consistently on all mobile devices (#99655) * Add useragent check to be comprehensive * move check outside function and add hopefully clarifying comment --- .../src/components/uPlot/plugins/TooltipPlugin2.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index cb0527a9961..b2e142d6a64 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -108,6 +108,8 @@ const maybeZoomAction = (e?: MouseEvent | null) => e != null && !e.ctrlKey && !e const getDataLinksFallback: GetDataLinksCallback = () => []; +const userAgentIsMobile = /Android|iPhone|iPad/i.test(navigator.userAgent); + /** * @alpha */ @@ -666,8 +668,9 @@ export const TooltipPlugin2 = ({ // if not viaSync, re-dispatch real event if (event != null) { - // we expect to re-dispatch mousemove, but on mobile we'll get mouseup or click - const isMobile = event.type !== 'mousemove'; + // we expect to re-dispatch mousemove, but may have a different event type, so create a mousemove event and fire that instead + // this doesn't work for every mobile device, so fall back to checking the useragent as well + const isMobile = event.type !== 'mousemove' || userAgentIsMobile; if (isMobile) { event = new MouseEvent('mousemove', { From 058d3946b7642f8450e07ffe6c7ba56d32d60bc0 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:11:10 +0100 Subject: [PATCH 147/894] Zipkin: Remove frontend query running code (#99557) * Zipkin: Remove frontend query running code * Fix lint --- .betterer.results | 3 +- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 7 -- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 1 + .../datasource/zipkin/datasource.test.ts | 77 ++++++++----------- .../plugins/datasource/zipkin/datasource.ts | 66 +++------------- 9 files changed, 48 insertions(+), 113 deletions(-) diff --git a/.betterer.results b/.betterer.results index c08d07d5459..01d1a68627b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -7550,8 +7550,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/plugins/datasource/zipkin/datasource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/plugins/datasource/zipkin/utils/transforms.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] 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 3038e1624b3..fecc19478b7 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -81,7 +81,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `userStorageAPI` | Enables the user storage API | Yes | | `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | | | `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes | -| `zipkinBackendMigration` | Enables querying Zipkin data source without the proxy | Yes | | `reportingUseRawTimeRange` | Uses the original report or dashboard time range instead of making an absolute transformation | Yes | | `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes | | `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 04d84ee202e..0a6775edac1 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -229,7 +229,6 @@ export interface FeatureToggles { exploreMetricsRelatedLogs?: boolean; prometheusSpecialCharsInLabelValues?: boolean; enableExtensionsAdminPage?: boolean; - zipkinBackendMigration?: boolean; enableSCIM?: boolean; crashDetection?: boolean; jaegerBackendMigration?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a717b3bb707..a4baa2652bf 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1587,13 +1587,6 @@ var ( Owner: grafanaPluginsPlatformSquad, RequiresRestart: true, }, - { - Name: "zipkinBackendMigration", - Description: "Enables querying Zipkin data source without the proxy", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaOSSBigTent, - Expression: "true", // enabled by default - }, { Name: "enableSCIM", Description: "Enables SCIM support for user and group management", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 5a23134e827..34f40c2585d 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -210,7 +210,6 @@ passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,f exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,false,true prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,false,true enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false -zipkinBackendMigration,GA,@grafana/oss-big-tent,false,false,false enableSCIM,experimental,@grafana/identity-access-team,false,false,false crashDetection,experimental,@grafana/observability-traces-and-profiling,false,false,true jaegerBackendMigration,experimental,@grafana/oss-big-tent,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 6ca1cd23fef..06bc4b00564 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -851,10 +851,6 @@ const ( // Enables the extension admin page regardless of development mode FlagEnableExtensionsAdminPage = "enableExtensionsAdminPage" - // FlagZipkinBackendMigration - // Enables querying Zipkin data source without the proxy - FlagZipkinBackendMigration = "zipkinBackendMigration" - // FlagEnableSCIM // Enables SCIM support for user and group management FlagEnableSCIM = "enableSCIM" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 0ac61ba717c..7ef236e5194 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -4062,6 +4062,7 @@ "name": "zipkinBackendMigration", "resourceVersion": "1733846643829", "creationTimestamp": "2024-11-07T09:35:53Z", + "deletionTimestamp": "2025-01-27T11:47:54Z", "annotations": { "grafana.app/updatedTimestamp": "2024-12-10 16:04:03.82919 +0000 UTC" } diff --git a/public/app/plugins/datasource/zipkin/datasource.test.ts b/public/app/plugins/datasource/zipkin/datasource.test.ts index 4123de7960d..5b1ab000004 100644 --- a/public/app/plugins/datasource/zipkin/datasource.test.ts +++ b/public/app/plugins/datasource/zipkin/datasource.test.ts @@ -1,5 +1,4 @@ import { lastValueFrom, of } from 'rxjs'; -import { createFetchResponse } from 'test/helpers/createFetchResponse'; import { DataFrameView, @@ -8,52 +7,49 @@ import { DataSourcePluginMeta, FieldType, } from '@grafana/data'; -import { BackendSrv, TemplateSrv } from '@grafana/runtime'; +import { BackendSrv, getBackendSrv, setBackendSrv, TemplateSrv } from '@grafana/runtime'; import { addNodeGraphFramesToResponse, ZipkinDatasource } from './datasource'; import mockJson from './mocks/mockJsonResponse.json'; import { mockTraceDataFrame } from './mocks/mockTraceDataFrame'; -import { ZipkinQuery, ZipkinSpan } from './types'; -import { traceFrameFields, zipkinResponse } from './utils/testData'; +import { ZipkinQuery } from './types'; -export const backendSrv = { fetch: jest.fn() } as unknown as BackendSrv; - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - getBackendSrv: () => backendSrv, -})); +const templateSrv: TemplateSrv = { + replace: jest.fn().mockImplementation((value) => value), + getVariables: jest.fn(), + containsTemplate: jest.fn(), + updateTimeRange: jest.fn(), +}; describe('ZipkinDatasource', () => { + let origBackendSrv: BackendSrv; + let ds: ZipkinDatasource; + beforeEach(() => { + origBackendSrv = getBackendSrv(); + setBackendSrv({ ...origBackendSrv, fetch: jest.fn().mockReturnValue(of({ data: {} })) }); + ds = new ZipkinDatasource(defaultSettings, templateSrv); + }); + + afterEach(() => { + setBackendSrv(origBackendSrv); + jest.clearAllMocks(); + }); describe('query', () => { - const templateSrv: TemplateSrv = { - replace: jest.fn(), - getVariables: jest.fn(), - containsTemplate: jest.fn(), - updateTimeRange: jest.fn(), - }; - it('runs query', async () => { - setupBackendSrv(zipkinResponse); - const ds = new ZipkinDatasource(defaultSettings, templateSrv); - await expect(ds.query({ targets: [{ query: '12345' }] } as DataQueryRequest)).toEmitValuesWith( - (val) => { - expect(val[0].data[0].fields).toMatchObject(traceFrameFields); - } - ); - }); + const origBackendSrv = getBackendSrv(); + setBackendSrv({ + ...origBackendSrv, + fetch: jest.fn().mockReturnValue(of({ data: [] })), + }); - it('runs query with traceId that includes special characters', async () => { - setupBackendSrv(zipkinResponse); - const ds = new ZipkinDatasource(defaultSettings, templateSrv); - await expect(ds.query({ targets: [{ query: 'a/b' }] } as DataQueryRequest)).toEmitValuesWith( - (val) => { - expect(val[0].data[0].fields).toMatchObject(traceFrameFields); - } - ); + const response = await lastValueFrom(ds.query({ targets: [{ query: 'test' }] } as DataQueryRequest)); + expect(response).toEqual({ + state: 'Done', + data: [], + }); }); it('should handle json file upload', async () => { - const ds = new ZipkinDatasource(defaultSettings); ds.uploadedJson = JSON.stringify(mockJson); const response = await lastValueFrom( ds.query({ @@ -67,7 +63,6 @@ describe('ZipkinDatasource', () => { }); it('should fail on invalid json file upload', async () => { - const ds = new ZipkinDatasource(defaultSettings); ds.uploadedJson = JSON.stringify({ key: 'value', arr: [] }); const response = await lastValueFrom( ds.query({ @@ -81,7 +76,10 @@ describe('ZipkinDatasource', () => { describe('metadataRequest', () => { it('runs query', async () => { - setupBackendSrv(['service 1', 'service 2'] as unknown as ZipkinSpan[]); + setBackendSrv({ + ...origBackendSrv, + fetch: jest.fn().mockReturnValue(of({ data: ['service 1', 'service 2'] })), + }); const ds = new ZipkinDatasource(defaultSettings); const response = await ds.metadataRequest('services'); expect(response).toEqual(['service 1', 'service 2']); @@ -118,13 +116,6 @@ describe('addNodeGraphFramesToResponse', () => { }); }); -function setupBackendSrv(response: ZipkinSpan[]) { - const defaultMock = () => of(createFetchResponse(response)); - - const fetchMock = jest.spyOn(backendSrv, 'fetch'); - fetchMock.mockImplementation(defaultMock); -} - const defaultSettings: DataSourceInstanceSettings = { id: 1, uid: '1', diff --git a/public/app/plugins/datasource/zipkin/datasource.ts b/public/app/plugins/datasource/zipkin/datasource.ts index 54ff15dafb1..7b49e7a0c1c 100644 --- a/public/app/plugins/datasource/zipkin/datasource.ts +++ b/public/app/plugins/datasource/zipkin/datasource.ts @@ -1,4 +1,4 @@ -import { lastValueFrom, Observable, of } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { @@ -9,26 +9,15 @@ import { FieldType, createDataFrame, ScopedVars, - urlUtil, toDataFrame, } from '@grafana/data'; import { createNodeGraphFrames, NodeGraphOptions, SpanBarOptions } from '@grafana/o11y-ds-frontend'; -import { - BackendSrvRequest, - config, - DataSourceWithBackend, - FetchResponse, - getBackendSrv, - getTemplateSrv, - TemplateSrv, -} from '@grafana/runtime'; +import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { ZipkinQuery, ZipkinSpan } from './types'; import { createGraphFrames } from './utils/graphTransform'; import { transformResponse } from './utils/transforms'; -const apiPrefix = '/api/v2'; - export interface ZipkinJsonData extends DataSourceJsonData { nodeGraph?: NodeGraphOptions; } @@ -38,7 +27,7 @@ export class ZipkinDatasource extends DataSourceWithBackend, + instanceSettings: DataSourceInstanceSettings, private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); @@ -61,40 +50,24 @@ export class ZipkinDatasource extends DataSourceWithBackend { - if (this.nodeGraph?.enabled) { - return addNodeGraphFramesToResponse(response); - } - return response; - }) - ); - } - const query = this.applyTemplateVariables(target, options.scopedVars); - return this.request(`${apiPrefix}/trace/${encodeURIComponent(query.query)}`).pipe( - map((res) => responseToDataQueryResponse(res, this.nodeGraph?.enabled)) + return super.query(options).pipe( + map((response) => { + if (this.nodeGraph?.enabled) { + return addNodeGraphFramesToResponse(response); + } + return response; + }) ); } return of(emptyDataQueryResponse); } async metadataRequest(url: string, params?: Record) { - if (config.featureToggles.zipkinBackendMigration) { - return await this.getResource(url, params); - } - const urlWithPrefix = `${apiPrefix}/${url}`; - const res = await lastValueFrom(this.request(urlWithPrefix, params, { hideFromInspector: true })); - return res.data; + return await this.getResource(url, params); } async testDatasource(): Promise<{ status: string; message: string }> { - if (config.featureToggles.zipkinBackendMigration) { - return await super.testDatasource(); - } - - await this.metadataRequest('services'); - return { status: 'success', message: 'Data source is working' }; + return await super.testDatasource(); } getQueryDisplayText(query: ZipkinQuery): string { @@ -123,21 +96,6 @@ export class ZipkinDatasource extends DataSourceWithBackend( - apiUrl: string, - data?: unknown, - options?: Partial - ): Observable> { - const params = data ? urlUtil.serializeParams(data) : ''; - const url = `${this.instanceSettings.url}${apiUrl}${params.length ? `?${params}` : ''}`; - const req = { - ...options, - url, - }; - - return getBackendSrv().fetch(req); - } } function responseToDataQueryResponse(response: { data: ZipkinSpan[] }, nodeGraph = false): DataQueryResponse { From 05905a50692ae90a973ed6e8f019188ae703797b Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 28 Jan 2025 14:16:36 +0000 Subject: [PATCH 148/894] Chore: bracket properly to ensure name is set correctly (#99660) bracket properly to ensure name is set --- packages/grafana-data/src/themes/createTheme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-data/src/themes/createTheme.ts b/packages/grafana-data/src/themes/createTheme.ts index a224f1be11b..554e567c7c4 100644 --- a/packages/grafana-data/src/themes/createTheme.ts +++ b/packages/grafana-data/src/themes/createTheme.ts @@ -41,7 +41,7 @@ export function createTheme(options: NewThemeOptions = {}): GrafanaTheme2 { const visualization = createVisualizationColors(colors); const theme = { - name: (name ?? colors.mode === 'dark') ? 'Dark' : 'Light', + name: name ?? (colors.mode === 'dark' ? 'Dark' : 'Light'), isDark: colors.mode === 'dark', isLight: colors.mode === 'light', colors, From 0cef2b9ae7cc2f2ac82cdd4dd4768c18f95160f6 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 07:17:52 -0700 Subject: [PATCH 149/894] Dashboard Versions: Make compatible with app platform (#99327) --- pkg/api/dashboard.go | 34 ++- pkg/api/dashboard_test.go | 12 +- pkg/api/dtos/dashboard.go | 4 +- pkg/components/dashdiffs/compare.go | 2 +- .../dashboard/legacy/query_dashboards.sql | 6 +- .../apis/dashboard/legacy/sql_dashboards.go | 26 +- pkg/registry/apis/dashboard/legacy/storage.go | 2 +- ...uery_dashboards-history_uid_at_version.sql | 4 +- ...uery_dashboards-history_uid_at_version.sql | 4 +- ...uery_dashboards-history_uid_at_version.sql | 4 +- pkg/registry/apis/dashboard/legacy/utils.go | 9 - .../apis/dashboard/legacy/utils_test.go | 13 - pkg/services/apiserver/client/client.go | 37 ++- pkg/services/apiserver/client/client_mock.go | 13 +- pkg/services/apiserver/client/client_test.go | 34 +++ .../dashboards/service/dashboard_service.go | 41 +-- .../service/dashboard_service_test.go | 67 +++-- pkg/services/dashboardversion/dashver.go | 2 +- .../dashboardversion/dashverimpl/dashver.go | 187 ++++++++++++- .../dashverimpl/dashver_test.go | 114 ++++++-- .../dashverimpl/store_test.go | 4 +- .../dashboardversion/dashvertest/fake.go | 8 +- pkg/services/dashboardversion/model.go | 24 +- pkg/storage/unified/resource/cdk_backend.go | 5 + pkg/storage/unified/resource/server.go | 10 + pkg/storage/unified/sql/backend.go | 4 + .../unified/sql/data/resource_history_get.sql | 2 +- .../unified/sql/test/integration_test.go | 77 ++++++ ...rce_history_get-read trash second page.sql | 2 +- ...rce_history_get-read trash second page.sql | 2 +- ...rce_history_get-read trash second page.sql | 2 +- .../dashboard-scene/scene/DashboardScene.tsx | 8 +- .../settings/VersionsEditView.test.tsx | 79 +++--- .../settings/VersionsEditView.tsx | 25 +- .../version-history/HistorySrv.test.ts | 4 +- .../settings/version-history/HistorySrv.ts | 1 + .../__mocks__/dashboardHistoryMocks.ts | 97 +++---- .../VersionsSettings.test.tsx | 41 ++- .../DashboardSettings/VersionsSettings.tsx | 25 +- .../DashboardSettings/__mocks__/versions.ts | 249 +++++++++--------- .../VersionHistory/RevertDashboardModal.tsx | 5 +- .../VersionHistoryComparison.tsx | 1 + .../VersionHistory/VersionHistoryTable.tsx | 1 + .../VersionHistory/useDashboardRestore.tsx | 9 +- 44 files changed, 887 insertions(+), 413 deletions(-) delete mode 100644 pkg/registry/apis/dashboard/legacy/utils.go delete mode 100644 pkg/registry/apis/dashboard/legacy/utils_test.go create mode 100644 pkg/services/apiserver/client/client_test.go diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 486911d626c..5079d7d9877 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -24,6 +24,7 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" + "github.com/grafana/grafana/pkg/services/dashboardversion/dashverimpl" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" @@ -828,21 +829,22 @@ func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response. } query := dashver.ListDashboardVersionsQuery{ - OrgID: c.SignedInUser.GetOrgID(), - DashboardID: dash.ID, - DashboardUID: dash.UID, - Limit: c.QueryInt("limit"), - Start: c.QueryInt("start"), + OrgID: c.SignedInUser.GetOrgID(), + DashboardID: dash.ID, + DashboardUID: dash.UID, + Limit: c.QueryInt("limit"), + Start: c.QueryInt("start"), + ContinueToken: c.Query("continueToken"), } - versions, err := hs.dashboardVersionService.List(c.Req.Context(), &query) + resp, err := hs.dashboardVersionService.List(c.Req.Context(), &query) if err != nil { return response.Error(http.StatusNotFound, fmt.Sprintf("No versions found for dashboardId %d", dash.ID), err) } - loginMem := make(map[int64]string, len(versions)) - res := make([]dashver.DashboardVersionMeta, 0, len(versions)) - for _, version := range versions { + loginMem := make(map[int64]string, len(resp.Versions)) + res := make([]dashver.DashboardVersionMeta, 0, len(resp.Versions)) + for _, version := range resp.Versions { msg := version.Message if version.RestoredFrom == version.Version { msg = "Initial save (created by migration)" @@ -883,7 +885,10 @@ func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response. }) } - return response.JSON(http.StatusOK, res) + return response.JSON(http.StatusOK, dashver.DashboardVersionResponseMeta{ + Versions: res, + ContinueToken: resp.ContinueToken, + }) } // swagger:route GET /dashboards/id/{DashboardID}/versions/{DashboardVersionID} dashboard_versions getDashboardVersionByID @@ -943,12 +948,15 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R return dashboardGuardianResponse(err) } - version, _ := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 32) + version, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) + if err != nil { + return response.Err(err) + } query := dashver.GetDashboardVersionQuery{ OrgID: c.SignedInUser.GetOrgID(), DashboardID: dash.ID, DashboardUID: dash.UID, - Version: int(version), + Version: version, } res, err := hs.dashboardVersionService.Get(c.Req.Context(), &query) @@ -1158,7 +1166,7 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) respon saveCmd.Dashboard = version.Data saveCmd.Dashboard.Set("version", dash.Version) saveCmd.Dashboard.Set("uid", dash.UID) - saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version) + saveCmd.Message = dashverimpl.DashboardRestoreMessage(version.Version) // nolint:staticcheck saveCmd.FolderID = dash.FolderID metrics.MFolderIDsAPICount.WithLabelValues(metrics.RestoreDashboardVersion).Inc() diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 34c6c2c4dbe..a08deefb4bb 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -743,10 +743,10 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { }).callGetDashboardVersions(sc) assert.Equal(t, http.StatusOK, sc.resp.Code) - var versions []dashver.DashboardVersionMeta + var versions dashver.DashboardVersionResponseMeta err := json.NewDecoder(sc.resp.Body).Decode(&versions) require.NoError(t, err) - for _, v := range versions { + for _, v := range versions.Versions { assert.Equal(t, "test-user", v.CreatedBy) } }, mockSQLStore) @@ -769,10 +769,10 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { }).callGetDashboardVersions(sc) assert.Equal(t, http.StatusOK, sc.resp.Code) - var versions []dashver.DashboardVersionMeta + var versions dashver.DashboardVersionResponseMeta err := json.NewDecoder(sc.resp.Body).Decode(&versions) require.NoError(t, err) - for _, v := range versions { + for _, v := range versions.Versions { assert.Equal(t, anonString, v.CreatedBy) } }, mockSQLStore) @@ -795,10 +795,10 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { }).callGetDashboardVersions(sc) assert.Equal(t, http.StatusOK, sc.resp.Code) - var versions []dashver.DashboardVersionMeta + var versions dashver.DashboardVersionResponseMeta err := json.NewDecoder(sc.resp.Body).Decode(&versions) require.NoError(t, err) - for _, v := range versions { + for _, v := range versions.Versions { assert.Equal(t, anonString, v.CreatedBy) } }, mockSQLStore) diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index 9d8f96f8c7a..73c3d7cae6f 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -54,10 +54,10 @@ type CalculateDiffOptions struct { type CalculateDiffTarget struct { DashboardId int64 `json:"dashboardId"` - Version int `json:"version"` + Version int64 `json:"version"` UnsavedDashboard *simplejson.Json `json:"unsavedDashboard"` } type RestoreDashboardVersionCommand struct { - Version int `json:"version" binding:"Required"` + Version int64 `json:"version" binding:"Required"` } diff --git a/pkg/components/dashdiffs/compare.go b/pkg/components/dashdiffs/compare.go index e5c6e841901..7bb06a92ff2 100644 --- a/pkg/components/dashdiffs/compare.go +++ b/pkg/components/dashdiffs/compare.go @@ -34,7 +34,7 @@ type Options struct { type DiffTarget struct { DashboardId int64 - Version int + Version int64 UnsavedDashboard *simplejson.Json } diff --git a/pkg/registry/apis/dashboard/legacy/query_dashboards.sql b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql index e5d532ffc87..c4901404f25 100644 --- a/pkg/registry/apis/dashboard/legacy/query_dashboards.sql +++ b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql @@ -30,11 +30,11 @@ WHERE dashboard.is_folder = {{ .Arg .Query.GetFolders }} {{ if .Query.Version }} AND dashboard_version.version = {{ .Arg .Query.Version }} {{ else if .Query.LastID }} - AND dashboard_version.version < {{ .Arg .Query.LastID }} + AND dashboard_version.version <= {{ .Arg .Query.LastID }} {{ end }} ORDER BY - dashboard_version.created ASC, - dashboard_version.version ASC, + dashboard_version.created DESC, + dashboard_version.version DESC, dashboard.uid ASC {{ else }} {{ if .Query.UID }} diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 999586b13a1..f3b86f8b639 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -115,8 +115,9 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, sql *legacysql.LegacyD rows = nil } return &rowsWrapper{ - rows: rows, - a: a, + rows: rows, + a: a, + history: query.GetHistory, // This looks up rules from the permissions on a user canReadDashboard: func(scopes ...string) bool { return true // ??? @@ -128,8 +129,9 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, sql *legacysql.LegacyD var _ resource.ListIterator = (*rowsWrapper)(nil) type rowsWrapper struct { - a *dashboardSqlAccess - rows *sql.Rows + a *dashboardSqlAccess + rows *sql.Rows + history bool canReadDashboard func(scopes ...string) bool @@ -157,7 +159,7 @@ func (r *rowsWrapper) Next() bool { // breaks after first readable value for r.rows.Next() { - r.row, err = r.a.scanRow(r.rows) + r.row, err = r.a.scanRow(r.rows, r.history) if err != nil { r.err = err return false @@ -187,6 +189,11 @@ func (r *rowsWrapper) ContinueToken() string { return r.row.token.String() } +// ContinueTokenWithCurrentRV implements resource.ListIterator. +func (r *rowsWrapper) ContinueTokenWithCurrentRV() string { + return r.row.token.String() +} + // Error implements resource.ListIterator. func (r *rowsWrapper) Error() error { return r.err @@ -218,7 +225,7 @@ func (r *rowsWrapper) Value() []byte { return b } -func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { +func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRow, error) { dash := &dashboard.Dashboard{ TypeMeta: dashboard.DashboardResourceInfo.TypeMeta(), ObjectMeta: metav1.ObjectMeta{Annotations: make(map[string]string)}, @@ -255,8 +262,12 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { ) row.token = &continueToken{orgId: orgId, id: dashboard_id} + // when listing from the history table, we want to use the version as the ID to continue from + if history { + row.token.id = version + } if err == nil { - row.RV = getResourceVersion(dashboard_id, version) + row.RV = version dash.ResourceVersion = fmt.Sprintf("%d", row.RV) dash.Namespace = a.namespacer(orgId) dash.UID = gapiutil.CalculateClusterWideUID(dash) @@ -405,6 +416,7 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das } out, err := a.dashStore.SaveDashboard(ctx, dashboards.SaveDashboardCommand{ OrgID: orgId, + Message: meta.GetMessage(), PluginID: service.GetPluginIDFromMeta(meta), Dashboard: simplejson.NewFromAny(dash.Spec.UnstructuredContent()), FolderUID: meta.GetFolder(), diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index e5f51bf78f9..7064d1d9994 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -138,7 +138,7 @@ func (a *dashboardSqlAccess) ReadResource(ctx context.Context, req *resource.Rea } version := int64(0) if req.ResourceVersion > 0 { - version = getVersionFromRV(req.ResourceVersion) + version = req.ResourceVersion } dash, rv, err := a.GetDashboard(ctx, info.OrgID, req.Key.Name, version) diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_at_version.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_at_version.sql index d85d3bbd8c8..728e9a8e72b 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_at_version.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_at_version.sql @@ -19,6 +19,6 @@ WHERE dashboard.is_folder = FALSE AND dashboard.uid = 'UUU' AND dashboard_version.version = 3 ORDER BY - dashboard_version.created ASC, - dashboard_version.version ASC, + dashboard_version.created DESC, + dashboard_version.version DESC, dashboard.uid ASC diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_at_version.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_at_version.sql index 5fe3708643a..b149e393780 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_at_version.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_at_version.sql @@ -19,6 +19,6 @@ WHERE dashboard.is_folder = FALSE AND dashboard.uid = 'UUU' AND dashboard_version.version = 3 ORDER BY - dashboard_version.created ASC, - dashboard_version.version ASC, + dashboard_version.created DESC, + dashboard_version.version DESC, dashboard.uid ASC diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_at_version.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_at_version.sql index 5fe3708643a..b149e393780 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_at_version.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_at_version.sql @@ -19,6 +19,6 @@ WHERE dashboard.is_folder = FALSE AND dashboard.uid = 'UUU' AND dashboard_version.version = 3 ORDER BY - dashboard_version.created ASC, - dashboard_version.version ASC, + dashboard_version.created DESC, + dashboard_version.version DESC, dashboard.uid ASC diff --git a/pkg/registry/apis/dashboard/legacy/utils.go b/pkg/registry/apis/dashboard/legacy/utils.go deleted file mode 100644 index c61db27788a..00000000000 --- a/pkg/registry/apis/dashboard/legacy/utils.go +++ /dev/null @@ -1,9 +0,0 @@ -package legacy - -func getResourceVersion(id int64, version int64) int64 { - return version + (id * 10000000) -} - -func getVersionFromRV(rv int64) int64 { - return rv % 10000000 -} diff --git a/pkg/registry/apis/dashboard/legacy/utils_test.go b/pkg/registry/apis/dashboard/legacy/utils_test.go deleted file mode 100644 index a058dae612a..00000000000 --- a/pkg/registry/apis/dashboard/legacy/utils_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package legacy - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestVersionHacks(t *testing.T) { - rv := getResourceVersion(123, 456) - require.Equal(t, int64(1230000456), rv) - require.Equal(t, int64(456), getVersionFromRV(rv)) -} diff --git a/pkg/services/apiserver/client/client.go b/pkg/services/apiserver/client/client.go index 2e25ca07ee2..2f45e426bc3 100644 --- a/pkg/services/apiserver/client/client.go +++ b/pkg/services/apiserver/client/client.go @@ -2,7 +2,10 @@ package client import ( "context" + "errors" "fmt" + "strconv" + "strings" "time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -16,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" k8sUser "k8s.io/apiserver/pkg/authentication/user" @@ -24,7 +28,7 @@ import ( type K8sHandler interface { GetNamespace(orgID int64) string - Get(ctx context.Context, name string, orgID int64, subresource ...string) (*unstructured.Unstructured, error) + Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) Delete(ctx context.Context, name string, orgID int64, options v1.DeleteOptions) error @@ -32,6 +36,7 @@ type K8sHandler interface { List(ctx context.Context, orgID int64, options v1.ListOptions) (*unstructured.UnstructuredList, error) Search(ctx context.Context, orgID int64, in *resource.ResourceSearchRequest) (*resource.ResourceSearchResponse, error) GetStats(ctx context.Context, orgID int64) (*resource.ResourceStatsResponse, error) + GetUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) } var _ K8sHandler = (*k8sHandler)(nil) @@ -41,9 +46,10 @@ type k8sHandler struct { gvr schema.GroupVersionResource restConfigProvider apiserver.RestConfigProvider searcher resource.ResourceIndexClient + userService user.Service } -func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient, dashStore dashboards.Store) K8sHandler { +func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient, dashStore dashboards.Store, userSvc user.Service) K8sHandler { legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore) searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, searcher, legacySearcher) return &k8sHandler{ @@ -51,6 +57,7 @@ func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr sch gvr: gvr, restConfigProvider: restConfigProvider, searcher: searchClient, + userService: userSvc, } } @@ -58,7 +65,7 @@ func (h *k8sHandler) GetNamespace(orgID int64) string { return h.namespacer(orgID) } -func (h *k8sHandler) Get(ctx context.Context, name string, orgID int64, subresource ...string) (*unstructured.Unstructured, error) { +func (h *k8sHandler) Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error) { // create a new context - prevents issues when the request stems from the k8s api itself // otherwise the context goes through the handlers twice and causes issues newCtx, cancel, err := h.getK8sContext(ctx) @@ -73,7 +80,7 @@ func (h *k8sHandler) Get(ctx context.Context, name string, orgID int64, subresou return nil, nil } - return client.Get(newCtx, name, v1.GetOptions{}, subresource...) + return client.Get(newCtx, name, options, subresource...) } func (h *k8sHandler) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) { @@ -191,6 +198,28 @@ func (h *k8sHandler) GetStats(ctx context.Context, orgID int64) (*resource.Resou }) } +// GetUserFromMeta takes what meta accessor gives you from `GetCreatedBy` or `GetUpdatedBy` and returns the user +func (h *k8sHandler) GetUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) { + parts := strings.Split(userMeta, ":") + if len(parts) < 2 { + return &user.User{}, nil + } + meta := parts[1] + + userId, err := strconv.ParseInt(meta, 10, 64) + var u *user.User + if err == nil { + u, err = h.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: userId}) + } else { + u, err = h.userService.GetByUID(ctx, &user.GetUserByUIDQuery{UID: meta}) + } + + if err != nil && errors.Is(err, user.ErrUserNotFound) { + return &user.User{}, nil + } + return u, err +} + func (h *k8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) { cfg := h.restConfigProvider.GetRestConfig(ctx) if cfg == nil { diff --git a/pkg/services/apiserver/client/client_mock.go b/pkg/services/apiserver/client/client_mock.go index f3a4e347e2f..b5e2fc2eecb 100644 --- a/pkg/services/apiserver/client/client_mock.go +++ b/pkg/services/apiserver/client/client_mock.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/mock" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/unified/resource" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -21,8 +22,8 @@ func (m *MockK8sHandler) GetNamespace(orgID int64) string { return args.String(0) } -func (m *MockK8sHandler) Get(ctx context.Context, name string, orgID int64, subresource ...string) (*unstructured.Unstructured, error) { - args := m.Called(ctx, name, orgID, subresource) +func (m *MockK8sHandler) Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error) { + args := m.Called(ctx, name, orgID, options, subresource) if args.Get(0) == nil { return nil, args.Error(1) } @@ -79,3 +80,11 @@ func (m *MockK8sHandler) GetStats(ctx context.Context, orgID int64) (*resource.R } return args.Get(0).(*resource.ResourceStatsResponse), args.Error(1) } + +func (m *MockK8sHandler) GetUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) { + args := m.Called(ctx, userMeta) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*user.User), args.Error(1) +} diff --git a/pkg/services/apiserver/client/client_test.go b/pkg/services/apiserver/client/client_test.go new file mode 100644 index 00000000000..c97c5f0a31d --- /dev/null +++ b/pkg/services/apiserver/client/client_test.go @@ -0,0 +1,34 @@ +package client + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" + "github.com/stretchr/testify/require" +) + +func TestGetUserFromMeta(t *testing.T) { + userSvcTest := usertest.NewUserServiceFake() + userSvcTest.ExpectedUser = &user.User{ + ID: 1, + UID: "uid-value", + } + client := &k8sHandler{ + userService: userSvcTest, + } + t.Run("returns user with valid UID", func(t *testing.T) { + result, err := client.GetUserFromMeta(context.Background(), "user:uid-value") + require.NoError(t, err) + require.Equal(t, "uid-value", result.UID) + require.Equal(t, int64(1), result.ID) + }) + + t.Run("returns user when id is passed in", func(t *testing.T) { + result, err := client.GetUserFromMeta(context.Background(), "user:1") + require.NoError(t, err) + require.Equal(t, "uid-value", result.UID) + require.Equal(t, int64(1), result.ID) + }) +} diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index dd8d95348ad..ad78d14952f 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -68,7 +68,6 @@ type DashboardServiceImpl struct { dashboardStore dashboards.Store folderStore folder.FolderStore folderService folder.Service - userService user.Service orgService org.Service features featuremgmt.FeatureToggles folderPermissions accesscontrol.FolderPermissionsService @@ -91,7 +90,7 @@ func ProvideDashboardServiceImpl( restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper, ) (*DashboardServiceImpl, error) { - k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore) + k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore, userService) dashSvc := &DashboardServiceImpl{ cfg: cfg, @@ -103,7 +102,6 @@ func ProvideDashboardServiceImpl( folderStore: folderStore, folderService: folderSvc, orgService: orgService, - userService: userService, k8sclient: k8sHandler, metrics: newDashboardsMetrics(r), dashboardPermissionsReady: make(chan struct{}), @@ -1489,7 +1487,7 @@ func (dr *DashboardServiceImpl) getDashboardThroughK8s(ctx context.Context, quer query.UID = result.UID } - out, err := dr.k8sclient.Get(ctx, query.UID, query.OrgID, subresource) + out, err := dr.k8sclient.Get(ctx, query.UID, query.OrgID, v1.GetOptions{}, subresource) if err != nil && !apierrors.IsNotFound(err) { return nil, err } else if err != nil || out == nil { @@ -1553,7 +1551,7 @@ func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd func (dr *DashboardServiceImpl) createOrUpdateDash(ctx context.Context, obj unstructured.Unstructured, orgID int64) (*dashboards.Dashboard, error) { var out *unstructured.Unstructured - current, err := dr.k8sclient.Get(ctx, obj.GetName(), orgID) + current, err := dr.k8sclient.Get(ctx, obj.GetName(), orgID, v1.GetOptions{}) if current == nil || err != nil { out, err = dr.k8sclient.Create(ctx, &obj, orgID) if err != nil { @@ -1751,7 +1749,7 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex for _, h := range searchResults.Hits { func(hit v0alpha1.DashboardHit) { g.Go(func() error { - out, err := dr.k8sclient.Get(ctx, hit.Name, query.OrgId) + out, err := dr.k8sclient.Get(ctx, hit.Name, query.OrgId, v1.GetOptions{}) if err != nil { return err } else if out == nil { @@ -1863,13 +1861,13 @@ func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Contex out.PluginID = GetPluginIDFromMeta(obj) - creator, err := dr.getUserFromMeta(ctx, obj.GetCreatedBy()) + creator, err := dr.k8sclient.GetUserFromMeta(ctx, obj.GetCreatedBy()) if err != nil { return nil, err } out.CreatedBy = creator.ID - updater, err := dr.getUserFromMeta(ctx, obj.GetUpdatedBy()) + updater, err := dr.k8sclient.GetUserFromMeta(ctx, obj.GetUpdatedBy()) if err != nil { return nil, err } @@ -1906,25 +1904,6 @@ func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Contex return &out, nil } -func (dr *DashboardServiceImpl) getUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) { - if userMeta == "" || toUID(userMeta) == "" { - return &user.User{}, nil - } - usr, err := dr.getUser(ctx, toUID(userMeta)) - if err != nil && errors.Is(err, user.ErrUserNotFound) { - return &user.User{}, nil - } - return usr, err -} - -func (dr *DashboardServiceImpl) getUser(ctx context.Context, uid string) (*user.User, error) { - userId, err := strconv.ParseInt(uid, 10, 64) - if err == nil { - return dr.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: userId}) - } - return dr.userService.GetByUID(ctx, &user.GetUserByUIDQuery{UID: uid}) -} - var pluginIDRepoName = "plugin" var fileProvisionedRepoPrefix = "file:" @@ -2010,11 +1989,3 @@ func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, names return finalObj, nil } - -func toUID(rawIdentifier string) string { - parts := strings.Split(rawIdentifier, ":") - if len(parts) < 2 { - return "" - } - return parts[1] -} diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index fbe99cf0fb4..d7adcd9ad9a 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -26,7 +26,6 @@ import ( "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -315,7 +314,8 @@ func TestGetDashboard(t *testing.T) { Version: 1, Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), } - k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once() + k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once() + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) dashboard, err := service.GetDashboard(ctx, query) require.NoError(t, err) @@ -351,7 +351,8 @@ func TestGetDashboard(t *testing.T) { Version: 1, Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), } - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once() + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once() + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ @@ -391,7 +392,7 @@ func TestGetDashboard(t *testing.T) { t.Run("Should return error when Kubernetes client fails", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything).Return(nil, assert.AnError).Once() + k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(nil, assert.AnError).Once() dashboard, err := service.GetDashboard(ctx, query) require.Error(t, err) @@ -401,7 +402,7 @@ func TestGetDashboard(t *testing.T) { t.Run("Should return dashboard not found if Kubernetes client returns nil", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything).Return(nil, nil).Once() + k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once() dashboard, err := service.GetDashboard(ctx, query) require.Error(t, err) require.Equal(t, dashboards.ErrDashboardNotFound, err) @@ -450,6 +451,7 @@ func TestGetAllDashboards(t *testing.T) { Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), } + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once() dashes, err := service.GetAllDashboards(ctx) @@ -501,6 +503,7 @@ func TestGetAllDashboardsByOrgId(t *testing.T) { Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), } + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once() dashes, err := service.GetAllDashboardsByOrgId(ctx, 1) @@ -534,7 +537,7 @@ func TestGetProvisionedDashboardData(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled and get from relevant org", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", "labels": map[string]any{ @@ -632,7 +635,7 @@ func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled and get from whatever org it is in", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", "labels": map[string]any{ @@ -721,7 +724,7 @@ func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", "labels": map[string]any{ @@ -812,7 +815,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid", OrgID: 1}).Return(nil).Once() fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid3", OrgID: 2}).Return(nil).Once() fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) - k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", "annotations": map[string]any{ @@ -825,7 +828,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { "spec": map[string]any{}, }}, nil).Once() // should not delete this one, because it does not start with "file:" - k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid2", "annotations": map[string]any{ @@ -836,7 +839,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { "spec": map[string]any{}, }}, nil).Once() - k8sCliMock.On("Get", mock.Anything, "uid3", mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ + k8sCliMock.On("Get", mock.Anything, "uid3", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid3", "annotations": map[string]any{ @@ -958,7 +961,7 @@ func TestUnprovisionDashboard(t *testing.T) { }, "spec": map[string]any{}, }} - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dash, nil) + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dash, nil) dashWithoutAnnotations := &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "dashboard.grafana.app/v0alpha1", "kind": "Dashboard", @@ -975,6 +978,7 @@ func TestUnprovisionDashboard(t *testing.T) { // should update it to be without annotations k8sCliMock.On("Update", mock.Anything, dashWithoutAnnotations, mock.Anything, mock.Anything).Return(dashWithoutAnnotations, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ @@ -1039,7 +1043,8 @@ func TestGetDashboardsByPluginID(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything).Return(uidUnstructured, nil) + k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(uidUnstructured, nil) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool { return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == "plugin" && req.Options.Fields[1].Key == "repo.path" && req.Options.Fields[1].Values[0] == "testing" @@ -1127,7 +1132,8 @@ func TestSaveProvisionedDashboard(t *testing.T) { t.Run("Should use Kubernetes create if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") @@ -1188,7 +1194,8 @@ func TestSaveDashboard(t *testing.T) { t.Run("Should use Kubernetes create if feature flags are enabled and dashboard doesn't exist", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) @@ -1199,7 +1206,8 @@ func TestSaveDashboard(t *testing.T) { t.Run("Should use Kubernetes update if feature flags are enabled and dashboard exists", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) @@ -1210,7 +1218,7 @@ func TestSaveDashboard(t *testing.T) { t.Run("Should return an error if uid is invalid", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) @@ -1495,8 +1503,9 @@ func TestGetDashboards(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("Get", mock.Anything, "uid1", mock.Anything, mock.Anything).Return(uid1Unstructured, nil) - k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything).Return(uid2Unstructured, nil) + k8sCliMock.On("Get", mock.Anything, "uid1", mock.Anything, mock.Anything, mock.Anything).Return(uid1Unstructured, nil) + k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything, mock.Anything).Return(uid2Unstructured, nil) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ @@ -1611,10 +1620,10 @@ func TestGetDashboardUIDByID(t *testing.T) { } func TestUnstructuredToLegacyDashboard(t *testing.T) { - fake := usertest.NewUserServiceFake() - fake.ExpectedUser = &user.User{ID: 10, UID: "useruid"} + k8sCliMock := new(client.MockK8sHandler) + k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{ID: 10, UID: "useruid"}, nil) dr := &DashboardServiceImpl{ - userService: fake, + k8sclient: k8sCliMock, } t.Run("successfully converts unstructured to legacy dashboard", func(t *testing.T) { uid := "36b7c825-79cc-435e-acf6-c78bd96a4510" @@ -1912,17 +1921,3 @@ func TestLegacySaveCommandToUnstructured(t *testing.T) { assert.Equal(t, result.GetAnnotations(), map[string]string(nil)) }) } - -func TestToUID(t *testing.T) { - t.Run("parses valid UID", func(t *testing.T) { - rawIdentifier := "user:uid-value" - result := toUID(rawIdentifier) - assert.Equal(t, "uid-value", result) - }) - - t.Run("returns empty string for invalid identifier", func(t *testing.T) { - rawIdentifier := "invalid-uid" - result := toUID(rawIdentifier) - assert.Equal(t, "", result) - }) -} diff --git a/pkg/services/dashboardversion/dashver.go b/pkg/services/dashboardversion/dashver.go index 8609e238d55..9d4dcb60264 100644 --- a/pkg/services/dashboardversion/dashver.go +++ b/pkg/services/dashboardversion/dashver.go @@ -7,5 +7,5 @@ import ( type Service interface { Get(context.Context, *GetDashboardVersionQuery) (*DashboardVersionDTO, error) DeleteExpired(context.Context, *DeleteExpiredVersionsCommand) error - List(context.Context, *ListDashboardVersionsQuery) ([]*DashboardVersionDTO, error) + List(context.Context, *ListDashboardVersionsQuery) (*DashboardVersionResponse, error) } diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 04fed84c9bd..02bed95b96e 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -3,12 +3,26 @@ package dashverimpl import ( "context" "errors" + "fmt" + "strconv" + "strings" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/apiserver" + "github.com/grafana/grafana/pkg/services/apiserver/client" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) const ( @@ -17,19 +31,32 @@ const ( ) type Service struct { - cfg *setting.Cfg - store store - dashSvc dashboards.DashboardService - log log.Logger + cfg *setting.Cfg + store store + dashSvc dashboards.DashboardService + k8sclient client.K8sHandler + features featuremgmt.FeatureToggles + log log.Logger } -func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.DashboardService) dashver.Service { +func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.DashboardService, dashboardStore dashboards.Store, features featuremgmt.FeatureToggles, + restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient) dashver.Service { return &Service{ cfg: cfg, store: &sqlStore{ db: db, dialect: db.GetDialect(), }, + features: features, + k8sclient: client.NewK8sHandler( + cfg, + request.GetNamespaceMapper(cfg), + v0alpha1.DashboardResourceInfo.GroupVersionResource(), + restConfigProvider, + unified, + dashboardStore, + userService, + ), dashSvc: dashboardService, log: log.New("dashboard-version"), } @@ -49,13 +76,21 @@ func (s *Service) Get(ctx context.Context, query *dashver.GetDashboardVersionQue // versions table, at time of this writing), so get the DashboardID if it // was not populated. if query.DashboardID == 0 { - id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID) + id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID, query.OrgID) if err != nil { return nil, err } query.DashboardID = id } + if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { + version, err := s.getHistoryThroughK8s(ctx, query.OrgID, query.DashboardUID, query.Version) + if err != nil { + return nil, err + } + return version, nil + } + version, err := s.store.Get(ctx, query) if err != nil { return nil, err @@ -95,7 +130,7 @@ func (s *Service) DeleteExpired(ctx context.Context, cmd *dashver.DeleteExpiredV } // List all dashboard versions for the given dashboard ID. -func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) ([]*dashver.DashboardVersionDTO, error) { +func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) (*dashver.DashboardVersionResponse, error) { // Get the DashboardUID if not populated if query.DashboardUID == "" { u, err := s.getDashUIDMaybeEmpty(ctx, query.DashboardID) @@ -109,7 +144,7 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions // versions table, at time of this writing), so get the DashboardID if it // was not populated. if query.DashboardID == 0 { - id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID) + id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID, query.OrgID) if err != nil { return nil, err } @@ -118,6 +153,21 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions if query.Limit == 0 { query.Limit = 1000 } + + if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { + versions, err := s.listHistoryThroughK8s( + ctx, + query.OrgID, + query.DashboardUID, + int64(query.Limit), + query.ContinueToken, + ) + if err != nil { + return nil, err + } + return versions, nil + } + dvs, err := s.store.List(ctx, query) if err != nil { return nil, err @@ -126,7 +176,9 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions for i, v := range dvs { dtos[i] = v.ToDTO(query.DashboardUID) } - return dtos, nil + return &dashver.DashboardVersionResponse{ + Versions: dtos, + }, nil } // getDashUIDMaybeEmpty is a helper function which takes a dashboardID and @@ -149,8 +201,8 @@ func (s *Service) getDashUIDMaybeEmpty(ctx context.Context, id int64) (string, e // getDashIDMaybeEmpty is a helper function which takes a dashboardUID and // returns the ID. If the dashboard is not found, it will return -1. -func (s *Service) getDashIDMaybeEmpty(ctx context.Context, uid string) (int64, error) { - q := dashboards.GetDashboardQuery{UID: uid} +func (s *Service) getDashIDMaybeEmpty(ctx context.Context, uid string, orgID int64) (int64, error) { + q := dashboards.GetDashboardQuery{UID: uid, OrgID: orgID} result, err := s.dashSvc.GetDashboard(ctx, &q) if err != nil { if errors.Is(err, dashboards.ErrDashboardNotFound) { @@ -163,3 +215,116 @@ func (s *Service) getDashIDMaybeEmpty(ctx context.Context, uid string) (int64, e } return result.ID, nil } + +func (s *Service) getHistoryThroughK8s(ctx context.Context, orgID int64, dashboardUID string, rv int64) (*dashver.DashboardVersionDTO, error) { + out, err := s.k8sclient.Get(ctx, dashboardUID, orgID, v1.GetOptions{ResourceVersion: strconv.FormatInt(rv, 10)}) + if err != nil { + return nil, err + } else if out == nil { + return nil, dashboards.ErrDashboardNotFound + } + + dash, err := s.UnstructuredToLegacyDashboardVersion(ctx, out, orgID) + if err != nil { + return nil, err + } + + return dash, nil +} + +func (s *Service) listHistoryThroughK8s(ctx context.Context, orgID int64, dashboardUID string, limit int64, continueToken string) (*dashver.DashboardVersionResponse, error) { + out, err := s.k8sclient.List(ctx, orgID, v1.ListOptions{ + LabelSelector: utils.LabelKeyGetHistory + "=" + dashboardUID, + Limit: limit, + Continue: continueToken, + }) + if err != nil { + return nil, err + } else if out == nil { + return nil, dashboards.ErrDashboardNotFound + } + + dashboards := make([]*dashver.DashboardVersionDTO, len(out.Items)) + for i, item := range out.Items { + dash, err := s.UnstructuredToLegacyDashboardVersion(ctx, &item, orgID) + if err != nil { + return nil, err + } + dashboards[i] = dash + } + + return &dashver.DashboardVersionResponse{ + ContinueToken: out.GetContinue(), + Versions: dashboards, + }, nil +} + +func (s *Service) UnstructuredToLegacyDashboardVersion(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*dashver.DashboardVersionDTO, error) { + spec, ok := item.Object["spec"].(map[string]any) + if !ok { + return nil, errors.New("error parsing dashboard from k8s response") + } + obj, err := utils.MetaAccessor(item) + if err != nil { + return nil, err + } + uid := obj.GetName() + spec["uid"] = uid + + dashVersion := 0 + parentVersion := 0 + if version, ok := spec["version"].(int64); ok { + dashVersion = int(version) + parentVersion = dashVersion - 1 + } + + createdBy, err := s.k8sclient.GetUserFromMeta(ctx, obj.GetCreatedBy()) + if err != nil { + return nil, err + } + + id, err := obj.GetResourceVersionInt64() + if err != nil { + return nil, err + } + + restoreVer, err := getRestoreVersion(obj.GetMessage()) + if err != nil { + return nil, err + } + + out := dashver.DashboardVersionDTO{ + ID: id, + DashboardID: obj.GetDeprecatedInternalID(), // nolint:staticcheck + DashboardUID: uid, + Created: obj.GetCreationTimestamp().Time, + CreatedBy: createdBy.ID, + Message: obj.GetMessage(), + RestoredFrom: restoreVer, + Version: dashVersion, + ParentVersion: parentVersion, + Data: simplejson.NewFromAny(spec), + } + + return &out, nil +} + +var restoreMsg = "Restored from version " + +func DashboardRestoreMessage(version int) string { + return fmt.Sprintf("%s%d", restoreMsg, version) +} + +func getRestoreVersion(msg string) (int, error) { + parts := strings.Split(msg, restoreMsg) + if len(parts) < 2 { + return 0, nil + } + + ver, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return 0, err + } + + return int(ver), nil +} diff --git a/pkg/services/dashboardversion/dashverimpl/dashver_test.go b/pkg/services/dashboardversion/dashverimpl/dashver_test.go index 7ec8fc4ef64..ed58c27bb7f 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver_test.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver_test.go @@ -7,18 +7,24 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) func TestDashboardVersionService(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, features: featuremgmt.WithFeatures()} t.Run("Get dashboard version", func(t *testing.T) { dashboard := &dashver.DashboardVersion{ @@ -32,6 +38,44 @@ func TestDashboardVersionService(t *testing.T) { require.NoError(t, err) require.Equal(t, dashboard.ToDTO("uid"), dashboardVersion) }) + + t.Run("Get dashboard version through k8s", func(t *testing.T) { + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()} + mockCli := new(client.MockK8sHandler) + dashboardVersionService.k8sclient = mockCli + dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards) + dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil) + + mockCli.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) + mockCli.On("Get", mock.Anything, "uid", int64(1), v1.GetOptions{ResourceVersion: "10"}, mock.Anything).Return(&unstructured.Unstructured{ + Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "resourceVersion": "12", + "labels": map[string]any{ + utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + }, + }, + "spec": map[string]any{ + "version": int64(10), + }, + }}, nil).Once() + res, err := dashboardVersionService.Get(context.Background(), &dashver.GetDashboardVersionQuery{ + DashboardID: 42, + OrgID: 1, + Version: 10, + }) + require.Nil(t, err) + require.Equal(t, res, &dashver.DashboardVersionDTO{ + ID: 12, // RV should be used + Version: 10, + ParentVersion: 9, + DashboardID: 42, + DashboardUID: "uid", + Data: simplejson.NewFromAny(map[string]any{"uid": "uid", "version": int64(10)}), + }) + }) } func TestDeleteExpiredVersions(t *testing.T) { @@ -42,7 +86,7 @@ func TestDeleteExpiredVersions(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) dashboardVersionService := Service{ - cfg: cfg, store: dashboardVersionStore, dashSvc: dashboardService} + cfg: cfg, store: dashboardVersionStore, dashSvc: dashboardService, features: featuremgmt.WithFeatures()} t.Run("Don't delete anything if there are no expired versions", func(t *testing.T) { err := dashboardVersionService.DeleteExpired(context.Background(), &dashver.DeleteExpiredVersionsCommand{DeletedRows: 4}) @@ -67,7 +111,7 @@ func TestListDashboardVersions(t *testing.T) { t.Run("List all versions for a given Dashboard ID", func(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, features: featuremgmt.WithFeatures()} dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{ {ID: 1, DashboardID: 42}, } @@ -78,15 +122,15 @@ func TestListDashboardVersions(t *testing.T) { query := dashver.ListDashboardVersionsQuery{DashboardID: 42} res, err := dashboardVersionService.List(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 1, len(res)) + require.Equal(t, 1, len(res.Versions)) // validate that the UID was populated - require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}, res) + require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}}, res) }) t.Run("List all versions for a non-existent DashboardID", func(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()} dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{ {ID: 1, DashboardID: 42}, } @@ -96,15 +140,15 @@ func TestListDashboardVersions(t *testing.T) { query := dashver.ListDashboardVersionsQuery{DashboardID: 42} res, err := dashboardVersionService.List(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 1, len(res)) + require.Equal(t, 1, len(res.Versions)) // The DashboardID remains populated with the given value, even though the dash was not found - require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42}}, res) + require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42}}}, res) }) t.Run("List all versions for a given DashboardUID", func(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()} dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{{DashboardID: 42, ID: 1}} dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")). Return(&dashboards.Dashboard{ID: 42}, nil) @@ -112,15 +156,15 @@ func TestListDashboardVersions(t *testing.T) { query := dashver.ListDashboardVersionsQuery{DashboardUID: "uid"} res, err := dashboardVersionService.List(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 1, len(res)) + require.Equal(t, 1, len(res.Versions)) // validate that the dashboardID was populated from the GetDashboard method call. - require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}, res) + require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}}, res) }) t.Run("List all versions for a given non-existent DashboardUID", func(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()} dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{{DashboardID: 42, ID: 1}} dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")). Return(nil, dashboards.ErrDashboardNotFound) @@ -128,15 +172,15 @@ func TestListDashboardVersions(t *testing.T) { query := dashver.ListDashboardVersionsQuery{DashboardUID: "uid"} res, err := dashboardVersionService.List(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 1, len(res)) + require.Equal(t, 1, len(res.Versions)) // validate that the dashboardUID & ID are populated, even though the dash was not found - require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}, res) + require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}}, res) }) t.Run("List Dashboard versions - error from store", func(t *testing.T) { dashboardVersionStore := newDashboardVersionStoreFake() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()} + dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()} dashboardVersionStore.ExpectedError = dashver.ErrDashboardVersionNotFound query := dashver.ListDashboardVersionsQuery{DashboardID: 42, DashboardUID: "42"} @@ -144,6 +188,46 @@ func TestListDashboardVersions(t *testing.T) { require.Nil(t, res) require.ErrorIs(t, err, dashver.ErrDashboardVersionNotFound) }) + + t.Run("List all versions for a given Dashboard ID through k8s", func(t *testing.T) { + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()} + mockCli := new(client.MockK8sHandler) + dashboardVersionService.k8sclient = mockCli + dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards) + + dashboardService.On("GetDashboardUIDByID", mock.Anything, + mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")). + Return(&dashboards.DashboardRef{UID: "uid"}, nil) + + query := dashver.ListDashboardVersionsQuery{DashboardID: 42} + mockCli.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) + mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{{Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "resourceVersion": "12", + "labels": map[string]any{ + utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + }, + }, + "spec": map[string]any{ + "version": int64(5), + }, + }}}}, nil).Once() + res, err := dashboardVersionService.List(context.Background(), &query) + require.Nil(t, err) + require.Equal(t, 1, len(res.Versions)) + require.EqualValues(t, &dashver.DashboardVersionResponse{ + Versions: []*dashver.DashboardVersionDTO{{ + ID: 12, // should take rv + DashboardID: 42, + ParentVersion: 4, + Version: 5, // should take from spec + DashboardUID: "uid", + Data: simplejson.NewFromAny(map[string]any{"uid": "uid", "version": int64(5)}), + }}}, res) + }) } type FakeDashboardVersionStore struct { diff --git a/pkg/services/dashboardversion/dashverimpl/store_test.go b/pkg/services/dashboardversion/dashverimpl/store_test.go index cccd1d220ce..a8c26b1d102 100644 --- a/pkg/services/dashboardversion/dashverimpl/store_test.go +++ b/pkg/services/dashboardversion/dashverimpl/store_test.go @@ -34,7 +34,7 @@ func testIntegrationGetDashboardVersion(t *testing.T, fn getStore) { query := dashver.GetDashboardVersionQuery{ DashboardID: savedDash.ID, - Version: savedDash.Version, + Version: int64(savedDash.Version), OrgID: 1, } @@ -60,7 +60,7 @@ func testIntegrationGetDashboardVersion(t *testing.T, fn getStore) { t.Run("Attempt to get a version that doesn't exist", func(t *testing.T) { query := dashver.GetDashboardVersionQuery{ DashboardID: int64(999), - Version: 123, + Version: int64(123), OrgID: 1, } diff --git a/pkg/services/dashboardversion/dashvertest/fake.go b/pkg/services/dashboardversion/dashvertest/fake.go index 1d45c63aa3c..acef53263bc 100644 --- a/pkg/services/dashboardversion/dashvertest/fake.go +++ b/pkg/services/dashboardversion/dashvertest/fake.go @@ -10,6 +10,7 @@ type FakeDashboardVersionService struct { ExpectedDashboardVersion *dashver.DashboardVersionDTO ExpectedDashboardVersions []*dashver.DashboardVersionDTO ExpectedListDashboarVersions []*dashver.DashboardVersionDTO + ExpectedContinueToken string counter int ExpectedError error } @@ -30,6 +31,9 @@ func (f *FakeDashboardVersionService) DeleteExpired(ctx context.Context, cmd *da return f.ExpectedError } -func (f *FakeDashboardVersionService) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) ([]*dashver.DashboardVersionDTO, error) { - return f.ExpectedListDashboarVersions, f.ExpectedError +func (f *FakeDashboardVersionService) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) (*dashver.DashboardVersionResponse, error) { + return &dashver.DashboardVersionResponse{ + ContinueToken: f.ExpectedContinueToken, + Versions: f.ExpectedListDashboarVersions, + }, f.ExpectedError } diff --git a/pkg/services/dashboardversion/model.go b/pkg/services/dashboardversion/model.go index 435fc794311..c4f585dc366 100644 --- a/pkg/services/dashboardversion/model.go +++ b/pkg/services/dashboardversion/model.go @@ -52,7 +52,7 @@ type GetDashboardVersionQuery struct { DashboardID int64 DashboardUID string OrgID int64 - Version int + Version int64 } type DeleteExpiredVersionsCommand struct { @@ -60,12 +60,19 @@ type DeleteExpiredVersionsCommand struct { } type ListDashboardVersionsQuery struct { - DashboardID int64 - DashboardUID string - OrgID int64 - Limit int - Start int + DashboardID int64 + DashboardUID string + OrgID int64 + Limit int + Start int + ContinueToken string } + +type DashboardVersionResponse struct { + ContinueToken string `json:"continueToken"` + Versions []*DashboardVersionDTO `json:"versions"` +} + type DashboardVersionDTO struct { ID int64 `json:"id"` DashboardID int64 `json:"dashboardId"` @@ -94,3 +101,8 @@ type DashboardVersionMeta struct { Data *simplejson.Json `json:"data"` CreatedBy string `json:"createdBy"` } + +type DashboardVersionResponseMeta struct { + ContinueToken string `json:"continueToken"` + Versions []DashboardVersionMeta `json:"versions"` +} diff --git a/pkg/storage/unified/resource/cdk_backend.go b/pkg/storage/unified/resource/cdk_backend.go index ded2d34d7aa..eefba09d78f 100644 --- a/pkg/storage/unified/resource/cdk_backend.go +++ b/pkg/storage/unified/resource/cdk_backend.go @@ -321,6 +321,11 @@ func (c *cdkListIterator) ContinueToken() string { return fmt.Sprintf("index:%d/key:%s", c.index, c.currentKey) } +// ContinueTokenWithCurrentRV implements ListIterator. +func (c *cdkListIterator) ContinueTokenWithCurrentRV() string { + return fmt.Sprintf("index:%d/key:%s", c.index, c.currentKey) +} + // Name implements ListIterator. func (c *cdkListIterator) Name() string { return c.currentKey // TODO (parse name from key) diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index fab1e585486..58d555bd429 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -41,6 +41,9 @@ type ListIterator interface { // The token that can be used to start iterating *after* this item ContinueToken() string + // The token that can be used to start iterating *before* this item + ContinueTokenWithCurrentRV() string + // ResourceVersion of the current item ResourceVersion() int64 @@ -756,9 +759,16 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err rsp.Items = append(rsp.Items, item) if len(rsp.Items) >= int(req.Limit) || pageBytes >= maxPageBytes { t := iter.ContinueToken() + if req.Source == ListRequest_HISTORY { + // history lists in desc order, so the continue token takes the + // final RV in the list, and then will start from there in the next page, + // rather than the lists first RV + t = iter.ContinueTokenWithCurrentRV() + } if iter.Next() { rsp.NextPageToken = t } + break } } diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index d56cd87b2e1..0401c77a25c 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -519,6 +519,10 @@ func (l *listIter) ContinueToken() string { return ContinueToken{ResourceVersion: l.listRV, StartOffset: l.offset}.String() } +func (l *listIter) ContinueTokenWithCurrentRV() string { + return ContinueToken{ResourceVersion: l.rv, StartOffset: l.offset}.String() +} + func (l *listIter) Error() error { return l.err } diff --git a/pkg/storage/unified/sql/data/resource_history_get.sql b/pkg/storage/unified/sql/data/resource_history_get.sql index cbb2786e534..5ab25eb2e0c 100644 --- a/pkg/storage/unified/sql/data/resource_history_get.sql +++ b/pkg/storage/unified/sql/data/resource_history_get.sql @@ -16,6 +16,6 @@ WHERE 1 = 1 AND {{ .Ident "action" }} = 3 {{ end }} {{ if (gt .StartRV 0) }} - AND {{ .Ident "resource_version" }} > {{ .Arg .StartRV }} + AND {{ .Ident "resource_version" }} < {{ .Arg .StartRV }} {{ end }} ORDER BY resource_version DESC diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 5883610bd54..cc5cacc524b 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -351,6 +351,83 @@ func TestIntegrationBackendList(t *testing.T) { require.Equal(t, rv8, continueToken.ResourceVersion) require.Equal(t, int64(4), continueToken.StartOffset) }) + + // add 5 events for item1 - should be saved to history + rvHistory1, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory1, rv1) + rvHistory2, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory2, rvHistory1) + rvHistory3, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory3, rvHistory2) + rvHistory4, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory4, rvHistory3) + rvHistory5, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory5, rvHistory4) + + t.Run("fetch first history page at revision with limit", func(t *testing.T) { + res, err := server.List(ctx, &resource.ListRequest{ + Limit: 3, + Source: resource.ListRequest_HISTORY, + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: "namespace", + Group: "group", + Resource: "resource", + Name: "item1", + }, + }, + }) + require.NoError(t, err) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 3) + t.Log(res.Items) + // should be in desc order, so the newest RVs are returned first + require.Equal(t, "item1 MODIFIED", string(res.Items[0].Value)) + require.Equal(t, rvHistory5, res.Items[0].ResourceVersion) + require.Equal(t, "item1 MODIFIED", string(res.Items[1].Value)) + require.Equal(t, rvHistory4, res.Items[1].ResourceVersion) + require.Equal(t, "item1 MODIFIED", string(res.Items[2].Value)) + require.Equal(t, rvHistory3, res.Items[2].ResourceVersion) + + continueToken, err := sql.GetContinueToken(res.NextPageToken) + require.NoError(t, err) + // should return the furthest back RV as the next page token + require.Equal(t, rvHistory3, continueToken.ResourceVersion) + }) + + t.Run("fetch second page of history at revision", func(t *testing.T) { + continueToken := &sql.ContinueToken{ + ResourceVersion: rvHistory3, + StartOffset: 2, + } + res, err := server.List(ctx, &resource.ListRequest{ + NextPageToken: continueToken.String(), + Limit: 2, + Source: resource.ListRequest_HISTORY, + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: "namespace", + Group: "group", + Resource: "resource", + Name: "item1", + }, + }, + }) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 2) + t.Log(res.Items) + require.Equal(t, "item1 MODIFIED", string(res.Items[0].Value)) + require.Equal(t, rvHistory2, res.Items[0].ResourceVersion) + require.Equal(t, "item1 MODIFIED", string(res.Items[1].Value)) + require.Equal(t, rvHistory1, res.Items[1].ResourceVersion) + }) } func TestIntegrationBlobSupport(t *testing.T) { diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read trash second page.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read trash second page.sql index 16359886766..cd026c1dfef 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read trash second page.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read trash second page.sql @@ -10,5 +10,5 @@ WHERE 1 = 1 AND `group` = 'gg' AND `resource` = 'rr' AND `action` = 3 - AND `resource_version` > 123456 + AND `resource_version` < 123456 ORDER BY resource_version DESC diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read trash second page.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read trash second page.sql index 0bf9c65a25a..b5df5de6315 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read trash second page.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read trash second page.sql @@ -10,5 +10,5 @@ WHERE 1 = 1 AND "group" = 'gg' AND "resource" = 'rr' AND "action" = 3 - AND "resource_version" > 123456 + AND "resource_version" < 123456 ORDER BY resource_version DESC diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read trash second page.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read trash second page.sql index 0bf9c65a25a..b5df5de6315 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read trash second page.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read trash second page.sql @@ -10,5 +10,5 @@ WHERE 1 = 1 AND "group" = 'gg' AND "resource" = 'rr' AND "action" = 3 - AND "resource_version" > 123456 + AND "resource_version" < 123456 ORDER BY resource_version DESC diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index e7f9a6e7f07..3bef71caf21 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -374,7 +374,13 @@ export class DashboardScene extends SceneObjectBase { } public onRestore = async (version: DecoratedRevisionModel): Promise => { - const versionRsp = await historySrv.restoreDashboard(version.uid, version.version); + let versionRsp; + if (config.featureToggles.kubernetesCliDashboards) { + // the id here is the resource version in k8s, use this instead to get the specific version + versionRsp = await historySrv.restoreDashboard(version.uid, version.id); + } else { + versionRsp = await historySrv.restoreDashboard(version.uid, version.version); + } if (!Number.isInteger(versionRsp.version)) { return false; diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx index 6d2b37219f6..e6449d0d3f6 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx @@ -112,44 +112,47 @@ describe('VersionsEditView', () => { }); function getVersions() { - return [ - { - id: 4, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 3, - restoredFrom: 0, - version: 4, - created: '2017-02-22T17:43:01-08:00', - createdBy: 'admin', - message: '', - checked: false, - }, - { - id: 3, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 1, - restoredFrom: 1, - version: 3, - created: '2017-02-22T17:43:01-08:00', - createdBy: 'admin', - message: '', - checked: false, - }, - { - id: 2, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 1, - restoredFrom: 1, - version: 2, - created: '2017-02-23T17:43:01-08:00', - createdBy: 'admin', - message: '', - checked: false, - }, - ]; + return { + continueToken: '', + versions: [ + { + id: 4, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 3, + restoredFrom: 0, + version: 4, + created: '2017-02-22T17:43:01-08:00', + createdBy: 'admin', + message: '', + checked: false, + }, + { + id: 3, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 1, + restoredFrom: 1, + version: 3, + created: '2017-02-22T17:43:01-08:00', + createdBy: 'admin', + message: '', + checked: false, + }, + { + id: 2, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 1, + restoredFrom: 1, + version: 2, + created: '2017-02-23T17:43:01-08:00', + createdBy: 'admin', + message: '', + checked: false, + }, + ], + }; } async function buildTestScene() { diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx index c2acbb9b7af..17256382d0b 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { PageLayoutType, dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes'; import { Spinner, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; @@ -41,6 +42,7 @@ export class VersionsEditView extends SceneObjectBase imp public static Component = VersionsEditorSettingsListView; private _limit: number = VERSIONS_FETCH_LIMIT; private _start = 0; + private _continueToken = ''; constructor(state: VersionsEditViewState) { super({ @@ -102,14 +104,20 @@ export class VersionsEditView extends SceneObjectBase imp this.setState({ isAppending: append }); + const requestOptions = this._continueToken + ? { limit: this._limit, start: this._start, continueToken: this._continueToken } + : { limit: this._limit, start: this._start }; + historySrv - .getHistoryList(uid, { limit: this._limit, start: this._start }) + .getHistoryList(uid, requestOptions) .then((result) => { this.setState({ isLoading: false, - versions: [...(this.state.versions ?? []), ...this.decorateVersions(result)], + versions: [...(this.state.versions ?? []), ...this.decorateVersions(result.versions)], }); this._start += this._limit; + // Update the continueToken for the next request, if available + this._continueToken = result.continueToken ?? ''; }) .catch((err) => console.log(err)) .finally(() => this.setState({ isAppending: false })); @@ -127,9 +135,15 @@ export class VersionsEditView extends SceneObjectBase imp if (!this._dashboard.state.uid) { return; } - - const lhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, baseInfo.version); - const rhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, newInfo.version); + let lhs, rhs; + if (config.featureToggles.kubernetesCliDashboards) { + // the id here is the resource version in k8s, use this instead to get the specific version + lhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, baseInfo.id); + rhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, newInfo.id); + } else { + lhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, baseInfo.version); + rhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, newInfo.version); + } this.setState({ baseInfo, @@ -145,6 +159,7 @@ export class VersionsEditView extends SceneObjectBase imp }; public reset = () => { + this._continueToken = ''; this.setState({ baseInfo: undefined, diffData: { diff --git a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts index c2caee31a0e..84a737d5a1d 100644 --- a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts +++ b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts @@ -62,11 +62,11 @@ describe('historySrv', () => { describe('getDashboardVersion', () => { it('should return a version object for the given dashboard id and version', () => { - getMock.mockImplementation(() => Promise.resolve(versionsResponse[0])); + getMock.mockImplementation(() => Promise.resolve(versionsResponse.versions[0])); historySrv = new HistorySrv(); return historySrv.getDashboardVersion(dash.uid, 4).then((version) => { - expect(version).toEqual(versionsResponse[0]); + expect(version).toEqual(versionsResponse.versions[0]); }); }); diff --git a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts index 61312b8ad45..95cd468ec63 100644 --- a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts +++ b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts @@ -4,6 +4,7 @@ import { Dashboard } from '@grafana/schema'; export interface HistoryListOpts { limit: number; start: number; + continueToken?: string; } export interface RevisionsModel { diff --git a/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts b/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts index 0de97c47a95..3fbefb31c92 100644 --- a/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts +++ b/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts @@ -1,51 +1,54 @@ export function versions() { - return [ - { - id: 4, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 3, - restoredFrom: 0, - version: 4, - created: '2017-02-22T17:43:01-08:00', - createdBy: 'admin', - message: '', - }, - { - id: 3, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 1, - restoredFrom: 1, - version: 3, - created: '2017-02-22T17:43:01-08:00', - createdBy: 'admin', - message: '', - }, - { - id: 2, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 0, - restoredFrom: -1, - version: 2, - created: '2017-02-22T17:29:52-08:00', - createdBy: 'admin', - message: '', - }, - { - id: 1, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 0, - restoredFrom: -1, - slug: 'history-dashboard', - version: 1, - created: '2017-02-22T17:06:37-08:00', - createdBy: 'admin', - message: '', - }, - ]; + return { + continueToken: '', + versions: [ + { + id: 4, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 3, + restoredFrom: 0, + version: 4, + created: '2017-02-22T17:43:01-08:00', + createdBy: 'admin', + message: '', + }, + { + id: 3, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 1, + restoredFrom: 1, + version: 3, + created: '2017-02-22T17:43:01-08:00', + createdBy: 'admin', + message: '', + }, + { + id: 2, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 0, + restoredFrom: -1, + version: 2, + created: '2017-02-22T17:29:52-08:00', + createdBy: 'admin', + message: '', + }, + { + id: 1, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 0, + restoredFrom: -1, + slug: 'history-dashboard', + version: 1, + created: '2017-02-22T17:06:37-08:00', + createdBy: 'admin', + message: '', + }, + ], + }; } export function restore(version: number, restoredFrom?: number) { diff --git a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx index 4b0b91ef6e6..16f8d84f237 100644 --- a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx @@ -66,7 +66,7 @@ describe('VersionSettings', () => { await waitFor(() => expect(screen.getByRole('table')).toBeInTheDocument()); const tableBodyRows = within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row'); - expect(tableBodyRows.length).toBe(versions.length); + expect(tableBodyRows.length).toBe(versions.versions.length); const firstRow = within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row')[0]; @@ -76,7 +76,11 @@ describe('VersionSettings', () => { test('does not render buttons if versions === 1', async () => { // @ts-ignore - historySrv.getHistoryList.mockResolvedValue(versions.slice(0, 1)); + historySrv.getHistoryList.mockResolvedValue({ + continueToken: versions.continueToken, + versions: versions.versions.slice(0, 1), + }); + setup(); expect(screen.queryByRole('button', { name: /show more versions/i })).not.toBeInTheDocument(); @@ -90,7 +94,11 @@ describe('VersionSettings', () => { test('does not render show more button if versions < VERSIONS_FETCH_LIMIT', async () => { // @ts-ignore - historySrv.getHistoryList.mockResolvedValue(versions.slice(0, VERSIONS_FETCH_LIMIT - 5)); + historySrv.getHistoryList.mockResolvedValue({ + continueToken: versions.continueToken, + versions: versions.versions.slice(0, VERSIONS_FETCH_LIMIT - 5), + }); + setup(); expect(screen.queryByRole('button', { name: /show more versions/i })).not.toBeInTheDocument(); @@ -104,7 +112,11 @@ describe('VersionSettings', () => { test('renders buttons if versions >= VERSIONS_FETCH_LIMIT', async () => { // @ts-ignore - historySrv.getHistoryList.mockResolvedValue(versions.slice(0, VERSIONS_FETCH_LIMIT)); + historySrv.getHistoryList.mockResolvedValue({ + continueToken: versions.continueToken, + versions: versions.versions.slice(0, VERSIONS_FETCH_LIMIT), + }); + setup(); expect(screen.queryByRole('button', { name: /show more versions/i })).not.toBeInTheDocument(); @@ -124,9 +136,17 @@ describe('VersionSettings', () => { test('clicking show more appends results to the table', async () => { historySrv.getHistoryList // @ts-ignore - .mockImplementationOnce(() => Promise.resolve(versions.slice(0, VERSIONS_FETCH_LIMIT))) - .mockImplementationOnce( - () => new Promise((resolve) => setTimeout(() => resolve(versions.slice(VERSIONS_FETCH_LIMIT)), 1000)) + .mockImplementationOnce(() => + Promise.resolve({ + continueToken: versions.continueToken, + versions: versions.versions.slice(0, VERSIONS_FETCH_LIMIT), + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + continueToken: versions.continueToken, + versions: versions.versions.slice(VERSIONS_FETCH_LIMIT), + }) ); setup(); @@ -146,13 +166,16 @@ describe('VersionSettings', () => { await waitFor(() => { expect(screen.queryByText(/Fetching more entries/i)).not.toBeInTheDocument(); - expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(versions.length); + expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(versions.versions.length); }); }); test('selecting two versions and clicking compare button should render compare view', async () => { // @ts-ignore - historySrv.getHistoryList.mockResolvedValue(versions.slice(0, VERSIONS_FETCH_LIMIT)); + historySrv.getHistoryList.mockResolvedValue({ + continueToken: versions.continueToken, + versions: versions.versions.slice(0, VERSIONS_FETCH_LIMIT), + }); historySrv.getDashboardVersion // @ts-ignore .mockImplementationOnce(() => Promise.resolve(diffs.lhs)) diff --git a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx index c22f31c9437..30ccbe39708 100644 --- a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx @@ -1,6 +1,7 @@ import { PureComponent } from 'react'; import * as React from 'react'; +import { config } from '@grafana/runtime'; import { Spinner, HorizontalGroup } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { @@ -38,11 +39,13 @@ export const VERSIONS_FETCH_LIMIT = 10; export class VersionsSettings extends PureComponent { limit: number; start: number; + continueToken: string; constructor(props: Props) { super(props); this.limit = VERSIONS_FETCH_LIMIT; this.start = 0; + this.continueToken = ''; this.state = { isAppending: true, isLoading: true, @@ -62,14 +65,20 @@ export class VersionsSettings extends PureComponent { getVersions = (append = false) => { this.setState({ isAppending: append }); + const requestOptions = this.continueToken + ? { limit: this.limit, start: this.start, continueToken: this.continueToken } + : { limit: this.limit, start: this.start }; + historySrv - .getHistoryList(this.props.dashboard.uid, { limit: this.limit, start: this.start }) + .getHistoryList(this.props.dashboard.uid, requestOptions) .then((res) => { this.setState({ isLoading: false, - versions: [...this.state.versions, ...this.decorateVersions(res)], + versions: [...(this.state.versions ?? []), ...this.decorateVersions(res.versions)], }); this.start += this.limit; + // Update the continueToken for the next request, if available + this.continueToken = res.continueToken ?? ''; }) .catch((err) => console.log(err)) .finally(() => this.setState({ isAppending: false })); @@ -84,8 +93,15 @@ export class VersionsSettings extends PureComponent { isLoading: true, }); - const lhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, baseInfo.version); - const rhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, newInfo.version); + let lhs, rhs; + if (config.featureToggles.kubernetesCliDashboards) { + // the id here is the resource version in k8s, use this instead to get the specific version + lhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, baseInfo.id); + rhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, newInfo.id); + } else { + lhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, baseInfo.version); + rhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, newInfo.version); + } this.setState({ baseInfo, @@ -121,6 +137,7 @@ export class VersionsSettings extends PureComponent { }; reset = () => { + this.continueToken = ''; this.setState({ baseInfo: undefined, diffData: { diff --git a/public/app/features/dashboard/components/DashboardSettings/__mocks__/versions.ts b/public/app/features/dashboard/components/DashboardSettings/__mocks__/versions.ts index 62f3cf4d876..3b3dc5629d8 100644 --- a/public/app/features/dashboard/components/DashboardSettings/__mocks__/versions.ts +++ b/public/app/features/dashboard/components/DashboardSettings/__mocks__/versions.ts @@ -1,126 +1,129 @@ -export const versions = [ - { - id: 249, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 10, - restoredFrom: 0, - version: 11, - created: '2021-01-15T14:44:44+01:00', - createdBy: 'admin', - message: 'testing changes...', - }, - { - id: 247, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 9, - restoredFrom: 0, - version: 10, - created: '2021-01-15T10:19:17+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 246, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 8, - restoredFrom: 0, - version: 9, - created: '2021-01-15T10:18:12+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 245, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 7, - restoredFrom: 0, - version: 8, - created: '2021-01-15T10:11:16+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 239, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 6, - restoredFrom: 0, - version: 7, - created: '2021-01-14T15:14:25+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 237, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 5, - restoredFrom: 0, - version: 6, - created: '2021-01-14T14:55:29+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 236, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 4, - restoredFrom: 0, - version: 5, - created: '2021-01-14T14:28:01+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 218, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 3, - restoredFrom: 0, - version: 4, - created: '2021-01-08T10:45:33+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 217, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 2, - restoredFrom: 0, - version: 3, - created: '2021-01-05T15:41:33+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 216, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 1, - restoredFrom: 0, - version: 2, - created: '2021-01-05T15:01:50+01:00', - createdBy: 'admin', - message: '', - }, - { - id: 215, - dashboardId: 74, - dashboardUID: '_U4zObQMz', - parentVersion: 1, - restoredFrom: 0, - version: 1, - created: '2021-01-05T14:59:15+01:00', - createdBy: 'admin', - message: '', - }, -]; +export const versions = { + continueToken: '', + versions: [ + { + id: 249, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 10, + restoredFrom: 0, + version: 11, + created: '2021-01-15T14:44:44+01:00', + createdBy: 'admin', + message: 'testing changes...', + }, + { + id: 247, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 9, + restoredFrom: 0, + version: 10, + created: '2021-01-15T10:19:17+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 246, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 8, + restoredFrom: 0, + version: 9, + created: '2021-01-15T10:18:12+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 245, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 7, + restoredFrom: 0, + version: 8, + created: '2021-01-15T10:11:16+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 239, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 6, + restoredFrom: 0, + version: 7, + created: '2021-01-14T15:14:25+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 237, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 5, + restoredFrom: 0, + version: 6, + created: '2021-01-14T14:55:29+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 236, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 4, + restoredFrom: 0, + version: 5, + created: '2021-01-14T14:28:01+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 218, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 3, + restoredFrom: 0, + version: 4, + created: '2021-01-08T10:45:33+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 217, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 2, + restoredFrom: 0, + version: 3, + created: '2021-01-05T15:41:33+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 216, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 1, + restoredFrom: 0, + version: 2, + created: '2021-01-05T15:01:50+01:00', + createdBy: 'admin', + message: '', + }, + { + id: 215, + dashboardId: 74, + dashboardUID: '_U4zObQMz', + parentVersion: 1, + restoredFrom: 0, + version: 1, + created: '2021-01-05T14:59:15+01:00', + createdBy: 'admin', + message: '', + }, + ], +}; export const diffs = { lhs: { diff --git a/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx b/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx index 5a4451b4596..955d45388c4 100644 --- a/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx +++ b/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx @@ -5,12 +5,13 @@ import { ConfirmModal } from '@grafana/ui'; import { useDashboardRestore } from './useDashboardRestore'; export interface RevertDashboardModalProps { hideModal: () => void; + id: number; version: number; } -export const RevertDashboardModal = ({ hideModal, version }: RevertDashboardModalProps) => { +export const RevertDashboardModal = ({ hideModal, id, version }: RevertDashboardModalProps) => { // TODO: how should state.error be handled? - const { state, onRestoreDashboard } = useDashboardRestore(version); + const { state, onRestoreDashboard } = useDashboardRestore(id, version); useEffect(() => { if (!state.loading && state.value) { diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx index 2f23cd32e17..72294e80941 100644 --- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx +++ b/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx @@ -43,6 +43,7 @@ export const VersionHistoryComparison = ({ baseInfo, newInfo, diffData, isNewLat icon="history" onClick={() => { showModal(RevertDashboardModal, { + id: baseInfo.id, version: baseInfo.version, hideModal, }); diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx index 2cd8514992a..5edd119e490 100644 --- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx +++ b/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx @@ -60,6 +60,7 @@ export const VersionHistoryTable = ({ versions, canCompare, onCheck }: VersionsT icon="history" onClick={() => { showModal(RevertDashboardModal, { + id: version.id, version: version.version, hideModal, }); diff --git a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx index 4ee340a8110..91fd0964aca 100644 --- a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx +++ b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { useAsyncFn } from 'react-use'; import { locationUtil } from '@grafana/data'; -import { locationService } from '@grafana/runtime'; +import { config, locationService } from '@grafana/runtime'; import { useAppNotification } from 'app/core/copy/appNotification'; import { historySrv } from 'app/features/dashboard-scene/settings/version-history'; import { useSelector } from 'app/types'; @@ -16,9 +16,12 @@ const restoreDashboard = async (version: number, dashboard: DashboardModel) => { return await historySrv.restoreDashboard(dashboard.uid, version); }; -export const useDashboardRestore = (version: number) => { +export const useDashboardRestore = (id: number, version: number) => { const dashboard = useSelector((state) => state.dashboard.getModel()); - const [state, onRestoreDashboard] = useAsyncFn(async () => await restoreDashboard(version, dashboard!), []); + const [state, onRestoreDashboard] = useAsyncFn( + async () => await restoreDashboard(config.featureToggles.kubernetesCliDashboards ? id : version, dashboard!), + [] + ); const notifyApp = useAppNotification(); useEffect(() => { From fe49b6279adbc3d4037a54db2f8ef2b8dc0a0df1 Mon Sep 17 00:00:00 2001 From: Mitch Seaman Date: Tue, 28 Jan 2025 15:38:17 +0100 Subject: [PATCH 150/894] docs: update enterprise docs with license token renewal details (#99662) --- .../administration/enterprise-licensing/_index.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/sources/administration/enterprise-licensing/_index.md b/docs/sources/administration/enterprise-licensing/_index.md index e42696f97ec..b532bcf74db 100644 --- a/docs/sources/administration/enterprise-licensing/_index.md +++ b/docs/sources/administration/enterprise-licensing/_index.md @@ -246,6 +246,14 @@ Your license is controlled by the following rules: As the license expiration date approaches, you will see a banner in Grafana that encourages you to renew. To learn about how to renew your license and what happens in Grafana when a license expires, refer to [License expiration]({{< relref "#license-expiration" >}}). +**License token expiration:** Your license must contain a valid token, which renews periodically. + +A license token is a digital key that activates your license. By default, license tokens renew every 7 days by calling the Grafana.com API. Short-lived license tokens enable more frequent validation that licenses are compliant, and allow for more frequent license updates - for example, adding users or invalidating a compromised license. + +To view the details of your license token, sign in to Grafana Enterprise as a Server Administrator and visit **Administration** > **General** > **Statistics and licensing**. Token details are in the Token section under License Details. + +License token renewal requires internet access, and requires that the `auto_refresh_license` [configuration setting](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/enterprise-configuration/#auto_refresh_license) be set to `true`. If your Grafana Enterprise instance cannot connect to the internet, contact your Grafana Labs account team for additional options for token renewal and license audit. + **Grafana License URL:** Your license does not work with an instance of Grafana with a different root URL. The License URL is the complete URL of your Grafana instance, for example `https://grafana.your-company.com/`. It is defined in the [root_url]({{< relref "../../setup-grafana/configure-grafana/#root_url" >}}) configuration setting. @@ -264,7 +272,7 @@ For instructions about how to activate your license after it is updated, refer t ## Usage billing -Standard Grafana Enterprise licenses include a certain number of seats that can be used, and prevent more users logging into Grafana than have been licensed. This makes sense if you prefer a predictable bill. It can however be a problem if you anticipate uneven usage patterns over time or when it's critical that no user ever be prevented from logging into Grafana due to capacity constraints. +Standard Grafana Enterprise licenses include a certain number of seats that can be used, and prevent more users logging into Grafana than have been licensed. This makes sense if you prefer a predictable bill. It can however be a problem if you anticipate uneven usage patterns over time or when it's critical that no user ever be prevented from logging in to Grafana due to capacity constraints. For those use-cases we support usage-based billing, where your license includes a certain number of included users and you are billed on a monthly basis for any excess active users during the month. From 0a805a23144cc4250c71543db819dad37129687a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:40:21 +0200 Subject: [PATCH 151/894] Update dependency moment-timezone to v0.5.47 (#99657) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 15 ++++++++++++--- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 92651a8dbd1..f0a249ff633 100644 --- a/package.json +++ b/package.json @@ -349,7 +349,7 @@ "ml-regression-polynomial": "^3.0.0", "ml-regression-simple-linear": "^3.0.0", "moment": "2.30.1", - "moment-timezone": "0.5.46", + "moment-timezone": "0.5.47", "monaco-editor": "0.34.1", "moveable": "0.53.0", "nanoid": "^5.0.4", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index ff3a9928802..2d5576fa453 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -49,7 +49,7 @@ "marked": "15.0.6", "marked-mangle": "1.1.10", "moment": "2.30.1", - "moment-timezone": "0.5.46", + "moment-timezone": "0.5.47", "ol": "7.4.0", "papaparse": "5.5.2", "react-use": "17.6.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 3f3060e08cc..695108bb59f 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -61,7 +61,7 @@ "marked": "15.0.6", "marked-mangle": "1.1.10", "moment": "2.30.1", - "moment-timezone": "0.5.46", + "moment-timezone": "0.5.47", "monaco-promql": "1.7.4", "pluralize": "8.0.0", "prismjs": "1.29.0", diff --git a/yarn.lock b/yarn.lock index 2714fa57eea..5bab6cc7238 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3223,7 +3223,7 @@ __metadata: marked: "npm:15.0.6" marked-mangle: "npm:1.1.10" moment: "npm:2.30.1" - moment-timezone: "npm:0.5.46" + moment-timezone: "npm:0.5.47" ol: "npm:7.4.0" papaparse: "npm:5.5.2" react: "npm:18.3.1" @@ -3666,7 +3666,7 @@ __metadata: marked: "npm:15.0.6" marked-mangle: "npm:1.1.10" moment: "npm:2.30.1" - moment-timezone: "npm:0.5.46" + moment-timezone: "npm:0.5.47" monaco-promql: "npm:1.7.4" pluralize: "npm:8.0.0" prettier: "npm:3.4.2" @@ -17959,7 +17959,7 @@ __metadata: ml-regression-polynomial: "npm:^3.0.0" ml-regression-simple-linear: "npm:^3.0.0" moment: "npm:2.30.1" - moment-timezone: "npm:0.5.46" + moment-timezone: "npm:0.5.47" monaco-editor: "npm:0.34.1" moveable: "npm:0.53.0" msw: "npm:2.7.0" @@ -22433,6 +22433,15 @@ __metadata: languageName: node linkType: hard +"moment-timezone@npm:0.5.47": + version: 0.5.47 + resolution: "moment-timezone@npm:0.5.47" + dependencies: + moment: "npm:^2.29.4" + checksum: 10/b2ad32e6b7ea4ce99756c9f571f8eb1bdd6592ecdc2bf3f7fb988ae980a49a7e87b986776090571b9608c8576205f839b736dc7589c180ce36c82fad4b8416c6 + languageName: node + linkType: hard + "moment@npm:2.30.1, moment@npm:2.x, moment@npm:^2.20.1, moment@npm:^2.29.4, moment@npm:^2.30.1": version: 2.30.1 resolution: "moment@npm:2.30.1" From 78848095ca64eaca228144b6d539e79b8bcf9728 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:01:32 +0200 Subject: [PATCH 152/894] Update dependency swagger-ui-react to v5.18.3 (#99669) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 222 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 133 insertions(+), 91 deletions(-) diff --git a/package.json b/package.json index f0a249ff633..257f9e62616 100644 --- a/package.json +++ b/package.json @@ -400,7 +400,7 @@ "slate": "0.47.9", "slate-plain-serializer": "0.7.13", "slate-react": "0.22.10", - "swagger-ui-react": "5.18.2", + "swagger-ui-react": "5.18.3", "symbol-observable": "4.0.0", "systemjs": "6.15.1", "systemjs-cjs-extra": "0.2.1", diff --git a/yarn.lock b/yarn.lock index 5bab6cc7238..c0a3fe7677e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7977,57 +7977,57 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ast@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.5" +"@swagger-api/apidom-ast@npm:^1.0.0-beta.11, @swagger-api/apidom-ast@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" unraw: "npm:^3.0.0" - checksum: 10/b2ea32b8ed589a3aff122e9209d5f0c873364bb34b234d13796422d4fce6b9f52fab599ef47956f655316256cf6af821c233117c0ca96a677b867e075b70cb5d + checksum: 10/efe7caf37735f6b1a9b56ca780ce59134540c00c50ef00606607a458905142b7794466cacbffdcd9afe11ece9f5a99651f2f62d24b05f71727d86718e2dea91c languageName: node linkType: hard -"@swagger-api/apidom-core@npm:>=1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-core@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-core@npm:1.0.0-beta.5" +"@swagger-api/apidom-core@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-core@npm:^1.0.0-beta.11, @swagger-api/apidom-core@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-core@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" minim: "npm:~0.23.8" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" short-unique-id: "npm:^5.0.2" ts-mixer: "npm:^6.0.3" - checksum: 10/c034ef286738b2b5aab525b068fd22e1b54145e3024477abcafde926f3783c280c69e4de23cc28d9cc568a62fb02719a0a89e6fd2011136cf447f42ed66fca55 + checksum: 10/bb81c1ef603b80985c8e7ad8808a12eaaf2daa3de2a41ac2f9f8c041c7a7f26bcbfa9713738248db198af1a6b424127b10abd5ac7e6fcad95d852cea8fb8a526 languageName: node linkType: hard -"@swagger-api/apidom-error@npm:>=1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-error@npm:1.0.0-beta.5" +"@swagger-api/apidom-error@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.11, @swagger-api/apidom-error@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-error@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - checksum: 10/defb3ba3775be8a511ff01be4ed7d2eca66faf5ab478f65a39845c8981510a0286e622268240de56613fd3ee37de906a7c6947a82aceb214fdd89d0988b972bb + checksum: 10/d63ec68067a17c10fa51b2929c18d18f397d91ab45961e398ad99341b45e87d60a0ec6f634ac3883733ea7f4a5155e9929207dd44572f65754fa0f5b7d004466 languageName: node linkType: hard -"@swagger-api/apidom-json-pointer@npm:>=1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-json-pointer@npm:1.0.0-beta.5" +"@swagger-api/apidom-json-pointer@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.11, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.3 <1.0.0-rc.0": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-json-pointer@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/68b3b196de1d2cb86663350ba70f885599a12c21804cd9bc1be1777ee5e99e9f570623599d476d241bda298e0890c34d2c9a516f7a47cf060992107c31e9d9f5 + checksum: 10/dee858ee4b4a0f93ab082451a703c8b9e0250f7b486d8d9ef243b77f92acc49741cfdd1e10a9cc44d946b64e469aa4016a1380c7a28d660fe048a2897098afc7 languageName: node linkType: hard @@ -8062,50 +8062,82 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.0.0-beta.5" +"@swagger-api/apidom-ns-json-schema-2019-09@npm:^1.0.0-beta.11": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-2019-09@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-7": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.4" - checksum: 10/2518b3aa9b7387a90553565132101616dc24c6d1f8aee581f3562bac87403f87887217fdb3cf17b113f685e051980fc66ec25d9cab7e4a0bf4922b4bcd3bc502 + checksum: 10/b91f90e7376922f1112752520bcac265c20245c1d2d682cd5ba9344a3eb7287962f18e3430f501af4d4db43397a2977267b15ef02a3bcd6e8ba384901e195672 languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.0.0-beta.5" +"@swagger-api/apidom-ns-json-schema-2020-12@npm:^1.0.0-beta.11": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-2020-12@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-2019-09": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.4" - checksum: 10/c439c3679dc6ea1807affa0a2913deea52ff8d63b48722c741458da147e849be9acc0c13955757a7b6904a4e7148cdb7845291da758a085083c61610a96fc36f + checksum: 10/847103b0923fa7a0fcff124f5359f735f56e38a88666051e5959403e0060e33bea63b30098dec99d36bcda6619b2a3d13ce61c26e6ca03c508ea8518e871c123 languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.0.0-beta.5" +"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-json-schema-draft-6": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.4" - checksum: 10/28c8f989e26a453f2ca6c655d6822d63f7bc57137375c460c707caa964a5097566969031f322d772b49de0ce2924aeb69e2c5a6ff66191b457844e68a40f013e + checksum: 10/9e0ba9e29849915c0067227a82d8d9a741221b3ac73592b73e23d3cfe0c57548b21cbbb66cdeba914218c5f675f4ec497232d465880013f8aec8f486e4c3d91c + languageName: node + linkType: hard + +"@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.0.0-beta.11": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/0df53880202cdb8dbe1e534314842c2c10fe4cd03f6eecd56d67174492807df03134f360e1c0724610fc4531520c49bb41d81e6fd7373118031ad883e0101338 + languageName: node + linkType: hard + +"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-6": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/9ef5261e58b2e6798a3c22ad01940c613475a6515d2cd79d2f150b02152118036ef472bb75a271c0d1c3b617d51ccb2262680469824335f27150600f695eb6d8 languageName: node linkType: hard @@ -8125,36 +8157,37 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:1.0.0-beta.5" +"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/5bb9c191a3d79d9a69aa04deb5dd6ce2eaa09700210f5a89fdeb0668ec76eb3f2d4a26950405f001459e59ad199ddd4632251992b9fc1b9fa03955e1ad600810 + checksum: 10/44eabda02fb8ad965b7756931bba7ac2ae9f49ef720f401179efb9eafbf94ff6b3c8cb8fd260c9ddb1affe35c3876f780cef4cb090b755736847adb7eba699c8 languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-1@npm:>=1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.0.0-beta.5" +"@swagger-api/apidom-ns-openapi-3-1@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-json-pointer": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-json-pointer": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-2020-12": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-beta.11" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/69018147465c78a25efc5e7bc4439561c2d620c65c9cf86bdc503dbad9b40e52db77bb0cb11800a4c34bdc88d462126e3dc355bad290cdfe7c9fbeda391405c8 + checksum: 10/6ed3a3d7bc231c8f3273c60dcd459cdb117246e372ed9d0d2483601a966a3109b9617a4d8f327f494a31013d5bcf2bc372c5f3297bbf953c61ff4770c87414a5 languageName: node linkType: hard @@ -8391,12 +8424,12 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-reference@npm:>=1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-reference@npm:1.0.0-beta.5" +"@swagger-api/apidom-reference@npm:>=1.0.0-beta.11 <1.0.0-rc.0": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-reference@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" "@swagger-api/apidom-error": "npm:^1.0.0-beta.3 <1.0.0-rc.0" "@swagger-api/apidom-json-pointer": "npm:^1.0.0-beta.3 <1.0.0-rc.0" "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.0.0-beta.3 <1.0.0-rc.0" @@ -8467,7 +8500,16 @@ __metadata: optional: true "@swagger-api/apidom-parser-adapter-yaml-1-2": optional: true - checksum: 10/e118278a4a460545de4e393aecaef16f92828cc0af02f4b9b3e8773b9f61c495e0afbce97e68ebd901858dc4b73a178aec2ec18fc1cec2a54d3ccbd3f6bc5331 + checksum: 10/1ae856fd2d13884f8f063421c010f166da1602075ae3b08c041c98199c724615b389110672e2c1b129b7b551a21b8738d32df87c066cc105669a03b6b21897e6 + languageName: node + linkType: hard + +"@swaggerexpert/cookie@npm:^1.4.1": + version: 1.4.1 + resolution: "@swaggerexpert/cookie@npm:1.4.1" + dependencies: + apg-lite: "npm:^1.0.3" + checksum: 10/936590cb70fb7af4ec988e7ee7c5e965b66d09046d3fd2b52c140f9777cda1452f2f24d80eb64ff9678b79f566dd0ef51699050aea9c9a3fd5345dcc73a3fa7a languageName: node linkType: hard @@ -11269,7 +11311,7 @@ __metadata: languageName: node linkType: hard -"apg-lite@npm:^1.0.3": +"apg-lite@npm:^1.0.3, apg-lite@npm:^1.0.4": version: 1.0.4 resolution: "apg-lite@npm:1.0.4" checksum: 10/9c5eb431497415b738e332e5805f836c64ac9b75a399afaec0354859f3f44c95e203fd5c7b8fee9f28fe1e184dd30f6073ed9df062849c3df03c51624901a8a6 @@ -18032,7 +18074,7 @@ __metadata: style-loader: "npm:4.0.0" stylelint: "npm:16.13.2" stylelint-config-sass-guidelines: "npm:12.1.0" - swagger-ui-react: "npm:5.18.2" + swagger-ui-react: "npm:5.18.3" symbol-observable: "npm:4.0.0" systemjs: "npm:6.15.1" systemjs-cjs-extra: "npm:0.2.1" @@ -23443,21 +23485,21 @@ __metadata: languageName: node linkType: hard -"openapi-path-templating@npm:^1.5.1": - version: 1.6.0 - resolution: "openapi-path-templating@npm:1.6.0" +"openapi-path-templating@npm:^2.0.1": + version: 2.1.0 + resolution: "openapi-path-templating@npm:2.1.0" dependencies: - apg-lite: "npm:^1.0.3" - checksum: 10/35353a0ce712dd79b7a60beca75d6a67fb6ad4bbdac4ceef87a6ab78d916f88dd6b426bd81e8ecf12c51798221fb15e63da8f9cadb1bcab4bc7aace8e8a266c1 + apg-lite: "npm:^1.0.4" + checksum: 10/de3ba30a19cc4bed5ace5dad0314bea66e09689001bd3510224a441f7ced53d854655daea846f884ed024d90a8f2af2d4bd0f28256dbda0c49b337684a802da1 languageName: node linkType: hard -"openapi-server-url-templating@npm:^1.0.0": - version: 1.1.0 - resolution: "openapi-server-url-templating@npm:1.1.0" +"openapi-server-url-templating@npm:^1.2.0": + version: 1.3.0 + resolution: "openapi-server-url-templating@npm:1.3.0" dependencies: - apg-lite: "npm:^1.0.3" - checksum: 10/932f08f390269506e1ea1d24208319f0f2a90023368562f1ce167a30d4199f0e39236e17f250749359b4c7b64d86c510e68e8eb9c7f6168da35f0475534ef3ed + apg-lite: "npm:^1.0.4" + checksum: 10/4a98f67cedc0958d3a30cc6db91f35970b0aeae113d0403639adbe3f5e123ff02abebc0468c10639bd0d6db165a9bb0f21c4e367836e984c2345a5a4213620a9 languageName: node linkType: hard @@ -29373,35 +29415,35 @@ __metadata: languageName: node linkType: hard -"swagger-client@npm:^3.31.0": - version: 3.32.2 - resolution: "swagger-client@npm:3.32.2" +"swagger-client@npm:^3.34.0": + version: 3.34.0 + resolution: "swagger-client@npm:3.34.0" dependencies: "@babel/runtime-corejs3": "npm:^7.22.15" "@scarf/scarf": "npm:=1.4.0" - "@swagger-api/apidom-core": "npm:>=1.0.0-beta.3 <1.0.0-rc.0" - "@swagger-api/apidom-error": "npm:>=1.0.0-beta.3 <1.0.0-rc.0" - "@swagger-api/apidom-json-pointer": "npm:>=1.0.0-beta.3 <1.0.0-rc.0" - "@swagger-api/apidom-ns-openapi-3-1": "npm:>=1.0.0-beta.3 <1.0.0-rc.0" - "@swagger-api/apidom-reference": "npm:>=1.0.0-beta.3 <1.0.0-rc.0" - cookie: "npm:~0.7.2" + "@swagger-api/apidom-core": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-error": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-json-pointer": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-ns-openapi-3-1": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-reference": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swaggerexpert/cookie": "npm:^1.4.1" deepmerge: "npm:~4.3.0" fast-json-patch: "npm:^3.0.0-1" js-yaml: "npm:^4.1.0" neotraverse: "npm:=0.6.18" node-abort-controller: "npm:^3.1.1" node-fetch-commonjs: "npm:^3.3.2" - openapi-path-templating: "npm:^1.5.1" - openapi-server-url-templating: "npm:^1.0.0" + openapi-path-templating: "npm:^2.0.1" + openapi-server-url-templating: "npm:^1.2.0" ramda: "npm:^0.30.1" ramda-adjunct: "npm:^5.0.0" - checksum: 10/651f8e6446b57c42de61684440e854f146c9e6a0d9eab28892d07a00bc0dd9e742205c285d4db2dea85d657c62da3a81a44cf417db2383753a75d69377877253 + checksum: 10/ed89c44ba172abb9cd6a36a189cf2810ef9b1983cb71657772ede94ea804105b07549b90ba36bbead87e4ac71e609b51f0977d9f9c90112952756c7f4f8bde82 languageName: node linkType: hard -"swagger-ui-react@npm:5.18.2": - version: 5.18.2 - resolution: "swagger-ui-react@npm:5.18.2" +"swagger-ui-react@npm:5.18.3": + version: 5.18.3 + resolution: "swagger-ui-react@npm:5.18.3" dependencies: "@babel/runtime-corejs3": "npm:^7.24.7" "@braintree/sanitize-url": "npm:=7.0.4" @@ -29432,7 +29474,7 @@ __metadata: reselect: "npm:^5.1.1" serialize-error: "npm:^8.1.0" sha.js: "npm:^2.4.11" - swagger-client: "npm:^3.31.0" + swagger-client: "npm:^3.34.0" url-parse: "npm:^1.5.10" xml: "npm:=1.0.1" xml-but-prettier: "npm:^1.0.1" @@ -29440,7 +29482,7 @@ __metadata: peerDependencies: react: ">=16.8.0 <19" react-dom: ">=16.8.0 <19" - checksum: 10/04d4bd8e67ac19472d7915ed779933108b660babd14740c74efa33c76a6bd17e94ebcabd4449e365da3527937f3c60f53c1c392c3e13e5ac5f2e984e6cdcee42 + checksum: 10/4437fc3fd9a869ab3ef357c78137e20fc9d96dbc71332e9ac53b45decd41382c9467d610ebf0c15a628485c67475a8f89c37882ca548305d633e2dd4b632db5f languageName: node linkType: hard From 1fb1f8846f04856b85584b81a28a05880ca19dec Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 28 Jan 2025 09:16:42 -0600 Subject: [PATCH 153/894] Docs: What's new & Upgrade guide v11.5 (#99341) Co-authored-by: Jack Baldry Co-authored-by: Robby Milo Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Co-authored-by: Isabel Matwawana --- .../upgrade-guide/upgrade-v11.5/index.md | 22 +++++++ docs/sources/whatsnew/_index.md | 1 + docs/sources/whatsnew/whats-new-in-v11-5.md | 62 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 docs/sources/upgrade-guide/upgrade-v11.5/index.md create mode 100644 docs/sources/whatsnew/whats-new-in-v11-5.md diff --git a/docs/sources/upgrade-guide/upgrade-v11.5/index.md b/docs/sources/upgrade-guide/upgrade-v11.5/index.md new file mode 100644 index 00000000000..633dfa3b7e0 --- /dev/null +++ b/docs/sources/upgrade-guide/upgrade-v11.5/index.md @@ -0,0 +1,22 @@ +--- +description: Guide for upgrading to Grafana v11.5 +keywords: + - grafana + - configuration + - documentation + - upgrade + - '11.5' +title: Upgrade to Grafana v11.5 +menuTitle: Upgrade to v11.5 +weight: 700 +--- + +# Upgrade to Grafana v11.5 + +{{< docs/shared lookup="upgrade/intro.md" source="grafana" version="" >}} + +{{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} + +{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} + +## Technical notes diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index 0de15d17301..db397d8a537 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -76,6 +76,7 @@ For a complete list of every change, with links to pull requests and related iss ## Grafana 11 +- [What's new in 11.5](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-5/) - [What's new in 11.4](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-4/) - [What's new in 11.3](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-3/) - [What's new in 11.2](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v11-2/) diff --git a/docs/sources/whatsnew/whats-new-in-v11-5.md b/docs/sources/whatsnew/whats-new-in-v11-5.md new file mode 100644 index 00000000000..f532c4a82cb --- /dev/null +++ b/docs/sources/whatsnew/whats-new-in-v11-5.md @@ -0,0 +1,62 @@ +--- +description: Feature and improvement highlights for Grafana v11.5 +keywords: + - grafana + - new + - documentation + - '11.5' + - release notes +labels: +products: + - cloud + - enterprise + - oss +title: What's new in Grafana v11.5 +posts: + - title: Cloud Migration Assistant + items: + - docs/grafana-cloud/whats-new/2025-01-10-grafana-cloud-migration-assistant-supports-all-plugins-and-grafana-alerting.md + - title: Dashboards and visualizations + items: + - docs/grafana-cloud/whats-new/2024-10-16-redesigned-ad-hoc-filters-for-dashboards.md + - docs/grafana-cloud/whats-new/2024-11-19-new-regular-expression-option-for-extract-fields-transformation.md + - docs/grafana-cloud/whats-new/2024-09-04-sharing-drawer.md + - docs/grafana-cloud/whats-new/2024-12-16-customizable-shareable-dashboard-panel-images.md + - title: Reporting + items: + - docs/grafana-cloud/whats-new/2024-10-21-theme-options-for-reporting.md + - docs/grafana-cloud/whats-new/2024-12-02-pdf-export-improvements-in-ga.md + - title: Alerting + items: + - docs/grafana-cloud/whats-new/2025-01-22-rbac-for-alerting-notifications.md + - docs/grafana-cloud/whats-new/2025-01-22-rbac-for-notification-policies.md + - title: Data sources + items: + - docs/grafana-cloud/whats-new/2025-01-09-elasticsearch-cross-cluster-search-support.md + - docs/grafana-cloud/whats-new/2024-11-12-open-search-datasource-now-supports-private-datasource-connect.md + - docs/grafana-cloud/whats-new/2024-12-04-time-series-macro-support-in-visual-query-builder-for-sql-data-sources.md + - title: Authentication and authorization + items: + - docs/grafana-cloud/whats-new/2025-01-07-oauth-and-saml-session-handling-improvements.md + - title: Plugins + items: + - docs/grafana-cloud/whats-new/2025-01-10-plugin-frontend-sandbox.md + - title: Public dashboards + items: + - docs/grafana-cloud/whats-new/2024-09-09-public-dashboards-are-now-shared-dashboards.md +whats_new_grafana_version: 11.5 +weight: -47 +--- + +# What’s new in Grafana v11.5 + +Welcome to Grafana 11.5! +Read on to learn about new sharing, reporting, and export options, cross-cluster search for Elasticsearch, PDC support for several new data sources, and more. +The Grafana Cloud Migration Assistant is in public preview and now supports all plugins and Grafana Alerts, in addition to dashboards, folders, and data sources. +We've also made it more secure to run third-party apps and data sources, and improved user session handling for OAuth 2.0 and SAML. + +{{< youtube id="RGiktzfhRd0" >}} + +For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). For the specific steps we recommend when you upgrade to v11.5, check out our [Upgrade Guide](https://grafana.com/docs/grafana//upgrade-guide/upgrade-v11.5/). + +{{< docs/whats-new >}} From 6e8e320acec2e427a75a790b9298c2f6eef0ccb3 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 28 Jan 2025 16:00:54 +0000 Subject: [PATCH 154/894] Chore: emit event whenever the theme changes (#99672) emit event whenever the theme changes --- .../core/components/SharedPreferences/SharedPreferences.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 462e38863a5..a24bd4b027b 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -120,6 +120,10 @@ export class SharedPreferences extends PureComponent { onThemeChanged = (value: ComboboxOption) => { this.setState({ theme: value.value }); + reportInteraction('grafana_preferences_theme_changed', { + toTheme: value.value, + preferenceType: this.props.preferenceType, + }); if (value.value) { changeTheme(value.value, true); From d81b1bf803afb71be0cf0ef09c671a97a765ccf9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:37:22 +0200 Subject: [PATCH 155/894] Update scenes to v5.41.0 (#99684) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- yarn.lock | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 257f9e62616..ec79d22d1e5 100644 --- a/package.json +++ b/package.json @@ -271,8 +271,8 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "5.38.0", - "@grafana/scenes-react": "5.38.0", + "@grafana/scenes": "5.41.0", + "@grafana/scenes-react": "5.41.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/yarn.lock b/yarn.lock index c0a3fe7677e..62be189a3a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3799,11 +3799,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:5.38.0": - version: 5.38.0 - resolution: "@grafana/scenes-react@npm:5.38.0" +"@grafana/scenes-react@npm:5.41.0": + version: 5.41.0 + resolution: "@grafana/scenes-react@npm:5.41.0" dependencies: - "@grafana/scenes": "npm:5.38.0" + "@grafana/scenes": "npm:5.41.0" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3814,13 +3814,13 @@ __metadata: "@grafana/ui": ^11.0.0 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/f130dabfd82abeb232f5b9d485102d03d19a7b0a91924899f6df25ee79e093d216738c9c9b12cd0ae0378eeae91dc1a0b06dadc3c9bda80ab7a2f855fb1cdac9 + checksum: 10/80d5b51a190ed962e17468c0646e9b48a52133c3af026f8bc599c5e8e1150bcd0210f7973dd13e28f96a300798a1c2a1ec41c1bcedc580d8da393591d1e69eb4 languageName: node linkType: hard -"@grafana/scenes@npm:5.38.0": - version: 5.38.0 - resolution: "@grafana/scenes@npm:5.38.0" +"@grafana/scenes@npm:5.41.0": + version: 5.41.0 + resolution: "@grafana/scenes@npm:5.41.0" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3837,7 +3837,7 @@ __metadata: "@grafana/ui": ">=10.4" react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/ed6f67e1d54ec49df77cf5fce55e13402c84755dc2f2ef09650fa44c3783dd7f18d08de26721dd4ba5f51ac02187f2c35d27023d75deb9d71c8599490f05eaf8 + checksum: 10/a77b57688b5ac518fa1f3842a871ad21e2daf840d670abda6b69facce74bb6761195c43f25507a3dff87c8551625a979c4240debe62832834473b67145b5d589 languageName: node linkType: hard @@ -17794,8 +17794,8 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:5.38.0" - "@grafana/scenes-react": "npm:5.38.0" + "@grafana/scenes": "npm:5.41.0" + "@grafana/scenes-react": "npm:5.41.0" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0" From 94a844977e9fa19a6d9c1cff13bef5ea68045d0e Mon Sep 17 00:00:00 2001 From: "Arati R." <33031346+suntala@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:46:07 +0100 Subject: [PATCH 156/894] Folders/K8s: Fix createdBy and updatedBy fields in response (#99569) --- pkg/registry/apis/folders/conversions.go | 49 --------- pkg/services/folder/folderimpl/conversions.go | 104 ++++++++++++++++++ .../folder/folderimpl}/conversions_test.go | 18 ++- pkg/services/folder/folderimpl/folder.go | 4 +- .../folderimpl/folder_unifiedstorage_test.go | 7 +- .../folder/folderimpl/unifiedstore.go | 33 +++--- 6 files changed, 143 insertions(+), 72 deletions(-) create mode 100644 pkg/services/folder/folderimpl/conversions.go rename pkg/{registry/apis/folders => services/folder/folderimpl}/conversions_test.go (76%) diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index d5e2f50901d..e3af9a28d9a 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -6,13 +6,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" - "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/util" ) @@ -41,52 +38,6 @@ func LegacyCreateCommandToUnstructured(cmd *folder.CreateFolderCommand) (*unstru return obj, nil } -func UnstructuredToLegacyFolder(item *unstructured.Unstructured) (*folder.Folder, error) { - meta, err := utils.MetaAccessor(item) - if err != nil { - return nil, err - } - - info, _ := authlib.ParseNamespace(meta.GetNamespace()) - if info.OrgID < 0 { - info.OrgID = 1 // This resolves all test cases that assume org 1 - } - - title, _, _ := unstructured.NestedString(item.Object, "spec", "title") - description, _, _ := unstructured.NestedString(item.Object, "spec", "description") - - uid := meta.GetName() - url := "" - if uid != folder.RootFolder.UID { - slug := slugify.Slugify(title) - url = dashboards.GetFolderURL(uid, slug) - } - - created := meta.GetCreationTimestamp().Time.UTC() - updated, _ := meta.GetUpdatedTimestamp() - if updated == nil { - updated = &created - } else { - tmp := updated.UTC() - updated = &tmp - } - - return &folder.Folder{ - UID: uid, - Title: title, - Description: description, - ID: meta.GetDeprecatedInternalID(), // nolint:staticcheck - ParentUID: meta.GetFolder(), - Version: int(meta.GetGeneration()), - Repository: meta.GetRepositoryName(), - - URL: url, - Created: created, - Updated: *updated, - OrgID: info.OrgID, - }, nil -} - func LegacyFolderToUnstructured(v *folder.Folder, namespacer request.NamespaceMapper) (*v0alpha1.Folder, error) { return convertToK8sResource(v, namespacer) } diff --git a/pkg/services/folder/folderimpl/conversions.go b/pkg/services/folder/folderimpl/conversions.go new file mode 100644 index 00000000000..29ef06d14fc --- /dev/null +++ b/pkg/services/folder/folderimpl/conversions.go @@ -0,0 +1,104 @@ +package folderimpl + +import ( + "context" + "errors" + "strconv" + "strings" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/user" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func (ss *FolderUnifiedStoreImpl) UnstructuredToLegacyFolder(ctx context.Context, item *unstructured.Unstructured) (*folder.Folder, error) { + meta, err := utils.MetaAccessor(item) + if err != nil { + return nil, err + } + + info, _ := authlib.ParseNamespace(meta.GetNamespace()) + if info.OrgID < 0 { + info.OrgID = 1 // This resolves all test cases that assume org 1 + } + + title, _, _ := unstructured.NestedString(item.Object, "spec", "title") + description, _, _ := unstructured.NestedString(item.Object, "spec", "description") + + uid := meta.GetName() + url := "" + if uid != folder.RootFolder.UID { + slug := slugify.Slugify(title) + url = dashboards.GetFolderURL(uid, slug) + } + + created := meta.GetCreationTimestamp().Time.UTC() + updated, _ := meta.GetUpdatedTimestamp() + if updated == nil { + updated = &created + } else { + tmp := updated.UTC() + updated = &tmp + } + + creator, err := ss.getUserFromMeta(ctx, meta.GetCreatedBy()) + if err != nil { + return nil, err + } + + updater, err := ss.getUserFromMeta(ctx, meta.GetUpdatedBy()) + if err != nil { + return nil, err + } + if updater.UID == "" { + updater = creator + } + + return &folder.Folder{ + UID: uid, + Title: title, + Description: description, + ID: meta.GetDeprecatedInternalID(), // nolint:staticcheck + ParentUID: meta.GetFolder(), + Version: int(meta.GetGeneration()), + Repository: meta.GetRepositoryName(), + + URL: url, + Created: created, + Updated: *updated, + OrgID: info.OrgID, + CreatedBy: creator.ID, + UpdatedBy: updater.ID, + }, nil +} + +func (ss *FolderUnifiedStoreImpl) getUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) { + if userMeta == "" || toUID(userMeta) == "" { + return &user.User{}, nil + } + usr, err := ss.getUser(ctx, toUID(userMeta)) + if err != nil && errors.Is(err, user.ErrUserNotFound) { + return &user.User{}, nil + } + return usr, err +} + +func (ss *FolderUnifiedStoreImpl) getUser(ctx context.Context, uid string) (*user.User, error) { + userID, err := strconv.ParseInt(uid, 10, 64) + if err == nil { + return ss.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: userID}) + } + return ss.userService.GetByUID(ctx, &user.GetUserByUIDQuery{UID: uid}) +} + +func toUID(rawIdentifier string) string { + parts := strings.Split(rawIdentifier, ":") + if len(parts) < 2 { + return "" + } + return parts[1] +} diff --git a/pkg/registry/apis/folders/conversions_test.go b/pkg/services/folder/folderimpl/conversions_test.go similarity index 76% rename from pkg/registry/apis/folders/conversions_test.go rename to pkg/services/folder/folderimpl/conversions_test.go index f7bef8ba015..a6120c7c23d 100644 --- a/pkg/registry/apis/folders/conversions_test.go +++ b/pkg/services/folder/folderimpl/conversions_test.go @@ -1,6 +1,7 @@ -package folders +package folderimpl import ( + "context" "testing" "time" @@ -8,6 +9,8 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" ) func TestFolderConversions(t *testing.T) { @@ -29,8 +32,8 @@ func TestFolderConversions(t *testing.T) { "grafana.app/folder": "parent-folder-name", "grafana.app/updatedTimestamp": "2022-12-02T07:02:02Z", "grafana.app/repoName": "example-repo", - "grafana.app/createdBy": "user:abc", - "grafana.app/updatedBy": "service:xyz" + "grafana.app/createdBy": "user:useruid", + "grafana.app/updatedBy": "user:useruid" } }, "spec": { @@ -44,7 +47,12 @@ func TestFolderConversions(t *testing.T) { created = created.UTC() require.NoError(t, err) - converted, err := UnstructuredToLegacyFolder(input) + fake := usertest.NewUserServiceFake() + fake.ExpectedUser = &user.User{ID: 10, UID: "useruid"} + + fs := ProvideUnifiedStore(nil, fake) + + converted, err := fs.UnstructuredToLegacyFolder(context.Background(), input) require.NoError(t, err) require.Equal(t, folder.Folder{ ID: 234, @@ -58,5 +66,7 @@ func TestFolderConversions(t *testing.T) { Repository: "example-repo", Created: created, Updated: created.Add(time.Hour * 5), + CreatedBy: 10, + UpdatedBy: 10, }, *converted) } diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 9c918f9d360..cafa0e38c91 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -53,7 +53,6 @@ type Service struct { log *slog.Logger dashboardStore dashboards.Store dashboardFolderStore folder.FolderStore - userService user.Service features featuremgmt.FeatureToggles accessControl accesscontrol.AccessControl k8sclient folderK8sHandler @@ -88,7 +87,6 @@ func ProvideService( dashboardStore: dashboardStore, dashboardFolderStore: folderStore, store: store, - userService: userService, features: features, accessControl: ac, bus: bus, @@ -114,7 +112,7 @@ func ProvideService( recourceClientProvider: unified.GetResourceClient, } - unifiedStore := ProvideUnifiedStore(k8sHandler) + unifiedStore := ProvideUnifiedStore(k8sHandler, userService) srv.unifiedStore = unifiedStore srv.k8sclient = k8sHandler diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index c9e9f536d17..3d5a84a6aad 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -35,6 +35,7 @@ import ( "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -181,7 +182,11 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { recourceClientProvider: f, } - unifiedStore := ProvideUnifiedStore(k8sHandler) + userService := &usertest.FakeUserService{ + ExpectedUser: &user.User{}, + } + + unifiedStore := ProvideUnifiedStore(k8sHandler, userService) ctx := context.Background() usr := &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{ diff --git a/pkg/services/folder/folderimpl/unifiedstore.go b/pkg/services/folder/folderimpl/unifiedstore.go index fe835c16f7a..18c40751185 100644 --- a/pkg/services/folder/folderimpl/unifiedstore.go +++ b/pkg/services/folder/folderimpl/unifiedstore.go @@ -20,21 +20,24 @@ import ( internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" ) type FolderUnifiedStoreImpl struct { - log log.Logger - k8sclient folderK8sHandler + log log.Logger + k8sclient folderK8sHandler + userService user.Service } // sqlStore implements the store interface. var _ folder.Store = (*FolderUnifiedStoreImpl)(nil) -func ProvideUnifiedStore(k8sHandler *foldk8sHandler) *FolderUnifiedStoreImpl { +func ProvideUnifiedStore(k8sHandler *foldk8sHandler, userService user.Service) *FolderUnifiedStoreImpl { return &FolderUnifiedStoreImpl{ - k8sclient: k8sHandler, - log: log.New("folder-store"), + k8sclient: k8sHandler, + log: log.New("folder-store"), + userService: userService, } } @@ -60,7 +63,7 @@ func (ss *FolderUnifiedStoreImpl) Create(ctx context.Context, cmd folder.CreateF return nil, err } - folder, err := internalfolders.UnstructuredToLegacyFolder(out) + folder, err := ss.UnstructuredToLegacyFolder(ctx, out) if err != nil { return nil, err } @@ -135,7 +138,7 @@ func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateF return nil, err } - return internalfolders.UnstructuredToLegacyFolder(out) + return ss.UnstructuredToLegacyFolder(ctx, out) } // If WithFullpath is true it computes also the full path of a folder. @@ -178,7 +181,7 @@ func (ss *FolderUnifiedStoreImpl) Get(ctx context.Context, q folder.GetFolderQue return nil, dashboards.ErrFolderNotFound } - return internalfolders.UnstructuredToLegacyFolder(out) + return ss.UnstructuredToLegacyFolder(ctx, out) } func (ss *FolderUnifiedStoreImpl) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) { @@ -206,7 +209,7 @@ func (ss *FolderUnifiedStoreImpl) GetParents(ctx context.Context, q folder.GetPa return nil, err } - folder, err := internalfolders.UnstructuredToLegacyFolder(out) + folder, err := ss.UnstructuredToLegacyFolder(ctx, out) if err != nil { return nil, err } @@ -245,9 +248,9 @@ func (ss *FolderUnifiedStoreImpl) GetChildren(ctx context.Context, q folder.GetC hits := make([]*folder.Folder, 0) for _, item := range out.Items { // convert item to legacy folder format - f, err := internalfolders.UnstructuredToLegacyFolder(&item) + f, err := ss.UnstructuredToLegacyFolder(ctx, &item) if f == nil { - return nil, fmt.Errorf("unable covert unstructured item to legacy folder %w", err) + return nil, fmt.Errorf("unable to convert unstructured item to legacy folder %w", err) } // it we are at root level, skip subfolder @@ -345,9 +348,9 @@ func (ss *FolderUnifiedStoreImpl) GetFolders(ctx context.Context, q folder.GetFo m := map[string]*folder.Folder{} for _, item := range out.Items { // convert item to legacy folder format - f, err := internalfolders.UnstructuredToLegacyFolder(&item) + f, err := ss.UnstructuredToLegacyFolder(ctx, &item) if f == nil { - return nil, fmt.Errorf("unable covert unstructured item to legacy folder %w", err) + return nil, fmt.Errorf("unable to convert unstructured item to legacy folder %w", err) } m[f.UID] = f @@ -405,9 +408,9 @@ func (ss *FolderUnifiedStoreImpl) GetDescendants(ctx context.Context, orgID int6 nodes := map[string]*folder.Folder{} for _, item := range out.Items { // convert item to legacy folder format - f, err := internalfolders.UnstructuredToLegacyFolder(&item) + f, err := ss.UnstructuredToLegacyFolder(ctx, &item) if f == nil { - return nil, fmt.Errorf("unable covert unstructured item to legacy folder %w", err) + return nil, fmt.Errorf("unable to convert unstructured item to legacy folder %w", err) } nodes[f.UID] = f From abac53bd0a8df19626524e319e6ebf3091bb56c6 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Tue, 28 Jan 2025 17:53:19 +0100 Subject: [PATCH 157/894] Revert "Revert "LibraryPanel: Fallback to panel title if library panel title is not set"" (#99678) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "Revert "LibraryPanel: Fallback to panel title if library panel title …" This reverts commit 6e705ee67c1286df82eb97d44f887f9d38258a74. --- .../dashboard-scene/inspect/InspectJsonTab.test.tsx | 2 +- .../dashboard-scene/panel-edit/PanelEditor.test.ts | 4 +--- .../dashboard-scene/panel-edit/PanelOptions.test.tsx | 1 - .../scene/AddLibraryPanelDrawer.test.tsx | 2 +- .../scene/DashboardDatasourceBehaviour.test.tsx | 3 --- .../dashboard-scene/scene/DashboardScene.test.tsx | 10 +++++----- .../scene/LibraryPanelBehavior.test.tsx | 2 +- .../dashboard-scene/scene/LibraryPanelBehavior.tsx | 6 ++---- .../serialization/transformSceneToSaveModel.test.ts | 6 ++---- .../serialization/transformSceneToSaveModel.ts | 2 +- .../utils/PanelModelCompatibilityWrapper.test.ts | 1 - 11 files changed, 14 insertions(+), 25 deletions(-) diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index 7d93518167e..582e0855008 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -223,7 +223,7 @@ async function buildTestSceneWithLibraryPanel() { title: 'Panel A', pluginId: 'table', key: 'panel-12', - $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' })], + $behaviors: [new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: '111' })], titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], $data: new SceneDataTransformer({ transformations: [ diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index fa309c8bd21..f204eae7138 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -200,7 +200,6 @@ describe('PanelEditor', () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: true, - title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, @@ -239,7 +238,7 @@ describe('PanelEditor', () => { // Wait for mock api to return and update the library panel expect(libPanelBehavior.state._loadedPanel?.version).toBe(2); expect(libPanelBehavior.state.name).toBe('changed name'); - expect(libPanelBehavior.state.title).toBe('changed title'); + expect(panel.state.title).toBe('changed title'); expect((gridItem.state.body as VizPanel).state.title).toBe('changed title'); }); @@ -258,7 +257,6 @@ describe('PanelEditor', () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: true, - title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx index 264c053de17..3f54a519b70 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx @@ -177,7 +177,6 @@ describe('PanelOptions', () => { const libraryPanel = new LibraryPanelBehavior({ isLoaded: true, - title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx index f298e7b19f9..e8a0054a731 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx @@ -96,7 +96,7 @@ describe('AddLibraryPanelWidget', () => { title: 'Panel Title', pluginId: 'table', key: 'panel-1', - $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: 'uid' })], }); addLibPanelDrawer = new AddLibraryPanelDrawer({ panelToReplaceRef: libPanel.getRef() }); diff --git a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx index 393f3cfaea7..1b183577b1f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx @@ -408,7 +408,6 @@ describe('DashboardDatasourceBehaviour', () => { it('should re-run queries when library panel re-runs query', async () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: false, - title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, @@ -469,7 +468,6 @@ describe('DashboardDatasourceBehaviour', () => { jest.spyOn(console, 'error').mockImplementation(); const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: false, - title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, @@ -519,7 +517,6 @@ describe('DashboardDatasourceBehaviour', () => { // Simulate library panel being loaded libPanelBehavior.setState({ isLoaded: true, - title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 2c54e174bea..80d4b19fa3a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -468,7 +468,7 @@ describe('DashboardScene', () => { title: 'Library Panel', pluginId: 'table', key: 'panel-4', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }); scene.copyPanel(libVizPanel); @@ -523,7 +523,7 @@ describe('DashboardScene', () => { title: 'Library Panel', pluginId: 'table', key: 'panel-4', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }), }) ); @@ -542,7 +542,7 @@ describe('DashboardScene', () => { const libPanel = new VizPanel({ title: 'Panel B', pluginId: 'table', - $behaviors: [new LibraryPanelBehavior({ title: 'title', name: 'lib panel', uid: 'abc', isLoaded: true })], + $behaviors: [new LibraryPanelBehavior({ name: 'lib panel', uid: 'abc', isLoaded: true })], }); const scene = buildTestScene({ @@ -907,7 +907,7 @@ function buildTestScene(overrides?: Partial) { title: 'Library Panel', pluginId: 'table', key: 'panel-5', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }), }), ], @@ -925,7 +925,7 @@ function buildTestScene(overrides?: Partial) { title: 'Library Panel', pluginId: 'table', key: 'panel-6', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], }), }), ], diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx index 02a363d5d3a..b6eb6872566 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx @@ -167,7 +167,7 @@ describe('LibraryPanelBehavior', () => { }); async function buildTestSceneWithLibraryPanel() { - const behavior = new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' }); + const behavior = new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: '111' }); const vizPanel = new VizPanel({ title: 'Panel A', diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx index 423cc2c34ee..44a1c0de5a8 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx @@ -17,8 +17,6 @@ import { AngularDeprecation } from './angular/AngularDeprecation'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; export interface LibraryPanelBehaviorState extends SceneObjectState { - // Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it. - title?: string; uid: string; name: string; isLoaded?: boolean; @@ -66,7 +64,7 @@ export class LibraryPanelBehavior extends SceneObjectBase { $behaviors: [ new LibraryPanelBehavior({ name: 'Some lib panel panel', - title: 'A panel', uid: 'lib-panel-uid', }), ], @@ -399,7 +398,7 @@ describe('transformSceneToSaveModel', () => { x: 0, y: 0, }); - expect(result.title).toBe('A panel'); + expect(result.title).toBe('Panel blahh blah'); expect(result.transformations).toBeUndefined(); expect(result.fieldConfig).toBeUndefined(); expect(result.options).toBeUndefined(); @@ -851,7 +850,6 @@ describe('transformSceneToSaveModel', () => { $behaviors: [ new LibraryPanelBehavior({ name: 'Some lib panel panel', - title: 'A panel', uid: 'lib-panel-uid', }), ], @@ -865,7 +863,7 @@ describe('transformSceneToSaveModel', () => { expect(result[0]).toMatchObject({ id: 4, - title: 'A panel', + title: 'Panel blahh blah', libraryPanel: { name: 'Some lib panel panel', uid: 'lib-panel-uid', diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 4c8368fc1fd..c1b6c96ec07 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -188,7 +188,7 @@ export function vizPanelToPanel( panel = { id: getPanelIdForVizPanel(vizPanel), - title: libPanel!.state.title, + title: vizPanel.state.title, gridPos: gridPos, libraryPanel: { name: libPanel!.state.name, diff --git a/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts b/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts index e6e44c7a583..02dcc897936 100644 --- a/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts +++ b/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts @@ -17,7 +17,6 @@ describe('PanelModelCompatibilityWrapper', () => { const libPanel = new LibraryPanelBehavior({ uid: 'a', name: 'aa', - title: 'a', }); panel.setState({ From 92d5e82a334e046c3408c97a83fe668fd6d528dd Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Tue, 28 Jan 2025 18:19:33 +0100 Subject: [PATCH 158/894] LibraryPanels: Respect model title when adding a library panel (#99687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "Revert "LibraryPanel: Fallback to panel title if library panel title …" This reverts commit 6e705ee67c1286df82eb97d44f887f9d38258a74. * LibraryPanels: Respect model title when adding a library panel to a dashboard * remove debugger --------- Co-authored-by: Haris Rozajac --- .../scene/AddLibraryPanelDrawer.test.tsx | 29 ++++++++++++++++++- .../scene/AddLibraryPanelDrawer.tsx | 3 ++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx index e8a0054a731..cba0d0f9e01 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx @@ -32,6 +32,7 @@ describe('AddLibraryPanelWidget', () => { const panelInfo: LibraryPanel = { uid: 'uid', model: { + title: 'model title', type: 'timeseries', }, name: 'name', @@ -47,6 +48,8 @@ describe('AddLibraryPanelWidget', () => { expect(panels.length).toBe(1); expect(panel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); expect(panel.state.key).toBe('panel-1'); + expect(panel.state.title).toBe('model title'); + expect(panel.state.hoverHeader).toBe(false); }); it('should add library panel from menu and enter edit mode in a dashboard that is not already in edit mode', async () => { @@ -69,6 +72,7 @@ describe('AddLibraryPanelWidget', () => { const panelInfo: LibraryPanel = { uid: 'uid', model: { + title: 'model title', type: 'timeseries', }, name: 'name', @@ -88,12 +92,13 @@ describe('AddLibraryPanelWidget', () => { expect(panels.length).toBe(1); expect(panel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); expect(panel.state.key).toBe('panel-1'); + expect(panel.state.title).toBe('model title'); expect(dashboard.state.isEditing).toBe(true); }); it('should replace grid item when grid item state is passed', async () => { const libPanel = new VizPanel({ - title: 'Panel Title', + title: 'Some panel title', pluginId: 'table', key: 'panel-1', $behaviors: [new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: 'uid' })], @@ -115,6 +120,7 @@ describe('AddLibraryPanelWidget', () => { const panelInfo: LibraryPanel = { uid: 'new_uid', model: { + title: 'model title', type: 'timeseries', }, name: 'new_name', @@ -132,6 +138,27 @@ describe('AddLibraryPanelWidget', () => { expect(behavior).toBeInstanceOf(LibraryPanelBehavior); expect(behavior.state.uid).toBe('new_uid'); expect(behavior.state.name).toBe('new_name'); + expect(panels[0].state.title).toBe('model title'); + }); + + it('should set hoverHeader to true if the library panel title is empty', () => { + const panelInfo: LibraryPanel = { + uid: 'uid', + model: { + title: '', + type: 'timeseries', + }, + name: 'name', + version: 1, + type: 'timeseries', + }; + + addLibPanelDrawer.onAddLibraryPanel(panelInfo); + + const panels = dashboard.state.body.getVizPanels(); + const panel = panels[0]; + expect(panel.state.title).toBe(''); + expect(panel.state.hoverHeader).toBe(true); }); }); diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx index e8e462005eb..1d4c4b198a5 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx @@ -26,6 +26,9 @@ export class AddLibraryPanelDrawer extends SceneObjectBase Date: Tue, 28 Jan 2025 18:36:10 +0100 Subject: [PATCH 159/894] Dashboards: Monitor dashboard loading performance (#99629) * WIP benchmark dashboard rendering * Script * Benchmark with variable and a panel * Add one more benchmark * Explicitely enable profiling * Playwright tests * update scenes * Report measurement to faro when config set * Let user enable metrics reporting in UI * Fix logging * Change how performance metrics is enabled per dashboard, now in config file only * add benchmark run option * Fix benchmark runs * fix description for performance config * remove console.log * update codeowners * add back crashDetection init that was lost in merge * fix yarn.lock * restore custom.ini * fix import * Make sure we have the echoSrv * fix config type * Try to limit changes to e2e runs * remove benchmark * Fix lint issue * fix codeowners --------- Co-authored-by: Dominik Prokop Co-authored-by: Sergej-Vlasov --- .gitignore | 1 - conf/defaults.ini | 3 ++ package.json | 2 + packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + packages/grafana-runtime/src/index.ts | 2 +- packages/grafana-runtime/src/utils/logging.ts | 12 +++-- pkg/api/dtos/frontend_settings.go | 2 + pkg/api/frontendsettings.go | 1 + pkg/setting/setting.go | 8 ++-- .../pages/DashboardScenePageStateManager.ts | 4 ++ .../transformSaveModelToScene.ts | 46 ++++++++++++++++++- 12 files changed, 71 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index ac5ad6f5f7d..201697fce4d 100644 --- a/.gitignore +++ b/.gitignore @@ -176,7 +176,6 @@ compilation-stats.json /blob-report/ /playwright/.cache/ /playwright/.auth/ - # grafana server /scripts/grafana-server/server.log diff --git a/conf/defaults.ini b/conf/defaults.ini index 1ed0bb7a24f..55a89dfa205 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -452,6 +452,9 @@ min_refresh_interval = 5s # Path to the default home dashboard. If this value is empty, then Grafana uses StaticRootPath + "dashboards/home.json" default_home_dashboard_path = +# Dashboards UIDs to report performance metrics for. * can be used to report metrics for all dashboards +dashboard_performance_metrics = + ################################### Data sources ######################### [datasources] # Upper limit of data sources that Grafana will return. This limit is a temporary configuration and it will be deprecated when pagination will be introduced on the list data sources API. diff --git a/package.json b/package.json index ec79d22d1e5..12f8eb5769b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "e2e:enterprise": "./e2e/start-and-run-suite enterprise", "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", + "build-benchmark": "NODE_ENV=dev nx exec -- webpack --config scripts/webpack/webpack.dev.js --env benchmark=1", + "e2e:playwright:benchmark": "yarn build-benchmark && ./e2e/plugin-e2e/start-and-benchmark", "e2e:playwright": "yarn playwright test", "e2e:playwright:server": "yarn e2e:plugin:build && ./e2e/plugin-e2e/start-and-run-suite", "e2e:storybook": "PORT=9001 ./e2e/run-suite storybook true", diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index b1f6ca4b28a..560f4ba672f 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -228,6 +228,7 @@ export interface GrafanaConfig { rudderstackConfigUrl: string | undefined; rudderstackIntegrationsUrl: string | undefined; analyticsConsoleReporting: boolean; + dashboardPerformanceMetrics: string[]; sqlConnectionLimits: SqlConnectionLimits; sharedWithMeFolderUID?: string; rootFolderUID?: string; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index f8b087b1bc6..6b29cdc0dee 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -185,6 +185,7 @@ export class GrafanaBootConfig implements GrafanaConfig { rudderstackConfigUrl: undefined; rudderstackIntegrationsUrl: undefined; analyticsConsoleReporting = false; + dashboardPerformanceMetrics: string[] = []; sqlConnectionLimits = { maxOpenConns: 100, maxIdleConns: 100, diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index 0c64456428b..a28fab8050a 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -9,7 +9,7 @@ export * from './analytics/types'; export { loadPluginCss, type PluginCssOptions, setPluginImportUtils, getPluginImportUtils } from './utils/plugin'; export { reportMetaAnalytics, reportInteraction, reportPageview, reportExperimentView } from './analytics/utils'; export { featureEnabled } from './utils/licensing'; -export { logInfo, logDebug, logWarning, logError, createMonitoringLogger } from './utils/logging'; +export { logInfo, logDebug, logWarning, logError, createMonitoringLogger, logMeasurement } from './utils/logging'; export { DataSourceWithBackend, HealthCheckError, diff --git a/packages/grafana-runtime/src/utils/logging.ts b/packages/grafana-runtime/src/utils/logging.ts index d7c94da53d8..98eb7241361 100644 --- a/packages/grafana-runtime/src/utils/logging.ts +++ b/packages/grafana-runtime/src/utils/logging.ts @@ -66,11 +66,13 @@ export function logError(err: Error, contexts?: LogContext) { export type MeasurementValues = Record; export function logMeasurement(type: string, values: MeasurementValues, context?: LogContext) { if (config.grafanaJavascriptAgent.enabled) { - faro.api.pushMeasurement({ - type, - values, - context, - }); + faro.api.pushMeasurement( + { + type, + values, + }, + { context: context } + ); } } diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index d7094de9d90..6827525be04 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -190,6 +190,8 @@ type FrontendSettingsDTO struct { AnalyticsConsoleReporting bool `json:"analyticsConsoleReporting"` + DashboardPerformanceMetrics []string `json:"dashboardPerformanceMetrics"` + FeedbackLinksEnabled bool `json:"feedbackLinksEnabled"` ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString"` ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index f11114b299a..8ec544337b0 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -215,6 +215,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro RudderstackConfigUrl: hs.Cfg.RudderstackConfigURL, RudderstackIntegrationsUrl: hs.Cfg.RudderstackIntegrationsURL, AnalyticsConsoleReporting: hs.Cfg.FrontendAnalyticsConsoleReporting, + DashboardPerformanceMetrics: hs.Cfg.DashboardPerformanceMetrics, FeedbackLinksEnabled: hs.Cfg.FeedbackLinksEnabled, ApplicationInsightsConnectionString: hs.Cfg.ApplicationInsightsConnectionString, ApplicationInsightsEndpointUrl: hs.Cfg.ApplicationInsightsEndpointUrl, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index c78e455fa24..6bb86697f10 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -218,9 +218,10 @@ type Cfg struct { MetricsGrafanaEnvironmentInfo map[string]string // Dashboards - DashboardVersionsToKeep int - MinRefreshInterval string - DefaultHomeDashboardPath string + DashboardVersionsToKeep int + MinRefreshInterval string + DefaultHomeDashboardPath string + DashboardPerformanceMetrics []string // Auth LoginCookieName string @@ -1133,6 +1134,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20) cfg.MinRefreshInterval = valueAsString(dashboards, "min_refresh_interval", "5s") cfg.DefaultHomeDashboardPath = dashboards.Key("default_home_dashboard_path").MustString("") + cfg.DashboardPerformanceMetrics = util.SplitString(dashboards.Key("dashboard_performance_metrics").MustString("")) if err := readUserSettings(iniFile, cfg); err != nil { return err diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index d5dcf3d533b..8989681da8d 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -2,6 +2,7 @@ import { isEqual } from 'lodash'; import { locationUtil, UrlQueryMap } from '@grafana/data'; import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; +import { sceneGraph } from '@grafana/scenes'; import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { getMessageFromError, getMessageIdFromError, getStatusFromError } from 'app/core/utils/errors'; @@ -135,7 +136,10 @@ abstract class DashboardScenePageStateManagerBase this.setState({ dashboard: dashboard, isLoading: false, options }); const measure = stopMeasure(LOAD_SCENE_MEASUREMENT); + const queryController = sceneGraph.getQueryController(dashboard); + trackDashboardSceneLoaded(dashboard, measure?.duration); + queryController?.startProfile(dashboard); if (options.route !== DashboardRoutes.New) { emitDashboardViewEvent({ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index c3b82b69b66..e483b3bfe19 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -1,7 +1,7 @@ import { uniqueId } from 'lodash'; import { DataFrameDTO, DataFrameJSON } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { config, logMeasurement, reportInteraction } from '@grafana/runtime'; import { VizPanel, SceneTimePicker, @@ -19,6 +19,7 @@ import { SceneDataLayerProvider, SceneDataLayerControls, UserActionEvent, + SceneInteractionProfileEvent, SceneObjectState, } from '@grafana/scenes'; import { contextSrv } from 'app/core/core'; @@ -229,7 +230,11 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, new behaviors.CursorSync({ sync: oldModel.graphTooltip, }), - new behaviors.SceneQueryController(), + new behaviors.SceneQueryController({ + enableProfiling: + config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1, + onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title), + }), registerDashboardMacro, registerPanelInteractionsReporter, new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }), @@ -434,3 +439,40 @@ function trackIfEmpty(grid: SceneGridLayout) { sub.unsubscribe(); }; } + +function getDashboardInteractionCallback(uid: string, title: string) { + return (e: SceneInteractionProfileEvent) => { + let interactionType = ''; + + if (e.origin === 'SceneTimeRange') { + interactionType = 'time-range-change'; + } else if (e.origin === 'SceneRefreshPicker') { + interactionType = 'refresh'; + } else if (e.origin === 'DashboardScene') { + interactionType = 'view'; + } else if (e.origin.indexOf('Variable') > -1) { + interactionType = 'variable-change'; + } + reportInteraction('dashboard-render', { + interactionType, + duration: e.duration, + networkDuration: e.networkDuration, + totalJSHeapSize: e.totalJSHeapSize, + usedJSHeapSize: e.usedJSHeapSize, + jsHeapSizeLimit: e.jsHeapSizeLimit, + }); + + logMeasurement( + `dashboard.${interactionType}`, + { + duration: e.duration, + networkDuration: e.networkDuration, + totalJSHeapSize: e.totalJSHeapSize, + usedJSHeapSize: e.usedJSHeapSize, + jsHeapSizeLimit: e.jsHeapSizeLimit, + timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration, + }, + { dashboard: uid, title: title } + ); + }; +} From 3228ae727eb2c0711b3a0f96dbbf8416bbb0754e Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Tue, 28 Jan 2025 11:36:26 -0600 Subject: [PATCH 160/894] wires up dashboards page to be able to sort by usage stats (sprinkles) (#99479) * wires up dashboards page to be able to sort by usage stats (sprinkles) * dont mutate field * use better type for field * adds tests. Had to export some types and put the field type back to object. * frontend asks for sort field in response if needed * adds some unit tests for getSortOptions * use Record instead of object * prettier * adds ternaries, another unit test --- pkg/storage/unified/search/bleve.go | 4 ++ pkg/storage/unified/search/bleve_test.go | 30 +++++++++ .../features/search/service/unified.test.ts | 65 +++++++++++++++++++ public/app/features/search/service/unified.ts | 26 ++++++-- 4 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 public/app/features/search/service/unified.test.ts diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 79a5149efc7..d0c8b0f4ca7 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -655,6 +655,10 @@ func getSortFields(req *resource.ResourceSearchRequest) []string { input = field } + if slices.Contains(DashboardFields(), input) { + input = "fields." + input + } + if sort.Desc { input = "-" + input } diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index effc77bdd57..4535b276c0a 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -529,6 +529,36 @@ func TestBleveBackend(t *testing.T) { }) } +func TestGetSortFields(t *testing.T) { + t.Run("will prepend 'fields.' to sort fields when they are dashboard fields", func(t *testing.T) { + searchReq := &resource.ResourceSearchRequest{ + SortBy: []*resource.ResourceSearchRequest_Sort{ + {Field: "views_total", Desc: false}, + }, + } + sortFields := getSortFields(searchReq) + assert.Equal(t, []string{"fields.views_total"}, sortFields) + }) + t.Run("will prepend sort fields with a '-' when sort is Desc", func(t *testing.T) { + searchReq := &resource.ResourceSearchRequest{ + SortBy: []*resource.ResourceSearchRequest_Sort{ + {Field: "views_total", Desc: true}, + }, + } + sortFields := getSortFields(searchReq) + assert.Equal(t, []string{"-fields.views_total"}, sortFields) + }) + t.Run("will not prepend 'fields.' to common fields", func(t *testing.T) { + searchReq := &resource.ResourceSearchRequest{ + SortBy: []*resource.ResourceSearchRequest_Sort{ + {Field: "description", Desc: false}, + }, + } + sortFields := getSortFields(searchReq) + assert.Equal(t, []string{"description"}, sortFields) + }) +} + func asTimePointer(milli int64) *time.Time { if milli > 0 { t := time.UnixMilli(milli) diff --git a/public/app/features/search/service/unified.test.ts b/public/app/features/search/service/unified.test.ts new file mode 100644 index 00000000000..4d50e25193b --- /dev/null +++ b/public/app/features/search/service/unified.test.ts @@ -0,0 +1,65 @@ +import { toDashboardResults, SearchHit, SearchAPIResponse } from './unified'; + +describe('Unified Storage Searcher', () => { + it('can create dashboard search results and set meta sortBy so column is added for sprinkles sort field', () => { + const mockHits: SearchHit[] = [ + { + resource: 'dashboard', + name: 'Main Dashboard', + title: 'Main Dashboard Title', + location: '/dashboards/1', + folder: 'General', + tags: ['monitoring', 'performance'], + field: { errors_today: 1 }, + url: '/dashboards/1', + }, + { + resource: 'dashboard', + name: 'Main Dashboard', + title: 'Main Dashboard Title', + location: '/dashboards/1', + folder: 'General', + tags: ['monitoring', 'performance'], + field: { errors_today: 2 }, + url: '/dashboards/1', + }, + ]; + + const mockResponse: SearchAPIResponse = { + totalHits: 2, + hits: mockHits, + facets: {}, + }; + const results = toDashboardResults(mockResponse, 'errors_today'); + + expect(results.length).toBe(2); + const sprinklesField = results.fields[10]; + expect(sprinklesField.name).toBe('errors_today'); + expect(sprinklesField.values).toEqual([1, 2]); // this also tests the hits original order is preserved + expect(results.meta?.custom?.sortBy).toBe('errors_today'); + }); + + it('will trim "-" from the sort field name', () => { + const mockHits: SearchHit[] = [ + { + resource: 'dashboard', + name: 'Main Dashboard', + title: 'Main Dashboard Title', + location: '/dashboards/1', + folder: 'General', + tags: ['monitoring', 'performance'], + field: { errors_today: 1 }, + url: '/dashboards/1', + }, + ]; + + const mockResponse: SearchAPIResponse = { + totalHits: 0, + hits: mockHits, + facets: {}, + }; + const results = toDashboardResults(mockResponse, '-errors_today'); + + expect(results.meta?.custom?.sortBy).toBe('errors_today'); + }); +}); diff --git a/public/app/features/search/service/unified.ts b/public/app/features/search/service/unified.ts index 9e379873cc3..3ed44146c35 100644 --- a/public/app/features/search/service/unified.ts +++ b/public/app/features/search/service/unified.ts @@ -20,7 +20,7 @@ const loadingFrameName = 'Loading'; const searchURI = `apis/dashboard.grafana.app/v0alpha1/namespaces/${config.namespace}/search`; -type SearchHit = { +export type SearchHit = { resource: string; // dashboards | folders name: string; title: string; @@ -28,11 +28,13 @@ type SearchHit = { folder: string; tags: string[]; + field: Record; // extra fields from the backend - sort fields included here as well + // calculated in the frontend url: string; }; -type SearchAPIResponse = { +export type SearchAPIResponse = { totalHits: number; hits: SearchHit[]; facets?: { @@ -110,7 +112,7 @@ export class UnifiedSearcher implements GrafanaSearcher { const uri = await this.newRequest(query); const rsp = await getBackendSrv().get(uri); - const first = toDashboardResults(rsp); + const first = toDashboardResults(rsp, query.sort ?? ''); if (first.name === loadingFrameName) { return this.fallbackSearcher.search(query); } @@ -145,7 +147,7 @@ export class UnifiedSearcher implements GrafanaSearcher { } const nextPageUrl = `${uri}&offset=${offset}`; const resp = await getBackendSrv().get(nextPageUrl); - const frame = toDashboardResults(resp); + const frame = toDashboardResults(resp, query.sort ?? ''); if (!frame) { console.log('no results', frame); return; @@ -217,6 +219,9 @@ export class UnifiedSearcher implements GrafanaSearcher { if (query.sort) { const sort = query.sort.replace('_sort', '').replace('name', 'title'); uri += `&sort=${sort}`; + const sortField = sort.startsWith('-') ? sort.substring(1) : sort; + + uri += `&field=${sortField}`; // we want to the sort field to be included in the response } if (query.name?.length) { @@ -279,7 +284,7 @@ function getSortFieldDisplayName(name: string) { return name; } -function toDashboardResults(rsp: SearchAPIResponse): DataFrame { +export function toDashboardResults(rsp: SearchAPIResponse, sort: string): DataFrame { const hits = rsp.hits; if (hits.length < 1) { return { fields: [], length: 0 }; @@ -290,6 +295,11 @@ function toDashboardResults(rsp: SearchAPIResponse): DataFrame { location = 'general'; } + // display null field values as "-" + const field = Object.fromEntries( + Object.entries(hit.field ?? {}).map(([key, value]) => [key, value == null ? '-' : value]) + ); + return { ...hit, uid: hit.name, @@ -299,6 +309,7 @@ function toDashboardResults(rsp: SearchAPIResponse): DataFrame { location, name: hit.title, // 🤯 FIXME hit.name is k8s name, eg grafana dashboards UID kind: hit.resource.substring(0, hit.resource.length - 1), // dashboard "kind" is not plural + ...field, }; }); const frame = toDataFrame(dashboardHits); @@ -308,6 +319,11 @@ function toDashboardResults(rsp: SearchAPIResponse): DataFrame { max_score: 1, }, }; + if (sort && frame.meta.custom) { + // trim the "-" from sort if it exists + frame.meta.custom.sortBy = sort.startsWith('-') ? sort.substring(1) : sort; + } + for (const field of frame.fields) { field.display = getDisplayProcessor({ field, theme: config.theme2 }); } From 04c84e06e538a0482a1406ba6bf315a259b215af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:01:00 +0000 Subject: [PATCH 161/894] Update dependency @types/swagger-ui-react to v4.19.0 (#99674) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 12f8eb5769b..9b22a995e00 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "@types/slate": "0.47.11", "@types/slate-plain-serializer": "0.7.5", "@types/slate-react": "0.22.9", - "@types/swagger-ui-react": "4.18.3", + "@types/swagger-ui-react": "4.19.0", "@types/systemjs": "6.15.1", "@types/tinycolor2": "1.4.6", "@types/uuid": "10.0.0", diff --git a/yarn.lock b/yarn.lock index 62be189a3a7..53483ac0007 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10204,12 +10204,12 @@ __metadata: languageName: node linkType: hard -"@types/swagger-ui-react@npm:4.18.3": - version: 4.18.3 - resolution: "@types/swagger-ui-react@npm:4.18.3" +"@types/swagger-ui-react@npm:4.19.0": + version: 4.19.0 + resolution: "@types/swagger-ui-react@npm:4.19.0" dependencies: "@types/react": "npm:*" - checksum: 10/4927314f1b0d68edf200ef15bca7555f12ec1bb8cc699fa397ef2f4e1e1873d17880b663bbc6d70e30149e50c3e7055178c1e0aad110520dfb31d781ab326dcb + checksum: 10/c4a7ae85ca081bf079164b3e11ecc0c91dec1ed10da0f44fc67f8cf93a7d92d5bfa681035eb8b9d3327a0361d201861c02571b4b18f361253510f3ce19ce073f languageName: node linkType: hard @@ -17886,7 +17886,7 @@ __metadata: "@types/slate": "npm:0.47.11" "@types/slate-plain-serializer": "npm:0.7.5" "@types/slate-react": "npm:0.22.9" - "@types/swagger-ui-react": "npm:4.18.3" + "@types/swagger-ui-react": "npm:4.19.0" "@types/systemjs": "npm:6.15.1" "@types/tinycolor2": "npm:1.4.6" "@types/uuid": "npm:10.0.0" From 721c2b0c7ca7603209e2a06dfef5b249a57324fc Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:18:43 -0700 Subject: [PATCH 162/894] Release: update changelog for 11.5.0 (#99652) * Update changelog * baldm0mma/add changelog content --------- Co-authored-by: github-actions[bot] Co-authored-by: jev forsberg --- CHANGELOG.md | 168 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c7060ba7a..927936a27d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,171 @@ + + +# 11.5.0 (2025-01-28) + +### Features and enhancements + +- ** CloudMigration:** Create authapi service [#96581](https://github.com/grafana/grafana/pull/96581), [@leandro-deveikis](https://github.com/leandro-deveikis) +- **Alerting:** Add new button for exporting new alert rule in HCL format [#96785](https://github.com/grafana/grafana/pull/96785), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Add option to show inactive alerts in alert list panel [#96888](https://github.com/grafana/grafana/pull/96888), [@bradleypettit](https://github.com/bradleypettit) +- **Alerting:** Add state_periodic_save_batch_size config option [#98019](https://github.com/grafana/grafana/pull/98019), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Alerting:** Change default for max_attempts to 3. [#97461](https://github.com/grafana/grafana/pull/97461), [@stevesg](https://github.com/stevesg) +- **Alerting:** Consume k8s API for notification policies tree [#96147](https://github.com/grafana/grafana/pull/96147), [@konrad147](https://github.com/konrad147) +- **Alerting:** Enable flag alertingApiServer by default [#98282](https://github.com/grafana/grafana/pull/98282), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** Explore button in Insights view [#96496](https://github.com/grafana/grafana/pull/96496), [@ppcano](https://github.com/ppcano) +- **Alerting:** Improve performance ash page [#97619](https://github.com/grafana/grafana/pull/97619), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Make alert rule policies preview use k8s API [#97070](https://github.com/grafana/grafana/pull/97070), [@tomratcliffe](https://github.com/tomratcliffe) +- **Alerting:** Return default builtin templates in k8s templategroup API and UI [#96330](https://github.com/grafana/grafana/pull/96330), [@JacobsonMT](https://github.com/JacobsonMT) +- **Alerting:** Simplify notification step [#96430](https://github.com/grafana/grafana/pull/96430), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Update state manager to take image only once per rule evaluation [#98289](https://github.com/grafana/grafana/pull/98289), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Analytics Views:** Deprecate :dashboardID endpoints in favor of uid/:dashboardUID (Enterprise) +- **Analytics:** Summaries: Deprecate dashboard_id endpoints in favor of dashboard_uid (Enterprise) +- **Announcement Banners:** Enable feature for all cloud tiers (Enterprise) +- **Announcement banner:** Remove feature toggle [#98782](https://github.com/grafana/grafana/pull/98782), [@Clarity-89](https://github.com/Clarity-89) +- **Announcement banner:** Remove feature toggle (Enterprise) +- **Announcement banner:** Sort by last updated (Enterprise) +- **Auth:** Return error when retries have been exhausted for OAuth token refresh [#98034](https://github.com/grafana/grafana/pull/98034), [@mgyongyosi](https://github.com/mgyongyosi) +- **Azure Monitor:** Add a feature flag to toggle user auth for Azure Monitor only [#96858](https://github.com/grafana/grafana/pull/96858), [@adamyeats](https://github.com/adamyeats) +- **Azure:** Improve Azure Prometheus exemplars UI/UX [#97198](https://github.com/grafana/grafana/pull/97198), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Unify credentials in frontend for Prometheus [#96568](https://github.com/grafana/grafana/pull/96568), [@yjsong11](https://github.com/yjsong11) +- **Chore:** Bump Go to 1.23.4 [#98853](https://github.com/grafana/grafana/pull/98853), [@Proximyst](https://github.com/Proximyst) +- **Chore:** Bump Go to 1.23.4 (Enterprise) +- **Chore:** Remove experimental Storage UI [#96887](https://github.com/grafana/grafana/pull/96887), [@ryantxu](https://github.com/ryantxu) +- **Chore:** Update to node 22 [#97779](https://github.com/grafana/grafana/pull/97779), [@ashharrison90](https://github.com/ashharrison90) +- **CloudMigrations:** Enable feature toggle by default in 11.5 [#98686](https://github.com/grafana/grafana/pull/98686), [@mmandrus](https://github.com/mmandrus) +- **CloudMigrations:** Introduce RBAC role for migration assistant [#98588](https://github.com/grafana/grafana/pull/98588), [@macabu](https://github.com/macabu) +- **CloudWatch:** Add OpenSearch PPL and SQL support in Logs Insights [#97508](https://github.com/grafana/grafana/pull/97508), [@idastambuk](https://github.com/idastambuk) +- **CloudWatch:** Batch different time ranges separately [#98230](https://github.com/grafana/grafana/pull/98230), [@iwysiu](https://github.com/iwysiu) +- **Cloudwatch:** Accept empty string for logstimeout and mark errors downstream [#96947](https://github.com/grafana/grafana/pull/96947), [@iwysiu](https://github.com/iwysiu) +- **Cloudwatch:** Update grafana-aws-sdk for AWS/AmplifyHosting metrics [#97799](https://github.com/grafana/grafana/pull/97799), [@iwysiu](https://github.com/iwysiu) +- **Dashboard Scene:** Shows usages in variables list [#96000](https://github.com/grafana/grafana/pull/96000), [@harisrozajac](https://github.com/harisrozajac) +- **Dashboards:** Add option to specify explicit percent change text size for stat panels [#96952](https://github.com/grafana/grafana/pull/96952), [@XZCendence](https://github.com/XZCendence) +- **Dashboards:** Allow DashboardDS subqueries in MixedDS [#97116](https://github.com/grafana/grafana/pull/97116), [@mdvictor](https://github.com/mdvictor) +- **Dashboards:** Update docs of the `overwrite` param in Save Dashboard API Call [#97011](https://github.com/grafana/grafana/pull/97011), [@ArturWierzbicki](https://github.com/ArturWierzbicki) +- **Datasources:** Add toggle to control default behaviour of 'Manage alerts via Alerts UI' toggle [#98441](https://github.com/grafana/grafana/pull/98441), [@macabu](https://github.com/macabu) +- **Datasources:** Allow clearing trace to logs, metrics and profiles datasource pickers [#96554](https://github.com/grafana/grafana/pull/96554), [@adrapereira](https://github.com/adrapereira) +- **Docker:** Don't use legacy ENV syntax [#93218](https://github.com/grafana/grafana/pull/93218), [@simPod](https://github.com/simPod) +- **Elasticsearch:** Health endpoint should handle http errors [#96803](https://github.com/grafana/grafana/pull/96803), [@iwysiu](https://github.com/iwysiu) +- **Elasticsearch:** Use \_field_caps instead of \_mapping to get fields [#97607](https://github.com/grafana/grafana/pull/97607), [@iwysiu](https://github.com/iwysiu) +- **Explore Profiles:** Preinstall for onprem Grafana instances [#97775](https://github.com/grafana/grafana/pull/97775), [@ifrost](https://github.com/ifrost) +- **Explore metrics:** Consolidate filters with the OTel experience [#98371](https://github.com/grafana/grafana/pull/98371), [@bohandley](https://github.com/bohandley) +- **Explore:** Show links to queryless apps [#96625](https://github.com/grafana/grafana/pull/96625), [@ifrost](https://github.com/ifrost) +- **Expressions:** Add notification for Strict Mode behavior in Reduce component [#97224](https://github.com/grafana/grafana/pull/97224), [@shubhankarunhale](https://github.com/shubhankarunhale) +- **Faro:** Improve performance of TRACKING_URLS regex [#98022](https://github.com/grafana/grafana/pull/98022), [@kpelelis](https://github.com/kpelelis) +- **FeatureToggles:** Make newFiltersUI feature toggle generally available [#97460](https://github.com/grafana/grafana/pull/97460), [@Sergej-Vlasov](https://github.com/Sergej-Vlasov) +- **Features:** Remove cloudwatchMetricInsightsCrossAccount feature toggle [#98826](https://github.com/grafana/grafana/pull/98826), [@idastambuk](https://github.com/idastambuk) +- **Frontend Sandbox:** Add switch to toggle plugins frontend sandbox via catalog UI (Enterprise) +- **Graphite:** Set `maxDataPoints` based on user value in alerting [#97178](https://github.com/grafana/grafana/pull/97178), [@aangelisc](https://github.com/aangelisc) +- **Licensing:** Tidy up license token database code (Enterprise) +- **LoginAttempt:** Add setting to control max number of attempts before user login gets locked [#97091](https://github.com/grafana/grafana/pull/97091), [@kalleep](https://github.com/kalleep) +- **Logs Panel:** Add infinite scrolling support for Dashboards and Apps [#97095](https://github.com/grafana/grafana/pull/97095), [@matyax](https://github.com/matyax) +- **Logs Panel:** Allow text selection without changing Log Details state [#96995](https://github.com/grafana/grafana/pull/96995), [@matyax](https://github.com/matyax) +- **Logs Panel:** Limit displayed characters to MAX_CHARACTERS [#96997](https://github.com/grafana/grafana/pull/96997), [@matyax](https://github.com/matyax) +- **Logs:** Added option to show the log line body when displayed fields are used [#97209](https://github.com/grafana/grafana/pull/97209), [@matyax](https://github.com/matyax) +- **Logs:** Added support to disable and re-enable the popover menu [#98254](https://github.com/grafana/grafana/pull/98254), [@matyax](https://github.com/matyax) +- **Logs:** Allow scroll to reach the bottom of the log list before loading more [#96668](https://github.com/grafana/grafana/pull/96668), [@matyax](https://github.com/matyax) +- **Loki:** Added support for disabled operations in Query Builder [#96751](https://github.com/grafana/grafana/pull/96751), [@matyax](https://github.com/matyax) +- **Loki:** Added support to show label types in Log Details [#97284](https://github.com/grafana/grafana/pull/97284), [@matyax](https://github.com/matyax) +- **Loki:** Allow regex in `label` derived field [#96609](https://github.com/grafana/grafana/pull/96609), [@svennergr](https://github.com/svennergr) +- **Loki:** Hide internal labels [#97323](https://github.com/grafana/grafana/pull/97323), [@svennergr](https://github.com/svennergr) +- **Loki:** Sync query direction with sort order in Explore and Dashboards [#98722](https://github.com/grafana/grafana/pull/98722), [@matyax](https://github.com/matyax) +- **OAuth:** Support client_secret_jwt for oauth providers when doing token exchange [#95455](https://github.com/grafana/grafana/pull/95455), [@naizerjohn-ms](https://github.com/naizerjohn-ms) +- **OAuth:** Use the attached external session data in OAuthToken and OAuthTokenSync [#96655](https://github.com/grafana/grafana/pull/96655), [@mgyongyosi](https://github.com/mgyongyosi) +- **Org Selection:** Show correct selected org when select is open [#96601](https://github.com/grafana/grafana/pull/96601), [@yincongcyincong](https://github.com/yincongcyincong) +- **PDF:** Add new zoom options (Enterprise) +- **Plugin Extensions:** Only load app plugins when necessary [#86624](https://github.com/grafana/grafana/pull/86624), [@leventebalogh](https://github.com/leventebalogh) +- **Plugins:** Add token to gcom requests [#96261](https://github.com/grafana/grafana/pull/96261), [@oshirohugo](https://github.com/oshirohugo) +- **Plugins:** Add token to gcom requests (Enterprise) +- **Plugins:** Disable version install when angular version is not supported [#97189](https://github.com/grafana/grafana/pull/97189), [@oshirohugo](https://github.com/oshirohugo) +- **Plugins:** Disable version installation for specific plugin types [#98597](https://github.com/grafana/grafana/pull/98597), [@oshirohugo](https://github.com/oshirohugo) +- **Plugins:** Update to latest go plugin SDK (v0.260.3) w/ arrow v18 [#97561](https://github.com/grafana/grafana/pull/97561), [@ryantxu](https://github.com/ryantxu) +- **Plugins:** Use grafana-com sso_api_token [#97096](https://github.com/grafana/grafana/pull/97096), [@oshirohugo](https://github.com/oshirohugo) +- **Plugins:** Use grafana-com sso_api_token (Enterprise) +- **Prometheus datasource:** Show info annotations in the UI [#97978](https://github.com/grafana/grafana/pull/97978), [@zenador](https://github.com/zenador) +- **Prometheus:** Improve handling of special chars in label values [#96067](https://github.com/grafana/grafana/pull/96067), [@NWRichmond](https://github.com/NWRichmond) +- **PublicDashboards:** Remove publicDashboards FF [#96578](https://github.com/grafana/grafana/pull/96578), [@juanicabanas](https://github.com/juanicabanas) +- **Reporting:** Add allow list email domain configuration (Enterprise) +- **Reporting:** Include the apiserver by default and deprecated internal ids (Enterprise) +- **RuntimeDataSource:** Support in core for runtime registered data sources [#93956](https://github.com/grafana/grafana/pull/93956), [@torkelo](https://github.com/torkelo) +- **SAML:** Add the ability to specify EntityID (Enterprise) +- **SAML:** Implement correct SLO with NameID and SessionIndex handling (Enterprise) +- **Security:** Update to Go 1.23.5 - Backport to v11.5.x [#99122](https://github.com/grafana/grafana/pull/99122), [@Proximyst](https://github.com/Proximyst) +- **Security:** Update to Go 1.23.5 - Backport to v11.5.x (Enterprise) +- **Snapshots:** Add RBAC roles for creating and deleting [#96126](https://github.com/grafana/grafana/pull/96126), [@evictorero](https://github.com/evictorero) +- **Storage:** Removes integration tests for MySQL 5.7 since it is EOL [#98013](https://github.com/grafana/grafana/pull/98013), [@inf0rmer](https://github.com/inf0rmer) +- **Tempo:** Add support for TraceQL Metrics exemplars [#96859](https://github.com/grafana/grafana/pull/96859), [@adrapereira](https://github.com/adrapereira) +- **Tempo:** Honor datasource TLS settings for gRPC requests [#97484](https://github.com/grafana/grafana/pull/97484), [@mdisibio](https://github.com/mdisibio) +- **Tempo:** Improve handling of multiple values in the Search tab query generation [#98427](https://github.com/grafana/grafana/pull/98427), [@adrapereira](https://github.com/adrapereira) +- **ToolbarButton:** Auto width on smaller screen sizes [#96023](https://github.com/grafana/grafana/pull/96023), [@yincongcyincong](https://github.com/yincongcyincong) +- **Trace View:** Set span filters as panel options [#98328](https://github.com/grafana/grafana/pull/98328), [@adrapereira](https://github.com/adrapereira) +- **TransformationFilter:** Implement RefID multi picker [#96841](https://github.com/grafana/grafana/pull/96841), [@Sergej-Vlasov](https://github.com/Sergej-Vlasov) +- **Transformations:** Add Delimiter format option to Extract fields [#97340](https://github.com/grafana/grafana/pull/97340), [@tskarhed](https://github.com/tskarhed) +- **Transformations:** Add RegExp option to Extract fields transformer [#96593](https://github.com/grafana/grafana/pull/96593), [@leeoniya](https://github.com/leeoniya) +- **Transformations:** GroupToMatrix add 0 as special value [#97642](https://github.com/grafana/grafana/pull/97642), [@tskarhed](https://github.com/tskarhed) +- **Zipkin:** Run queries through backend [#97754](https://github.com/grafana/grafana/pull/97754), [@ivanahuckova](https://github.com/ivanahuckova) + +### Bug fixes + +- **Alerting:** AlertingQueryRunner should skip descendant nodes of invalid queries [#97528](https://github.com/grafana/grafana/pull/97528), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Allow notification policy filters to match quoted matchers [#98525](https://github.com/grafana/grafana/pull/98525), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Fix alert rule list view summaries [#98433](https://github.com/grafana/grafana/pull/98433), [@yincongcyincong](https://github.com/yincongcyincong) +- **Alerting:** Fix alert rules unpausing after moving rule to different folder [#97580](https://github.com/grafana/grafana/pull/97580), [@santihernandezc](https://github.com/santihernandezc) +- **Alerting:** Fix ash not showing history graph in firefox [#98128](https://github.com/grafana/grafana/pull/98128), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix bug when saving a rule more than once [#96658](https://github.com/grafana/grafana/pull/96658), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix data-testid in RuleEditorSection [#97473](https://github.com/grafana/grafana/pull/97473), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix filtering rule group permissions based on their full path (Enterprise) +- **Alerting:** Fix go template parsing [#97145](https://github.com/grafana/grafana/pull/97145), [@konrad147](https://github.com/konrad147) +- **Alerting:** Fix label escaping in rule export [#97985](https://github.com/grafana/grafana/pull/97985), [@moustafab](https://github.com/moustafab) +- **Alerting:** Fix missing instances and history when Grafana rule is stored in folder with / [#97956](https://github.com/grafana/grafana/pull/97956), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Fix navigating to URLs with "%25" [#96992](https://github.com/grafana/grafana/pull/96992), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Fix no-change scenario in provisioning rule update API [#98389](https://github.com/grafana/grafana/pull/98389), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Alerting:** Fix not being able to remove a reducer when using range query [#97757](https://github.com/grafana/grafana/pull/97757), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix recording rules rendering simplified condition [#97497](https://github.com/grafana/grafana/pull/97497), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix removing reducer when inital value is instant [#97054](https://github.com/grafana/grafana/pull/97054), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix simplified query step [#97046](https://github.com/grafana/grafana/pull/97046), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix slack image uploading to use new api [#97817](https://github.com/grafana/grafana/pull/97817), [@moustafab](https://github.com/moustafab) +- **Alerting:** Fix terraform export of notification policy [#98429](https://github.com/grafana/grafana/pull/98429), [@moustafab](https://github.com/moustafab) +- **Alerting:** Fix updating condition when refId changes [#97753](https://github.com/grafana/grafana/pull/97753), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix using stacks- prefix instead of stack- for checking the namespace in boot data [#97492](https://github.com/grafana/grafana/pull/97492), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Anonymous User:** Adds validator service for anonymous users (Enterprise) +- **Auth:** Fix SAML user IsExternallySynced not being set correctly [#98487](https://github.com/grafana/grafana/pull/98487), [@volcanonoodle](https://github.com/volcanonoodle) +- **Azure Monitor:** Add safety around usage of frame.Meta.Custom struct [#97766](https://github.com/grafana/grafana/pull/97766), [@adamyeats](https://github.com/adamyeats) +- **Azure/GCM:** Improve error display [#96921](https://github.com/grafana/grafana/pull/96921), [@aangelisc](https://github.com/aangelisc) +- **CloudWatch:** Fix conditions for fetching wildcards [#98648](https://github.com/grafana/grafana/pull/98648), [@iwysiu](https://github.com/iwysiu) +- **CloudWatch:** Fix interpolation of log groups when fetching fields [#98054](https://github.com/grafana/grafana/pull/98054), [@idastambuk](https://github.com/idastambuk) +- **Dashboard:** Fixes issue with compatability of old DashboardModel.annotations [#97328](https://github.com/grafana/grafana/pull/97328), [@torkelo](https://github.com/torkelo) +- **Dashboards:** Fix issue where filtered panels would not react to variable changes [#98718](https://github.com/grafana/grafana/pull/98718), [@oscarkilhed](https://github.com/oscarkilhed) +- **Dashboards:** Fixes week relative time ranges when weekStart was changed [#98167](https://github.com/grafana/grafana/pull/98167), [@torkelo](https://github.com/torkelo) +- **Dashboards:** Panel react for `timeFrom` and `timeShift` changes using variables [#98510](https://github.com/grafana/grafana/pull/98510), [@Sergej-Vlasov](https://github.com/Sergej-Vlasov) +- **DateTimePicker:** Fixes issue with date picker showing invalid date [#97888](https://github.com/grafana/grafana/pull/97888), [@torkelo](https://github.com/torkelo) +- **Fix:** Add support for datasource variable queries [#98098](https://github.com/grafana/grafana/pull/98098), [@sunker](https://github.com/sunker) +- **Fix:** Do not fetch Orgs if the user is authenticated by apikey/sa or render key [#97162](https://github.com/grafana/grafana/pull/97162), [@mgyongyosi](https://github.com/mgyongyosi) +- **Fix:** Double encoding of URLs when using data proxy [#98494](https://github.com/grafana/grafana/pull/98494), [@s4kh](https://github.com/s4kh) +- **Font:** Disable contextual font ligatures [#98521](https://github.com/grafana/grafana/pull/98521), [@ashharrison90](https://github.com/ashharrison90) +- **GrafanaUI:** Fix inconsistent controlled/uncontrolled state in AutoSizeInput [#96696](https://github.com/grafana/grafana/pull/96696), [@joshhunt](https://github.com/joshhunt) +- **GrafanaUI:** Revert: Fix inconsistent controlled/uncontrolled state in AutoSizeInput [#97551](https://github.com/grafana/grafana/pull/97551), [@itsmylife](https://github.com/itsmylife) +- **InfluxDB:** Adhoc filters can use template vars as values [#98567](https://github.com/grafana/grafana/pull/98567), [@bossinc](https://github.com/bossinc) +- **Library Panel:** Fix issue where library panels did not display panel links. [#98655](https://github.com/grafana/grafana/pull/98655), [@yincongcyincong](https://github.com/yincongcyincong) +- **LibraryPanel:** Fallback to panel title if library panel title is not set [#99411](https://github.com/grafana/grafana/pull/99411), [@ivanortegaalba](https://github.com/ivanortegaalba) +- **Loki:** Fix a bug when reading frames without values but warnings [#97197](https://github.com/grafana/grafana/pull/97197), [@svennergr](https://github.com/svennergr) +- **Loki:** Only hide a set of labels instead of every label starting with `__` [#98730](https://github.com/grafana/grafana/pull/98730), [@svennergr](https://github.com/svennergr) +- **Org:** Fix redirection logic to work consistently [#96521](https://github.com/grafana/grafana/pull/96521), [@yincongcyincong](https://github.com/yincongcyincong) +- **Panel inspect:** Fix file names of data download included uninterpolated variable names. [#98832](https://github.com/grafana/grafana/pull/98832), [@alexrosenfeld10](https://github.com/alexrosenfeld10) +- **Scenes:** Upgrade to 5.36.3 [#98661](https://github.com/grafana/grafana/pull/98661), [@ivanortegaalba](https://github.com/ivanortegaalba) +- **Snapshot:** Show proper breadcrumb path [#98806](https://github.com/grafana/grafana/pull/98806), [@ashharrison90](https://github.com/ashharrison90) +- **Time Picker:** Fix "Fiscal year start month" selection behaviour [#98576](https://github.com/grafana/grafana/pull/98576), [@ashharrison90](https://github.com/ashharrison90) +- **Unified Storage:** Add support for verify-full in postgres [#96825](https://github.com/grafana/grafana/pull/96825), [@chaudyg](https://github.com/chaudyg) +- **Unified Storage:** Use tls preferred when grafana db using ssl [#97378](https://github.com/grafana/grafana/pull/97378), [@owensmallwood](https://github.com/owensmallwood) +- **Usage Insights:** Fix usage insight errors being logged as [object Object] [#93502](https://github.com/grafana/grafana/pull/93502), [@mmandrus](https://github.com/mmandrus) + +### Breaking changes + +- **Loki:** Default to `/labels` API with `query` param instead of `/series` API [#97935](https://github.com/grafana/grafana/pull/97935), [@svennergr](https://github.com/svennergr) + +### Plugin development fixes & changes + +- **Grafana UI:** Re-add react-router-dom as a dependency [#97540](https://github.com/grafana/grafana/pull/97540), [@leventebalogh](https://github.com/leventebalogh) + + # 11.4.1 (2025-01-28) From dddfce2df76e571fe2090bd545d1d48f53d2f65a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:21:45 +0000 Subject: [PATCH 163/894] Update dependency knip to v5.43.6 (#99698) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 53483ac0007..f6881a6c123 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20982,8 +20982,8 @@ __metadata: linkType: hard "knip@npm:^5.10.0": - version: 5.43.5 - resolution: "knip@npm:5.43.5" + version: 5.43.6 + resolution: "knip@npm:5.43.6" dependencies: "@nodelib/fs.walk": "npm:3.0.1" "@snyk/github-codeowners": "npm:1.1.0" @@ -21007,7 +21007,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10/d6d7b6561b07fc1696aa109c822c4653c4cfdfd4aafb3bbb27e508415ff8b6e7d2fd14158cea800d59aa783e8fee25c641d1348374af86f743e2dee8140722d0 + checksum: 10/d843ed0f5b56baf5c29257308b0cf1956348cfa9d2b9b627420db023a6ccdaf54450f047fe900b263dc10291f395bc3eceef221dc6050e7fd55fb1fbe4fce3a2 languageName: node linkType: hard From 3ba0d8d4b53aed10b0d6c446a2c93d1d2c23aa35 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Tue, 28 Jan 2025 12:30:20 -0600 Subject: [PATCH 164/894] Unified Storage: Improve observability for indexing latency (#99700) * adds extra debug logs and a new metric for poller query latency to help us better understand watch and index latency for write events * adds trace span to the index for handling index write events --- pkg/storage/unified/resource/metrics.go | 12 ++++++++++++ pkg/storage/unified/resource/search.go | 11 +++++++++++ pkg/storage/unified/sql/backend.go | 5 +++++ 3 files changed, 28 insertions(+) diff --git a/pkg/storage/unified/resource/metrics.go b/pkg/storage/unified/resource/metrics.go index 8747fa52a8a..f17d0133b2f 100644 --- a/pkg/storage/unified/resource/metrics.go +++ b/pkg/storage/unified/resource/metrics.go @@ -15,6 +15,7 @@ var ( type StorageApiMetrics struct { WatchEventLatency *prometheus.HistogramVec + PollerLatency prometheus.Histogram } func NewStorageMetrics() *StorageApiMetrics { @@ -29,6 +30,15 @@ func NewStorageMetrics() *StorageApiMetrics { NativeHistogramMaxBucketNumber: 160, NativeHistogramMinResetDuration: time.Hour, }, []string{"resource"}), + PollerLatency: prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: "storage_server", + Name: "poller_query_latency_seconds", + Help: "poller query latency", + Buckets: instrument.DefBuckets, + NativeHistogramBucketFactor: 1.1, // enable native histograms + NativeHistogramMaxBucketNumber: 160, + NativeHistogramMinResetDuration: time.Hour, + }), } }) @@ -37,8 +47,10 @@ func NewStorageMetrics() *StorageApiMetrics { func (s *StorageApiMetrics) Collect(ch chan<- prometheus.Metric) { s.WatchEventLatency.Collect(ch) + s.PollerLatency.Collect(ch) } func (s *StorageApiMetrics) Describe(ch chan<- *prometheus.Desc) { s.WatchEventLatency.Describe(ch) + s.PollerLatency.Describe(ch) } diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index a769cb37d2a..a3cae66501f 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -394,10 +394,19 @@ func (s *searchSupport) init(ctx context.Context) error { // Async event func (s *searchSupport) handleEvent(ctx context.Context, evt *WrittenEvent) { + ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"HandleEvent") if !slices.Contains([]WatchEvent_Type{WatchEvent_ADDED, WatchEvent_MODIFIED, WatchEvent_DELETED}, evt.Type) { s.log.Info("ignoring watch event", "type", evt.Type) return } + defer span.End() + span.SetAttributes( + attribute.String("event_type", evt.Type.String()), + attribute.String("namespace", evt.Key.Namespace), + attribute.String("group", evt.Key.Group), + attribute.String("resource", evt.Key.Resource), + attribute.String("name", evt.Key.Name), + ) nsr := NamespacedResource{ Namespace: evt.Key.Namespace, @@ -447,7 +456,9 @@ func (s *searchSupport) handleEvent(ctx context.Context, evt *WrittenEvent) { // record latency from when event was created to when it was indexed latencySeconds := float64(time.Now().UnixMicro()-evt.ResourceVersion) / 1e6 + span.AddEvent("index latency", trace.WithAttributes(attribute.Float64("latency_seconds", latencySeconds))) if latencySeconds > 5 { + s.log.Debug("high index latency object details", "resource", evt.Key.Resource, "latency_seconds", latencySeconds, "name", evt.Object.GetName(), "namespace", evt.Object.GetNamespace(), "uid", evt.Object.GetUID()) s.log.Warn("high index latency", "latency", latencySeconds) } if IndexMetrics != nil { diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 0401c77a25c..1690d129a77 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -805,6 +805,8 @@ func fetchLatestRV(ctx context.Context, x db.ContextExecer, d sqltemplate.Dialec func (b *backend) poll(ctx context.Context, grp string, res string, since int64, stream chan<- *resource.WrittenEvent) (int64, error) { ctx, span := b.tracer.Start(ctx, tracePrefix+"poll") defer span.End() + + start := time.Now() var records []*historyPollResponse err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error { var err error @@ -820,6 +822,8 @@ func (b *backend) poll(ctx context.Context, grp string, res string, since int64, if err != nil { return 0, fmt.Errorf("poll history: %w", err) } + end := time.Now() + resource.NewStorageMetrics().PollerLatency.Observe(end.Sub(start).Seconds()) var nextRV int64 for _, rec := range records { @@ -847,6 +851,7 @@ func (b *backend) poll(ctx context.Context, grp string, res string, since int64, ResourceVersion: rec.ResourceVersion, // Timestamp: , // TODO: add timestamp } + b.log.Debug("poller sent event to stream", "namespace", rec.Key.Namespace, "group", rec.Key.Group, "resource", rec.Key.Resource, "name", rec.Key.Name, "action", rec.Action, "rv", rec.ResourceVersion) } return nextRV, nil From 516bd0fd1c570a19a3ac12cad3ac8c104c3b5d29 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 12:00:57 -0700 Subject: [PATCH 165/894] K8s: Folders: Fix get command (#99690) --- .../folder/folderimpl/folder_unifiedstorage.go | 11 ++++------- .../folder/folderimpl/folder_unifiedstorage_test.go | 9 +++++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index 3fe81899597..0308799fbde 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -106,27 +106,24 @@ func (s *Service) getFromApiServer(ctx context.Context, q *folder.GetFolderQuery var dashFolder *folder.Folder var err error switch { - case q.UID != nil: - if *q.UID == "" { - return &folder.GeneralFolder, nil - } + case q.UID != nil && *q.UID != "": dashFolder, err = s.unifiedStore.Get(ctx, *q) if err != nil { return nil, toFolderError(err) } // nolint:staticcheck - case q.ID != nil: + case q.ID != nil && *q.ID != 0: dashFolder, err = s.getFolderByIDFromApiServer(ctx, *q.ID, q.OrgID) if err != nil { return nil, toFolderError(err) } - case q.Title != nil: + case q.Title != nil && *q.Title != "": dashFolder, err = s.getFolderByTitleFromApiServer(ctx, q.OrgID, *q.Title, q.ParentUID) if err != nil { return nil, toFolderError(err) } default: - return nil, folder.ErrBadRequest.Errorf("either on of UID, ID, Title fields must be present") + return &folder.GeneralFolder, nil } if dashFolder.IsGeneral() { diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 3d5a84a6aad..977300ed7fd 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -424,9 +424,11 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { require.NoError(t, err) }) - t.Run("When get folder by ID should return folder", func(t *testing.T) { + t.Run("When get folder by ID and uid is an empty string should return folder by id", func(t *testing.T) { id := int64(123) + emptyString := "" query := &folder.GetFolderQuery{ + UID: &emptyString, ID: &id, OrgID: 1, SignedInUser: usr, @@ -482,10 +484,13 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { }) t.Run("Returns root folder", func(t *testing.T) { - t.Run("When the folder UID is blank should return the root folder", func(t *testing.T) { + t.Run("When the folder UID and title are blank, and id is 0, should return the root folder", func(t *testing.T) { emptyString := "" + idZero := int64(0) actual, err := folderService.Get(ctx, &folder.GetFolderQuery{ UID: &emptyString, + ID: &idZero, + Title: &emptyString, OrgID: 1, SignedInUser: usr, }) From 97f4a164d1a376c0b6b7c59a14fb4505f19b678d Mon Sep 17 00:00:00 2001 From: Aleksandar Petrov <8142643+aleks-p@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:02:53 -0400 Subject: [PATCH 166/894] [DOC] Add connection URL info to Pyroscope datasource doc (#99605) * [DOC] Add connection URL info to Pyroscope datasource doc * Fix wordlist violation * Apply suggestions from code review Co-authored-by: Kim Nylander <104772500+knylander-grafana@users.noreply.github.com> --------- Co-authored-by: Kim Nylander <104772500+knylander-grafana@users.noreply.github.com> --- .../configure-pyroscope-data-source.md | 28 +++++++++++++++---- .../pyroscope/query-profile-data.md | 2 +- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md b/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md index 8a344f6a7e6..6f6b30ad045 100644 --- a/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md +++ b/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md @@ -33,11 +33,11 @@ refs: destination: /docs/grafana//datasources/tempo/configure-tempo-data-source/ - pattern: /docs/grafana-cloud/ destination: docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/configure-tempo-data-source/ - provisioning-data-sources: + explore-profiles: - pattern: /docs/grafana/ - destination: /docs/grafana//administration/provisioning/#data-sources - - pattern: /docs/grafana-cloud/provision - destination: /docs/grafana//administration/provisioning/#data-sources + destination: /docs/grafana//explore/simplified-exploration/profiles/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/simplified-exploration/profiles/ --- # Configure the Grafana Pyroscope data source @@ -73,7 +73,7 @@ To configure basic settings for the data source, complete the following steps: 1. On the **Settings** tab, complete the **Name**, **Connection**, and **Authentication** sections. - Use the **Name** field to specify the name used for the data source in panels, queries, and Explore. Toggle the **Default** switch for the data source to be pre-selected for new panels. -- Under **Connection**, enter the **URL** of the Pyroscope instance. For example, `https://example.com:4100`. +- Under **Connection**, enter the **URL** of the Pyroscope instance. For example, `https://example.com:4100`. Refer to [Connection URL](#connection-url) for more information. - Complete the [**Authentication** section](#authentication). 1. Optional: Use **Additional settings** to configure other options. @@ -89,6 +89,24 @@ To modify an existing Pyroscope data source: 1. Optional: Use **Additional settings** to configure or modify other options. 1. After completing your updates, select **Save & test**. +#### Connection URL + +The data source connection URL should point to a location of a running Pyroscope backend. + +**Grafana Cloud Profiles** + +Your Grafana Cloud instance automatically includes a fully provisioned data source. + +If you are running a self-managed Grafana instance or need to configure an additional Pyroscope data source pointing to Grafana Cloud Profiles, you can find the Pyroscope URL under the **Manage your stack** section for your organization. + +**Self-managed Pyroscope backend** + +The connection URL for a self-managed Pyroscope backend depends on how Pyroscope is deployed. +Refer to the steps under [Query profiles in Grafana](https://grafana.com/docs/pyroscope//deploy-kubernetes/helm/#query-profiles-in-grafana) for more information on how to configure the data source. + +If you plan to use the [Explore Profiles](ref:explore-profiles) application and you are running a self-managed Pyroscope backend in microservices mode, the data source connection URL should point to a gateway or proxy that routes requests to the corresponding Pyroscope service. +Refer to the [Helm ingress configuration](https://github.com/grafana/pyroscope/blob/main/operations/pyroscope/helm/pyroscope/templates/ingress.yaml) for specific routing requirements. + ## Authentication Use this section to select an authentication method to access the data source. diff --git a/docs/sources/datasources/pyroscope/query-profile-data.md b/docs/sources/datasources/pyroscope/query-profile-data.md index d56a629d42d..5f391efe0b7 100644 --- a/docs/sources/datasources/pyroscope/query-profile-data.md +++ b/docs/sources/datasources/pyroscope/query-profile-data.md @@ -33,7 +33,7 @@ refs: destination: /docs/grafana//explore/simplified-exploration/profiles/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/simplified-exploration/profiles/ - explore-profile-install: + explore-profiles-install: - pattern: /docs/grafana/ destination: /docs/grafana//explore/simplified-exploration/profiles/access/ - pattern: /docs/grafana-cloud/ From a0bf9202f54057a1810497ee5da0a8fe429e8d38 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 28 Jan 2025 20:15:19 +0100 Subject: [PATCH 167/894] Alerting: Clear the state cache when the alert routine stops (#99681) --- pkg/services/ngalert/schedule/alert_rule.go | 23 +++--- .../ngalert/schedule/alert_rule_test.go | 71 +++++++++++++++---- pkg/services/ngalert/state/manager.go | 10 ++- 3 files changed, 83 insertions(+), 21 deletions(-) diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index f4a301aff78..85c254b0a59 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -352,16 +352,23 @@ func (a *alertRule) Run() error { case <-grafanaCtx.Done(): reason := grafanaCtx.Err() - // clean up the state only if the reason for stopping the evaluation loop is that the rule was deleted + + // We do not want a context to be unbounded which could potentially cause a go routine running + // indefinitely. 1 minute is an almost randomly chosen timeout, big enough to cover the majority of the + // cases. + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) + defer cancelFunc() + if errors.Is(reason, errRuleDeleted) { - // We do not want a context to be unbounded which could potentially cause a go routine running - // indefinitely. 1 minute is an almost randomly chosen timeout, big enough to cover the majority of the - // cases. - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) - defer cancelFunc() - states := a.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, a.key.AlertRuleKey), a.key, ngmodels.StateReasonRuleDeleted) - a.expireAndSend(grafanaCtx, states) + // Clean up the state and send resolved notifications for firing alerts only if the reason for stopping + // the evaluation loop is that the rule was deleted. + stateTransitions := a.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, a.key.AlertRuleKey), a.key, ngmodels.StateReasonRuleDeleted) + a.expireAndSend(grafanaCtx, stateTransitions) + } else { + // Otherwise, just clean up the cache. + a.stateManager.ForgetStateByRuleUID(ngmodels.WithRuleKey(ctx, a.key.AlertRuleKey), a.key) } + a.logger.Debug("Stopping alert rule routine", "reason", reason) return nil } diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index 4593a4052e0..02ea0dd5735 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -3,6 +3,7 @@ package schedule import ( "bytes" "context" + "errors" "fmt" "math" "math/rand" @@ -278,7 +279,12 @@ func TestAlertRuleIdentifier(t *testing.T) { } func blankRuleForTests(ctx context.Context, key models.AlertRuleKeyWithGroup) *alertRule { - return newAlertRule(ctx, key, nil, false, 0, nil, nil, nil, nil, nil, nil, log.NewNopLogger(), nil, nil, nil) + managerCfg := state.ManagerCfg{ + Historian: &state.FakeHistorian{}, + 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) } func TestRuleRoutine(t *testing.T) { @@ -477,12 +483,26 @@ func TestRuleRoutine(t *testing.T) { } t.Run("should exit", func(t *testing.T) { - t.Run("and not clear the state if parent context is cancelled", func(t *testing.T) { - stoppedChan := make(chan error) - sch, _, _, _ := createSchedule(make(chan time.Time), nil) + rule := gen.With(withQueryForState(t, eval.Alerting)).GenerateRef() + genEvalResults := func(now time.Time) eval.Results { + return eval.GenerateResults( + rand.Intn(5)+1, + eval.ResultGen( + eval.WithEvaluatedAt(now), + // State should be alerting to test resolved notifications in some cases. + // When the alert rule is firing and is deleted, we should send + // resolved notifications. + eval.WithState(eval.Alerting), + ), + ) + } - rule := gen.GenerateRef() - _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, eval.GenerateResults(rand.Intn(5)+1, eval.ResultGen(eval.WithEvaluatedAt(sch.clock.Now()))), nil, nil) + t.Run("and clean up the state if parent context is cancelled", func(t *testing.T) { + stoppedChan := make(chan error) + sender := NewSyncAlertsSenderMock() + sch, _, _, _ := createSchedule(make(chan time.Time), sender) + + _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, genEvalResults(sch.clock.Now()), nil, nil) expectedStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) require.NotEmpty(t, expectedStates) @@ -497,14 +517,40 @@ func TestRuleRoutine(t *testing.T) { cancel() err := waitForErrChannel(t, stoppedChan) require.NoError(t, err) - require.Equal(t, len(expectedStates), len(sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID))) + require.Empty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) + sender.AlertsSenderMock.AssertNotCalled(t, "Send") }) - t.Run("and clean up the state if delete is cancellation reason for inner context", func(t *testing.T) { - stoppedChan := make(chan error) - sch, _, _, _ := createSchedule(make(chan time.Time), nil) - rule := gen.GenerateRef() - _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, eval.GenerateResults(rand.Intn(5)+1, eval.ResultGen(eval.WithEvaluatedAt(sch.clock.Now()))), nil, nil) + t.Run("and clean up the state but not send anything if the reason is not rule deleted", func(t *testing.T) { + stoppedChan := make(chan error) + sender := NewSyncAlertsSenderMock() + sch, _, _, _ := createSchedule(make(chan time.Time), sender) + + _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, genEvalResults(sch.clock.Now()), nil, nil) + require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) + + factory := ruleFactoryFromScheduler(sch) + ruleInfo := factory.new(context.Background(), rule) + go func() { + err := ruleInfo.Run() + stoppedChan <- err + }() + + ruleInfo.Stop(errors.New("some reason")) + err := waitForErrChannel(t, stoppedChan) + require.NoError(t, err) + + require.Empty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) + sender.AlertsSenderMock.AssertNotCalled(t, "Send") + }) + + t.Run("and send resolved notifications if errRuleDeleted is the reason for stopping", func(t *testing.T) { + stoppedChan := make(chan error) + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, mock.Anything, mock.Anything).Times(1) + sch, _, _, _ := createSchedule(make(chan time.Time), sender) + + _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, genEvalResults(sch.clock.Now()), nil, nil) require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) factory := ruleFactoryFromScheduler(sch) @@ -519,6 +565,7 @@ func TestRuleRoutine(t *testing.T) { require.NoError(t, err) require.Empty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) + sender.AlertsSenderMock.AssertExpectations(t) }) }) diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 3194527626b..b503f7ad25c 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -242,7 +242,7 @@ func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.Al logger := st.log.FromContext(ctx) logger.Debug("Resetting state of the rule") - states := st.cache.removeByRuleUID(ruleKey.OrgID, ruleKey.UID) + states := st.ForgetStateByRuleUID(ctx, ruleKey) if len(states) == 0 { return nil @@ -280,11 +280,19 @@ func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.Al logger.Error("Failed to delete states that belong to a rule from database", "error", err) } } + logger.Info("Rules state was reset", "states", len(states)) return transitions } +func (st *Manager) ForgetStateByRuleUID(ctx context.Context, ruleKey ngModels.AlertRuleKeyWithGroup) []*State { + logger := st.log.FromContext(ctx) + logger.Debug("Removing rule state from cache") + + return st.cache.removeByRuleUID(ruleKey.OrgID, ruleKey.UID) +} + // ResetStateByRuleUID removes the rule instances from cache and instanceStore and saves state history. If the state // history has to be saved, rule must not be nil. func (st *Manager) ResetStateByRuleUID(ctx context.Context, rule *ngModels.AlertRule, reason string) []StateTransition { From f55686a0b43a69b0dd118547fc766dc6b5eccabc Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Tue, 28 Jan 2025 13:27:01 -0600 Subject: [PATCH 168/894] Unified Storage: Adds some more traces to search and the bleve search impl (#99704) adds some more traces to search and the bleve search impl --- pkg/storage/unified/resource/search.go | 6 ++++++ pkg/storage/unified/search/bleve.go | 24 +++++++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index a3cae66501f..c7aa0c5d03a 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -230,6 +230,9 @@ func (s *searchSupport) CountRepositoryObjects(ctx context.Context, req *CountRe // Search implements ResourceIndexServer. func (s *searchSupport) Search(ctx context.Context, req *ResourceSearchRequest) (*ResourceSearchResponse, error) { + ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"Search") + defer span.End() + nsr := NamespacedResource{ Group: req.Options.Key.Group, Namespace: req.Options.Key.Namespace, @@ -471,6 +474,9 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso return nil, fmt.Errorf("search is not configured properly (missing unifiedStorageSearch feature toggle?)") } + ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"GetOrCreateIndex") + defer span.End() + // TODO??? // We want to block while building the index and return the same index for the key // simple mutex not great... we don't want to block while anything in building, just the same key diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index d0c8b0f4ca7..145bd025cd7 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -177,6 +177,7 @@ func (b *bleveBackend) BuildIndex(ctx context.Context, fields: fields, standard: resource.StandardSearchFields(), features: b.features, + tracing: b.tracer, } idx.allFields, err = getAllFields(idx.standard, fields) @@ -250,6 +251,7 @@ type bleveIndex struct { batchSize int // ??? not totally sure the units here features featuremgmt.FeatureToggles + tracing trace.Tracer } // Write implements resource.DocumentIndex. @@ -404,6 +406,9 @@ func (b *bleveIndex) Search( req *resource.ResourceSearchRequest, federate []resource.ResourceIndex, // For federated queries, these will match the values in req.federate ) (*resource.ResourceSearchResponse, error) { + ctx, span := b.tracing.Start(ctx, tracingPrexfixBleve+"Search") + defer span.End() + if req.Options == nil || req.Options.Key == nil { return &resource.ResourceSearchResponse{ Error: resource.NewBadRequestError("missing query key"), @@ -418,7 +423,7 @@ func (b *bleveIndex) Search( } // Verifies the index federation - index, err := b.getIndex(req, federate) + index, err := b.getIndex(ctx, req, federate) if err != nil { return nil, err } @@ -448,7 +453,7 @@ func (b *bleveIndex) Search( response.QueryCost = float64(res.Cost) response.MaxScore = res.MaxScore - response.Results, err = b.hitsToTable(searchrequest.Fields, res.Hits, req.Explain) + response.Results, err = b.hitsToTable(ctx, searchrequest.Fields, res.Hits, req.Explain) if err != nil { return nil, err } @@ -465,6 +470,9 @@ func (b *bleveIndex) Search( } func (b *bleveIndex) DocCount(ctx context.Context, folder string) (int64, error) { + ctx, span := b.tracing.Start(ctx, tracingPrexfixBleve+"DocCount") + defer span.End() + if folder == "" { count, err := b.index.DocCount() return int64(count), err @@ -500,9 +508,13 @@ func (b *bleveIndex) verifyKey(key *resource.ResourceKey) *resource.ErrorResult } func (b *bleveIndex) getIndex( + ctx context.Context, req *resource.ResourceSearchRequest, federate []resource.ResourceIndex, ) (bleve.Index, error) { + _, span := b.tracing.Start(ctx, tracingPrexfixBleve+"getIndex") + defer span.End() + if len(req.Federated) != len(federate) { return nil, fmt.Errorf("federation is misconfigured") } @@ -527,6 +539,9 @@ func (b *bleveIndex) getIndex( } func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resource.ResourceSearchRequest, access authlib.AccessClient) (*bleve.SearchRequest, *resource.ErrorResult) { + ctx, span := b.tracing.Start(ctx, tracingPrexfixBleve+"toBleveSearchRequest") + defer span.End() + facets := bleve.FacetsRequest{} for _, f := range req.Facet { facets[f.Field] = bleve.NewFacetRequest(f.Field, int(f.Limit)) @@ -739,7 +754,10 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r ) } -func (b *bleveIndex) hitsToTable(selectFields []string, hits search.DocumentMatchCollection, explain bool) (*resource.ResourceTable, error) { +func (b *bleveIndex) hitsToTable(ctx context.Context, selectFields []string, hits search.DocumentMatchCollection, explain bool) (*resource.ResourceTable, error) { + _, span := b.tracing.Start(ctx, tracingPrexfixBleve+"hitsToTable") + defer span.End() + fields := []*resource.ResourceTableColumnDefinition{} for _, name := range selectFields { if name == "_all" { From 046754c3c2610cf075b06cdec3a78fc663447ab0 Mon Sep 17 00:00:00 2001 From: Nikita Pande <37657012+nikita15p@users.noreply.github.com> Date: Wed, 29 Jan 2025 01:27:53 +0530 Subject: [PATCH 169/894] [TLS] Remove the hard-coded TLS ciphers in http.go to fix Pen test findings (#98749) Remove the hard-coded TLS ciphers in http.go to fix Pen test findings Signed-off-by: GitHub --- pkg/api/http_server.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 5d2308e7540..21f6f2dcd56 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -796,13 +796,9 @@ func (hs *HTTPServer) getDefaultCiphers(tlsVersion uint16, protocol string) []ui tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_RSA_WITH_AES_128_CBC_SHA, - tls.TLS_RSA_WITH_AES_256_CBC_SHA, } } if protocol == "h2" { From 055a63873ba3c355a2769e7c89766193bcb8f123 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:21:41 -0500 Subject: [PATCH 170/894] Docs: add pan and zoom key combos (#99523) --- .../panels-visualizations/visualizations/canvas/index.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/sources/panels-visualizations/visualizations/canvas/index.md b/docs/sources/panels-visualizations/visualizations/canvas/index.md index ebd7162047e..b31b8018902 100644 --- a/docs/sources/panels-visualizations/visualizations/canvas/index.md +++ b/docs/sources/panels-visualizations/visualizations/canvas/index.md @@ -258,6 +258,13 @@ You can enable panning and zooming in a canvas. This allows you to both create a {{< docs/public-preview product="Canvas pan and zoom" featureFlag="`canvasPanelPanZoom`" >}} +Use the following pointer and keyboard strokes: + +- **Zoom in** - Scroll up +- **Zoom out** - Scroll down +- **Pan** - Middle mouse/wheel + drag OR Control + right-click + drag +- **Reset** - Double-click + {{< video-embed src="/media/docs/grafana/2024-01-05-Canvas-Pan-&-Zoom-Enablement-Video.mp4" max-width="750px" alt="Canvas pan and zoom enablement video" >}} ##### Infinite panning From b4b49fc587e94f57fd8f8c6abd3b879eb3c7c80b Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 14:24:55 -0700 Subject: [PATCH 171/894] K8s: Dashboards: use title sort field instead (#99712) --- pkg/services/dashboards/service/dashboard_service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index ad78d14952f..8f4953d2723 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1689,9 +1689,9 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex // but is currently not needed by other services in the backend if query.Title != "" { req := []*resource.Requirement{{ - Key: resource.SEARCH_FIELD_TITLE, + Key: resource.SEARCH_FIELD_TITLE_SORT, // use title sort to prevent issues with `-` in the title & how bleve searches Operator: string(selection.In), - Values: []string{query.Title}, + Values: []string{strings.ToLower(query.Title)}, }} request.Options.Fields = append(request.Options.Fields, req...) } From d7070d11f6335eb88500d84dff0c7027d3d5f25b Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 14:53:12 -0700 Subject: [PATCH 172/894] k8s: Dashboard history: Fix created by (#99714) --- .../dashboardversion/dashverimpl/dashver.go | 9 ++++ .../dashverimpl/dashver_test.go | 42 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 02bed95b96e..98406b6a7b4 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -283,6 +283,15 @@ func (s *Service) UnstructuredToLegacyDashboardVersion(ctx context.Context, item return nil, err } + // if updated by is set, then this version of the dashboard was "created" + // by that user + if obj.GetUpdatedBy() != "" { + updatedBy, err := s.k8sclient.GetUserFromMeta(ctx, obj.GetUpdatedBy()) + if err == nil && updatedBy != nil { + createdBy = updatedBy + } + } + id, err := obj.GetResourceVersionInt64() if err != nil { return nil, err diff --git a/pkg/services/dashboardversion/dashverimpl/dashver_test.go b/pkg/services/dashboardversion/dashverimpl/dashver_test.go index ed58c27bb7f..5d71b3457d1 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver_test.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver_test.go @@ -39,7 +39,7 @@ func TestDashboardVersionService(t *testing.T) { require.Equal(t, dashboard.ToDTO("uid"), dashboardVersion) }) - t.Run("Get dashboard version through k8s", func(t *testing.T) { + t.Run("Get dashboard versions through k8s", func(t *testing.T) { dashboardService := dashboards.NewFakeDashboardService(t) dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()} mockCli := new(client.MockK8sHandler) @@ -47,7 +47,7 @@ func TestDashboardVersionService(t *testing.T) { dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards) dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil) - mockCli.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) + mockCli.On("GetUserFromMeta", mock.Anything, "user:1").Return(&user.User{ID: 1}, nil) mockCli.On("Get", mock.Anything, "uid", int64(1), v1.GetOptions{ResourceVersion: "10"}, mock.Anything).Return(&unstructured.Unstructured{ Object: map[string]any{ "metadata": map[string]any{ @@ -56,6 +56,9 @@ func TestDashboardVersionService(t *testing.T) { "labels": map[string]any{ utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck }, + "annotations": map[string]any{ + utils.AnnoKeyCreatedBy: "user:1", + }, }, "spec": map[string]any{ "version": int64(10), @@ -73,8 +76,43 @@ func TestDashboardVersionService(t *testing.T) { ParentVersion: 9, DashboardID: 42, DashboardUID: "uid", + CreatedBy: 1, Data: simplejson.NewFromAny(map[string]any{"uid": "uid", "version": int64(10)}), }) + + mockCli.On("GetUserFromMeta", mock.Anything, "user:2").Return(&user.User{ID: 2}, nil) + mockCli.On("Get", mock.Anything, "uid", int64(1), v1.GetOptions{ResourceVersion: "11"}, mock.Anything).Return(&unstructured.Unstructured{ + Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "resourceVersion": "11", + "labels": map[string]any{ + utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + }, + "annotations": map[string]any{ + utils.AnnoKeyCreatedBy: "user:1", + utils.AnnoKeyUpdatedBy: "user:2", // if updated by is set, that is the version creator + }, + }, + "spec": map[string]any{ + "version": int64(11), + }, + }}, nil).Once() + res, err = dashboardVersionService.Get(context.Background(), &dashver.GetDashboardVersionQuery{ + DashboardID: 42, + OrgID: 1, + Version: 11, + }) + require.Nil(t, err) + require.Equal(t, res, &dashver.DashboardVersionDTO{ + ID: 11, // RV should be used + Version: 11, + ParentVersion: 10, + DashboardID: 42, + DashboardUID: "uid", + CreatedBy: 2, + Data: simplejson.NewFromAny(map[string]any{"uid": "uid", "version": int64(11)}), + }) }) } From 6ba18d05be5a21205f6c080707aa19c609d2c11d Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 15:06:53 -0700 Subject: [PATCH 173/894] Folders: fix deletion logic that relies on the dashboard store (#99715) --- .../folderimpl/folder_unifiedstorage.go | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index 0308799fbde..5135793a796 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" + dashboardv0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/metrics" @@ -587,16 +588,54 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol // if dashboard restore is on we don't delete public dashboards, the hard delete will take care of it later if !s.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) { // We need a list of dashboard uids inside the folder to delete related public dashboards - dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{SignedInUser: cmd.SignedInUser, FolderUIDs: folders, OrgId: cmd.OrgID}) - if err != nil { - return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err) - } + var dashboardUIDs []string + // we cannot use the dashboard service directly due to circular dependencies, + // so either use the search client if the feature is enabled or use the dashboard store + if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { + dashboardKey := &resource.ResourceKey{ + Namespace: s.k8sclient.getNamespace(cmd.OrgID), + Group: dashboardv0.DashboardResourceInfo.GroupVersionResource().Group, + Resource: dashboardv0.DashboardResourceInfo.GroupVersionResource().Resource, + } + request := &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Key: dashboardKey, + Labels: []*resource.Requirement{}, + Fields: []*resource.Requirement{ + { + Key: resource.SEARCH_FIELD_FOLDER, + Operator: string(selection.In), + Values: folders, + }, + }, + }, + Limit: 100000} - dashboardUIDs := make([]string, 0, len(dashes)) - for _, dashboard := range dashes { - dashboardUIDs = append(dashboardUIDs, dashboard.UID) - } + client := s.k8sclient.getSearcher(ctx) + res, err := client.Search(ctx, request) + if err != nil { + return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err) + } + hits, err := dashboardsearch.ParseResults(res, 0) + if err != nil { + return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err) + } + + dashboardUIDs = make([]string, len(hits.Hits)) + for i, dashboard := range hits.Hits { + dashboardUIDs[i] = dashboard.Name + } + } else { + dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{SignedInUser: cmd.SignedInUser, FolderUIDs: folders, OrgId: cmd.OrgID}) + if err != nil { + return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err) + } + dashboardUIDs = make([]string, len(dashes)) + for i, dashboard := range dashes { + dashboardUIDs[i] = dashboard.UID + } + } // Delete all public dashboards in the folders err = s.publicDashboardService.DeleteByDashboardUIDs(ctx, cmd.OrgID, dashboardUIDs) if err != nil { From 5cd1efb2d199a356c3cb9bc7a35b666928a33b0f Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 29 Jan 2025 03:47:02 +0300 Subject: [PATCH 174/894] K8s/Dashboard: Improve legacy error handling (#99658) --- .../apis/dashboard/legacy/sql_dashboards.go | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index f3b86f8b639..0175e52b8bc 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -102,10 +102,10 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, sql *legacysql.LegacyD return nil, fmt.Errorf("execute template %q: %w", tmpl.Name(), err) } q := rawQuery - // if true { - // pretty := sqltemplate.RemoveEmptyLines(rawQuery) - // fmt.Printf("DASHBOARD QUERY: %s [%+v] // %+v\n", pretty, req.GetArgs(), query) - // } + if false { + pretty := sqltemplate.RemoveEmptyLines(rawQuery) + fmt.Printf("DASHBOARD QUERY: %s [%+v] // %+v\n", pretty, req.GetArgs(), query) + } rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) if err != nil { @@ -132,12 +132,16 @@ type rowsWrapper struct { a *dashboardSqlAccess rows *sql.Rows history bool + count int canReadDashboard func(scopes ...string) bool // Current row *dashboardRow err error + + // max 100 rejected? + rejected []dashboardRow } func (a *dashboardSqlAccess) GetResourceStats(ctx context.Context, namespace string, minCount int) ([]resource.ResourceStats, error) { @@ -159,10 +163,16 @@ func (r *rowsWrapper) Next() bool { // breaks after first readable value for r.rows.Next() { + r.count++ + r.row, err = r.a.scanRow(r.rows, r.history) if err != nil { - r.err = err - return false + if len(r.rejected) > 1000 || r.row == nil { + r.err = fmt.Errorf("too many rejected rows (%d) %w", len(r.rejected), err) + return false + } + r.rejected = append(r.rejected, *r.row) + continue } if r.row != nil { @@ -177,7 +187,7 @@ func (r *rowsWrapper) Next() bool { continue } - // returns the first folder it can + // returns the first visible dashboard return true } } @@ -245,7 +255,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo var createdByID sql.NullInt64 var message sql.NullString - var plugin_id string + var plugin_id sql.NullString var origin_name sql.NullString var origin_path sql.NullString var origin_ts sql.NullInt64 @@ -319,17 +329,17 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo repo.Path = originPath } meta.SetRepositoryInfo(repo) - } else if plugin_id != "" { + } else if plugin_id.String != "" { meta.SetRepositoryInfo(&utils.ResourceRepositoryInfo{ Name: "plugin", - Path: plugin_id, + Path: plugin_id.String, }) } if len(data) > 0 { err = dash.Spec.UnmarshalJSON(data) if err != nil { - return row, err + return row, fmt.Errorf("JSON unmarshal error for: %s // %w", dash.Name, err) } } dash.Spec.Remove("id") @@ -453,12 +463,12 @@ func (a *dashboardSqlAccess) GetLibraryPanels(ctx context.Context, query Library return nil, fmt.Errorf("expected non zero orgID") } - sql, err := a.sql(ctx) + sqlx, err := a.sql(ctx) if err != nil { return nil, err } - req := newLibraryQueryReq(sql, &query) + req := newLibraryQueryReq(sqlx, &query) rawQuery, err := sqltemplate.Execute(sqlQueryPanels, req) if err != nil { return nil, fmt.Errorf("execute template %q: %w", sqlQueryPanels.Name(), err) @@ -466,7 +476,7 @@ func (a *dashboardSqlAccess) GetLibraryPanels(ctx context.Context, query Library q := rawQuery res := &dashboard.LibraryPanelList{} - rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) + rows, err := sqlx.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) defer func() { if rows != nil { _ = rows.Close() @@ -479,7 +489,7 @@ func (a *dashboardSqlAccess) GetLibraryPanels(ctx context.Context, query Library type panel struct { ID int64 UID string - FolderUID string + FolderUID sql.NullString Created time.Time CreatedBy string @@ -551,7 +561,9 @@ func (a *dashboardSqlAccess) GetLibraryPanels(ctx context.Context, query Library if err != nil { return nil, err } - meta.SetFolder(p.FolderUID) + if p.FolderUID.Valid { + meta.SetFolder(p.FolderUID.String) + } meta.SetCreatedBy(p.CreatedBy) meta.SetGeneration(1) meta.SetDeprecatedInternalID(p.ID) //nolint:staticcheck @@ -570,7 +582,7 @@ func (a *dashboardSqlAccess) GetLibraryPanels(ctx context.Context, query Library } } if query.UID == "" { - rv, err := sql.GetResourceVersion(ctx, "library_element", "updated") + rv, err := sqlx.GetResourceVersion(ctx, "library_element", "updated") if err == nil { res.ResourceVersion = strconv.FormatInt(rv, 10) } From 745a25ad0a4af5af7f6eddaa79dd7c4da9eb2b35 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 19:13:26 -0700 Subject: [PATCH 175/894] Folders API: Return orgID in response (#99724) --- pkg/api/folder.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index eb4cb853dda..185eb427d7d 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -412,6 +412,7 @@ func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, f *folder.Folde return dtos.Folder{ ID: f.ID, // nolint:staticcheck UID: f.UID, + OrgID: f.OrgID, Title: f.Title, URL: f.URL, HasACL: f.HasACL, From 6908f914282dbcab29fce89422ccf785b4330ac5 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 19:57:34 -0700 Subject: [PATCH 176/894] Search fallback: prevent for now (#99725) --- pkg/registry/apis/dashboard/search_test.go | 5 ++--- pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/search_client.go | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index 23d7ad18278..b48c4a6fdd1 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -7,15 +7,14 @@ import ( "testing" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "google.golang.org/grpc" ) +/* Temporarily disabled search fallback while we add functionality func TestSearchFallback(t *testing.T) { t.Run("should hit legacy search handler on mode 0", func(t *testing.T) { mockClient := &MockClient{} @@ -172,7 +171,7 @@ func TestSearchFallback(t *testing.T) { t.Fatalf("expected Search NOT to be called, but it was") } }) -} +}*/ func TestSearchHandlerFields(t *testing.T) { // Create a mock client diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index c2b1383e9c9..b70c1800e01 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -17,7 +17,6 @@ require ( github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible github.com/grafana/grafana-plugin-sdk-go v0.263.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d - github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/prometheus/client_golang v1.20.5 @@ -121,6 +120,7 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.29.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/pkg/apiserver v0.0.0-20250121113133-e747350fee2d // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/grafana/sqlds/v4 v4.1.3 // indirect diff --git a/pkg/storage/unified/resource/search_client.go b/pkg/storage/unified/resource/search_client.go index 43e0faa7b1e..2ecbd5ee2bb 100644 --- a/pkg/storage/unified/resource/search_client.go +++ b/pkg/storage/unified/resource/search_client.go @@ -1,12 +1,11 @@ package resource import ( - "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/setting" ) func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient ResourceIndexClient, legacyClient ResourceIndexClient) ResourceIndexClient { - config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey] + /*config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey] if !ok { return legacyClient } @@ -16,5 +15,6 @@ func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedCl return legacyClient default: return unifiedClient - } + }*/ + return unifiedClient } From 20f02ec12f7c85fb6dcca3078dca3469416d3b42 Mon Sep 17 00:00:00 2001 From: maicon Date: Wed, 29 Jan 2025 00:19:38 -0300 Subject: [PATCH 177/894] Unistore: refactor provisioning to work with folder service (#99473) --- pkg/apimachinery/identity/context.go | 2 + .../dashboard/legacysearcher/search_client.go | 2 + pkg/registry/apis/dashboard/search_test.go | 8 ++- pkg/server/server.go | 2 +- .../folderimpl/folder_unifiedstorage.go | 4 +- .../folderimpl/folder_unifiedstorage_test.go | 2 +- .../alerting/rules_provisioner.go | 7 +- .../provisioning/dashboards/file_reader.go | 47 +++++++------ .../dashboards/file_reader_test.go | 69 +++++++++++++------ .../provisioning/dashboards/validator_test.go | 60 ++++++++++++---- pkg/services/provisioning/provisioning.go | 15 +++- .../provisioning/provisioning_test.go | 15 +++- pkg/storage/unified/resource/search_client.go | 24 ++++--- 13 files changed, 180 insertions(+), 77 deletions(-) diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index ae71f06eaf1..b2266899861 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -71,6 +71,8 @@ var serviceIdentityPermissions = getWildcardPermissions( "dashboards:write", "dashboards:create", "datasources:read", + "alert.provisioning:write", + "alert.provisioning.secrets:read", ) func IsServiceIdentity(ctx context.Context) bool { diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index ac221ba86e2..17aa5aa16a9 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -8,6 +8,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/storage/unified/resource" "google.golang.org/grpc" ) @@ -48,6 +49,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour Title: req.Query, Limit: req.Limit, // FolderUIDs: req.FolderUIDs, + Type: searchstore.TypeDashboard, SignedInUser: user, } diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index b48c4a6fdd1..fbd3a209ff5 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -14,7 +14,10 @@ import ( "google.golang.org/grpc" ) -/* Temporarily disabled search fallback while we add functionality +/* +Search Fallback was returning both Folders and Dashboards which resulted +in issues with rendering the Folder UI. Also, filters are not implemented +yet. For those reasons, we will be disabling Search Fallback for now func TestSearchFallback(t *testing.T) { t.Run("should hit legacy search handler on mode 0", func(t *testing.T) { mockClient := &MockClient{} @@ -171,7 +174,8 @@ func TestSearchFallback(t *testing.T) { t.Fatalf("expected Search NOT to be called, but it was") } }) -}*/ +} +*/ func TestSearchHandlerFields(t *testing.T) { // Create a mock client diff --git a/pkg/server/server.go b/pkg/server/server.go index ca1263d9e4d..a56b6c9f184 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -131,7 +131,7 @@ func (s *Server) Init() error { return err } - return s.provisioningService.RunInitProvisioners(s.context) + return nil } // Run initializes and starts services. This will block until all services have diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index 5135793a796..ae1f579efd0 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -240,9 +240,9 @@ func (s *Service) getFolderByTitleFromApiServer(ctx context.Context, orgID int64 Key: folderkey, Fields: []*resource.Requirement{ { - Key: resource.SEARCH_FIELD_TITLE, + Key: resource.SEARCH_FIELD_TITLE_SORT, Operator: string(selection.In), - Values: []string{title}, + Values: []string{strings.ToLower(title)}, }, }, Labels: []*resource.Requirement{}, diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 977300ed7fd..a19c83f209e 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -567,7 +567,7 @@ func (r resourceClientMock) Search(ctx context.Context, in *resource.ResourceSea } if len(in.Options.Fields) > 0 && - in.Options.Fields[0].Key == resource.SEARCH_FIELD_TITLE && + in.Options.Fields[0].Key == resource.SEARCH_FIELD_TITLE_SORT && in.Options.Fields[0].Operator == "in" && len(in.Options.Fields[0].Values) > 0 && in.Options.Fields[0].Values[0] == "foo" { diff --git a/pkg/services/provisioning/alerting/rules_provisioner.go b/pkg/services/provisioning/alerting/rules_provisioner.go index 4baefedfebf..f41e48a94da 100644 --- a/pkg/services/provisioning/alerting/rules_provisioner.go +++ b/pkg/services/provisioning/alerting/rules_provisioner.go @@ -45,7 +45,8 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context, files []*AlertingFile) error { for _, file := range files { for _, group := range file.Groups { - u := provisionerUser(group.OrgID) + ctx, u := identity.WithServiceIdentitiy(ctx, group.OrgID) + folderUID, err := prov.getOrCreateFolderFullpath(ctx, group.FolderFullpath, group.OrgID) if err != nil { prov.logger.Error("failed to get or create folder", "folder", group.FolderFullpath, "org", group.OrgID, "err", err) @@ -120,11 +121,13 @@ func (prov *defaultAlertRuleProvisioner) getOrCreateFolderFullpath( func (prov *defaultAlertRuleProvisioner) getOrCreateFolderByTitle( ctx context.Context, folderName string, orgID int64, parentUID *string) (string, error) { + ctx, user := identity.WithServiceIdentitiy(ctx, orgID) + cmd := &folder.GetFolderQuery{ Title: &folderName, ParentUID: parentUID, OrgID: orgID, - SignedInUser: provisionerUser(orgID), + SignedInUser: user, } cmdResult, err := prov.folderService.Get(ctx, cmd) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index e36bedda79e..b7c54ddcdb0 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" @@ -147,6 +148,8 @@ func (fr *FileReader) isDatabaseAccessRestricted() bool { // storeDashboardsInFolder saves dashboards from the filesystem on disk to the folder from config func (fr *FileReader) storeDashboardsInFolder(ctx context.Context, filesFoundOnDisk map[string]os.FileInfo, dashboardRefs map[string]*dashboards.DashboardProvisioning, usageTracker *usageTracker) error { + ctx, _ = identity.WithServiceIdentitiy(ctx, fr.Cfg.OrgID) + folderID, folderUID, err := fr.getOrCreateFolder(ctx, fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder) if err != nil && !errors.Is(err, ErrFolderNameMissing) { return fmt.Errorf("%w with name %q: %w", ErrGetOrCreateFolder, fr.Cfg.Folder, err) @@ -177,6 +180,7 @@ func (fr *FileReader) storeDashboardsInFoldersFromFileStructure(ctx context.Cont folderName = filepath.Base(dashboardsFolder) } + ctx, _ = identity.WithServiceIdentitiy(ctx, fr.Cfg.OrgID) folderID, folderUID, err := fr.getOrCreateFolder(ctx, fr.Cfg, fr.dashboardProvisioningService, folderName) if err != nil && !errors.Is(err, ErrFolderNameMissing) { return fmt.Errorf("%w with name %q from file system structure: %w", ErrGetOrCreateFolder, folderName, err) @@ -342,38 +346,42 @@ func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, servic return 0, "", ErrFolderNameMissing } - // TODO use folder service instead + user, err := identity.GetRequester(ctx) + if err != nil { + return 0, "", err + } + metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Provisioning).Inc() - cmd := &dashboards.GetDashboardQuery{ - FolderID: util.Pointer(int64(0)), // nolint:staticcheck - OrgID: cfg.OrgID, + cmd := &folder.GetFolderQuery{ + OrgID: cfg.OrgID, + SignedInUser: user, } if cfg.FolderUID != "" { - cmd.UID = cfg.FolderUID + cmd.UID = &cfg.FolderUID } else { // provisioning depends on unique names //nolint:staticcheck cmd.Title = &folderName } - result, err := fr.dashboardStore.GetDashboard(ctx, cmd) - - if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { + result, err := fr.folderService.Get(ctx, cmd) + if err != nil && !errors.Is(err, dashboards.ErrFolderNotFound) { return 0, "", err } - // dashboard folder not found. create one. - if errors.Is(err, dashboards.ErrDashboardNotFound) { - // set dashboard folderUid if given - if cfg.FolderUID == accesscontrol.GeneralFolderUID { - return 0, "", dashboards.ErrFolderInvalidUID - } + // do not allow the creation of folder with uid "general" + if result != nil && result.UID == accesscontrol.GeneralFolderUID { + return 0, "", dashboards.ErrFolderInvalidUID + } + // dashboard folder not found. create one. + if errors.Is(err, dashboards.ErrFolderNotFound) { createCmd := &folder.CreateFolderCommand{ - OrgID: cfg.OrgID, - UID: cfg.FolderUID, - Title: folderName, + OrgID: cfg.OrgID, + UID: cfg.FolderUID, + Title: folderName, + SignedInUser: user, } f, err := service.SaveFolderForProvisionedDashboards(ctx, createCmd) @@ -385,10 +393,7 @@ func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, servic return f.ID, f.UID, nil } - if !result.IsFolder { - return 0, "", fmt.Errorf("got invalid response. expected folder, found dashboard") - } - + //nolint:staticcheck return result.ID, result.UID, nil } diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index ac2741c86cc..52e9ac19e42 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -12,9 +12,20 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" + "github.com/grafana/grafana/pkg/services/tag/tagimpl" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) @@ -28,6 +39,10 @@ const ( configName = "default" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestCreatingNewDashboardFileReader(t *testing.T) { setup := func() *config { return &config{ @@ -107,6 +122,17 @@ func TestDashboardFileReader(t *testing.T) { } } + sql, cfgT := db.InitTestDBWithCfg(t) + features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders) + fStore := folderimpl.ProvideStore(sql) + tagService := tagimpl.ProvideService(sql) + dashStore, err := database.ProvideDashboardStore(sql, cfgT, features, tagService) + require.NoError(t, err) + folderStore := folderimpl.ProvideDashboardFolderStore(sql) + folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{}, bus.ProvideBus(tracing.InitializeTracerForTest()), + dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(), + supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest()) + t.Run("Reading dashboards from disk", func(t *testing.T) { t.Run("Can read default dashboard", func(t *testing.T) { setup() @@ -116,8 +142,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once() fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{ID: 1}, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 2}, nil).Times(2) - - reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -137,7 +162,7 @@ func TestDashboardFileReader(t *testing.T) { inserted++ }) - reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -174,7 +199,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -202,7 +227,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -237,7 +262,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -265,7 +290,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -280,7 +305,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -297,7 +322,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(2) fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(3) - reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) reader.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -314,7 +339,7 @@ func TestDashboardFileReader(t *testing.T) { Folder: "", } - _, err := NewDashboardFileReader(cfg, logger, nil, nil, nil) + _, err := NewDashboardFileReader(cfg, logger, nil, nil, folderSvc) require.NotNil(t, err) }) @@ -322,7 +347,7 @@ func TestDashboardFileReader(t *testing.T) { setup() cfg.Options["path"] = brokenDashboards - _, err := NewDashboardFileReader(cfg, logger, nil, nil, nil) + _, err := NewDashboardFileReader(cfg, logger, nil, nil, folderSvc) require.NoError(t, err) }) @@ -335,14 +360,14 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(2) fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(2) - reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, nil) + reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, folderSvc) reader1.dashboardProvisioningService = fakeService require.NoError(t, err) err = reader1.walkDisk(context.Background()) require.NoError(t, err) - reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, nil) + reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, folderSvc) reader2.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -362,7 +387,7 @@ func TestDashboardFileReader(t *testing.T) { "folder": defaultDashboards, }, } - r, err := NewDashboardFileReader(cfg, logger, nil, nil, nil) + r, err := NewDashboardFileReader(cfg, logger, nil, nil, folderSvc) require.NoError(t, err) _, _, err = r.getOrCreateFolder(context.Background(), cfg, fakeService, cfg.Folder) @@ -382,10 +407,12 @@ func TestDashboardFileReader(t *testing.T) { } fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{ID: 1}, nil).Once() - r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) require.NoError(t, err) - _, _, err = r.getOrCreateFolder(context.Background(), cfg, fakeService, cfg.Folder) + ctx := context.Background() + ctx, _ = identity.WithServiceIdentitiy(ctx, 1) + _, _, err = r.getOrCreateFolder(ctx, cfg, fakeService, cfg.Folder) require.NoError(t, err) }) @@ -402,10 +429,12 @@ func TestDashboardFileReader(t *testing.T) { }, } - r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) require.NoError(t, err) - _, _, err = r.getOrCreateFolder(context.Background(), cfg, fakeService, cfg.Folder) + ctx := context.Background() + ctx, _ = identity.WithServiceIdentitiy(ctx, 1) + _, _, err = r.getOrCreateFolder(ctx, cfg, fakeService, cfg.Folder) require.ErrorIs(t, err, dashboards.ErrFolderInvalidUID) }) @@ -457,7 +486,7 @@ func TestDashboardFileReader(t *testing.T) { cfg.DisableDeletion = true - reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) require.NoError(t, err) reader.dashboardProvisioningService = fakeService @@ -472,7 +501,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() fakeService.On("DeleteProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) reader.dashboardProvisioningService = fakeService require.NoError(t, err) diff --git a/pkg/services/provisioning/dashboards/validator_test.go b/pkg/services/provisioning/dashboards/validator_test.go index de8d438d02f..788eb7e017a 100644 --- a/pkg/services/provisioning/dashboards/validator_test.go +++ b/pkg/services/provisioning/dashboards/validator_test.go @@ -8,9 +8,19 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" + "github.com/grafana/grafana/pkg/services/tag/tagimpl" ) const ( @@ -31,16 +41,30 @@ func TestDuplicatesValidator(t *testing.T) { } logger := log.New("test.logger") + sql, cfgT := db.InitTestDBWithCfg(t) + features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders) + fStore := folderimpl.ProvideStore(sql) + tagService := tagimpl.ProvideService(sql) + dashStore, err := database.ProvideDashboardStore(sql, cfgT, features, tagService) + require.NoError(t, err) + folderStore := folderimpl.ProvideDashboardFolderStore(sql) + folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{}, bus.ProvideBus(tracing.InitializeTracerForTest()), + dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(), + supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest()) + t.Run("Duplicates validator should collect info about duplicate UIDs and titles within folders", func(t *testing.T) { const folderName = "duplicates-validator-folder" + ctx := context.Background() + ctx, _ = identity.WithServiceIdentitiy(ctx, 1) + fakeStore := &fakeDashboardStore{} - r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) require.NoError(t, err) fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(6) fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return([]*dashboards.DashboardProvisioning{}, nil).Times(4) fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5) - _, folderUID, err := r.getOrCreateFolder(context.Background(), cfg, fakeService, folderName) + _, folderUID, err := r.getOrCreateFolder(ctx, cfg, fakeService, folderName) require.NoError(t, err) identity := dashboardIdentity{folderUID: folderUID, title: "Grafana"} @@ -54,11 +78,11 @@ func TestDuplicatesValidator(t *testing.T) { Options: map[string]any{"path": dashboardContainingUID}, } - reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, nil) + reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, folderSvc) reader1.dashboardProvisioningService = fakeService require.NoError(t, err) - reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, nil) + reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, folderSvc) reader2.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -90,10 +114,13 @@ func TestDuplicatesValidator(t *testing.T) { t.Run("Duplicates validator should not collect info about duplicate UIDs and titles within folders for different orgs", func(t *testing.T) { const folderName = "duplicates-validator-folder" + ctx := context.Background() + ctx, _ = identity.WithServiceIdentitiy(ctx, 1) + fakeStore := &fakeDashboardStore{} - r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) require.NoError(t, err) - _, folderUID, err := r.getOrCreateFolder(context.Background(), cfg, fakeService, folderName) + _, folderUID, err := r.getOrCreateFolder(ctx, cfg, fakeService, folderName) require.NoError(t, err) identity := dashboardIdentity{folderUID: folderUID, title: "Grafana"} @@ -107,11 +134,11 @@ func TestDuplicatesValidator(t *testing.T) { Options: map[string]any{"path": dashboardContainingUID}, } - reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, nil) + reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, folderSvc) reader1.dashboardProvisioningService = fakeService require.NoError(t, err) - reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, nil) + reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, folderSvc) reader2.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -168,15 +195,15 @@ func TestDuplicatesValidator(t *testing.T) { Name: "third", Type: "file", OrgID: 2, Folder: "duplicates-validator-folder", Options: map[string]any{"path": twoDashboardsWithUID}, } - reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, nil) + reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, folderSvc) reader1.dashboardProvisioningService = fakeService require.NoError(t, err) - reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, nil) + reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, folderSvc) reader2.dashboardProvisioningService = fakeService require.NoError(t, err) - reader3, err := NewDashboardFileReader(cfg3, logger, nil, fakeStore, nil) + reader3, err := NewDashboardFileReader(cfg3, logger, nil, fakeStore, folderSvc) reader3.dashboardProvisioningService = fakeService require.NoError(t, err) @@ -193,9 +220,12 @@ func TestDuplicatesValidator(t *testing.T) { duplicates := duplicateValidator.getDuplicates() - r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) + ctx := context.Background() + ctx, _ = identity.WithServiceIdentitiy(ctx, 1) + + r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc) require.NoError(t, err) - _, folderUID, err := r.getOrCreateFolder(context.Background(), cfg, fakeService, cfg1.Folder) + _, folderUID, err := r.getOrCreateFolder(ctx, cfg, fakeService, cfg1.Folder) require.NoError(t, err) identity := dashboardIdentity{folderUID: folderUID, title: "Grafana"} @@ -210,9 +240,9 @@ func TestDuplicatesValidator(t *testing.T) { sort.Strings(titleUsageReaders) require.Equal(t, []string{"first"}, titleUsageReaders) - r, err = NewDashboardFileReader(cfg3, logger, nil, fakeStore, nil) + r, err = NewDashboardFileReader(cfg3, logger, nil, fakeStore, folderSvc) require.NoError(t, err) - _, folderUID, err = r.getOrCreateFolder(context.Background(), cfg3, fakeService, cfg3.Folder) + _, folderUID, err = r.getOrCreateFolder(ctx, cfg3, fakeService, cfg3.Folder) require.NoError(t, err) identity = dashboardIdentity{folderUID: folderUID, title: "Grafana"} diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 451b3a098e3..fcc8a50c2f4 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -164,6 +164,7 @@ type ProvisioningServiceImpl struct { folderService folder.Service resourcePermissions accesscontrol.ReceiverPermissionsService tracer tracing.Tracer + onceInitProvisioners sync.Once } func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error { @@ -189,7 +190,19 @@ func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) erro } func (ps *ProvisioningServiceImpl) Run(ctx context.Context) error { - err := ps.ProvisionDashboards(ctx) + var err error + + // run Init Provisioners only once + ps.onceInitProvisioners.Do(func() { + err = ps.RunInitProvisioners(ctx) + }) + + if err != nil { + // error already logged + return err + } + + err = ps.ProvisionDashboards(ctx) if err != nil { ps.log.Error("Failed to provision dashboard", "error", err) // Consider the allow list of errors for which running the provisioning service should not diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index de84250512a..beee1881a70 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -13,7 +13,11 @@ import ( dashboardstore "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" + prov_alerting "github.com/grafana/grafana/pkg/services/provisioning/alerting" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" + "github.com/grafana/grafana/pkg/services/provisioning/datasources" "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/services/searchV2" ) @@ -159,10 +163,17 @@ func setup(t *testing.T) *serviceTestStruct { serviceTest.dashboardProvisionerInstantiations++ return serviceTest.mock, nil }, - nil, - nil, + func(context.Context, string, datasources.BaseDataSourceService, datasources.CorrelationsStore, org.Service) error { + return nil + }, + func(context.Context, string, pluginstore.Store, pluginsettings.Service, org.Service) error { + return nil + }, searchStub, ) + service.provisionAlerting = func(context.Context, prov_alerting.ProvisionerConfig) error { + return nil + } serviceTest.service = service require.NoError(t, err) diff --git a/pkg/storage/unified/resource/search_client.go b/pkg/storage/unified/resource/search_client.go index 2ecbd5ee2bb..a9c9c95ef9d 100644 --- a/pkg/storage/unified/resource/search_client.go +++ b/pkg/storage/unified/resource/search_client.go @@ -4,17 +4,21 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +// Search Fallback was returning both Folders and Dashboards which resulted +// in issues with rendering the Folder UI. Also, filters are not implemented +// yet. For those reasons, we will be disabling Search Fallback for now func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient ResourceIndexClient, legacyClient ResourceIndexClient) ResourceIndexClient { - /*config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey] - if !ok { - return legacyClient - } + // config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey] + // if !ok { + // return legacyClient + // } + + // switch config.DualWriterMode { + // case rest.Mode0, rest.Mode1, rest.Mode2: + // return legacyClient + // default: + // return unifiedClient + // } - switch config.DualWriterMode { - case rest.Mode0, rest.Mode1, rest.Mode2: - return legacyClient - default: - return unifiedClient - }*/ return unifiedClient } From b06f83670e71605119b50f27ba05356ae7bb756b Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 21:00:49 -0700 Subject: [PATCH 178/894] Cleanup: comment out unreachable code (#99723) --- pkg/registry/apis/dashboard/legacy/sql_dashboards.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 0175e52b8bc..6c1553f9bad 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -102,10 +102,10 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, sql *legacysql.LegacyD return nil, fmt.Errorf("execute template %q: %w", tmpl.Name(), err) } q := rawQuery - if false { - pretty := sqltemplate.RemoveEmptyLines(rawQuery) - fmt.Printf("DASHBOARD QUERY: %s [%+v] // %+v\n", pretty, req.GetArgs(), query) - } + // if false { + // pretty := sqltemplate.RemoveEmptyLines(rawQuery) + // fmt.Printf("DASHBOARD QUERY: %s [%+v] // %+v\n", pretty, req.GetArgs(), query) + // } rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) if err != nil { From c7f83b7311958fae10e4750138e3cba9be319638 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 28 Jan 2025 22:17:17 -0700 Subject: [PATCH 179/894] K8s: Fix internal id setting in mode4 (#99720) --- pkg/storage/unified/apistore/prepare.go | 7 +- pkg/storage/unified/apistore/prepare_test.go | 140 +++++++++++++++++++ 2 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 pkg/storage/unified/apistore/prepare_test.go diff --git a/pkg/storage/unified/apistore/prepare.go b/pkg/storage/unified/apistore/prepare.go index 3e8275de1bf..b371444c66a 100644 --- a/pkg/storage/unified/apistore/prepare.go +++ b/pkg/storage/unified/apistore/prepare.go @@ -47,7 +47,7 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime return nil, err } if obj.GetName() == "" { - return nil, storage.ErrResourceVersionSetOnCreate + return nil, storage.NewInvalidObjError("", "missing name") } if obj.GetResourceVersion() != "" { return nil, storage.ErrResourceVersionSetOnCreate @@ -60,8 +60,11 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime // nolint:staticcheck id := obj.GetDeprecatedInternalID() if id < 1 { + // the ID must be smaller than 9007199254740991, otherwise we will lose prescision + // on the frontend, which uses the number type to store ids. The largest safe number in + // javascript is 9007199254740991, compared to 9223372036854775807 as the max int64 // nolint:staticcheck - obj.SetDeprecatedInternalID(s.snowflake.Generate().Int64()) + obj.SetDeprecatedInternalID(s.snowflake.Generate().Int64() & ((1 << 52) - 1)) } } diff --git a/pkg/storage/unified/apistore/prepare_test.go b/pkg/storage/unified/apistore/prepare_test.go new file mode 100644 index 00000000000..330c2791cfa --- /dev/null +++ b/pkg/storage/unified/apistore/prepare_test.go @@ -0,0 +1,140 @@ +package apistore + +import ( + "context" + "testing" + "time" + + "github.com/bwmarrin/snowflake" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" + "k8s.io/apimachinery/pkg/api/apitesting" + "k8s.io/apiserver/pkg/storage" +) + +func TestPrepareObjectForStorage(t *testing.T) { + _ = v0alpha1.AddToScheme(scheme) + node, err := snowflake.NewNode(rand.Int63n(1024)) + require.NoError(t, err) + s := &Storage{ + codec: apitesting.TestCodec(codecs, v0alpha1.DashboardResourceInfo.GroupVersion()), + snowflake: node, + opts: StorageOptions{ + LargeObjectSupport: nil, + }, + } + ctx := identity.WithRequester(context.Background(), &user.SignedInUser{UserID: 1, UserUID: "user-uid"}) + + t.Run("Error getting requester from context", func(t *testing.T) { + _, err := s.prepareObjectForStorage(context.Background(), nil) + require.Error(t, err) + require.Contains(t, err.Error(), "a Requester was not found in the context") + }) + + t.Run("Error on missing name", func(t *testing.T) { + dashboard := v0alpha1.Dashboard{} + _, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject()) + require.Error(t, err) + require.Contains(t, err.Error(), "missing name") + }) + + t.Run("Error on non-empty resource version", func(t *testing.T) { + dashboard := v0alpha1.Dashboard{} + dashboard.Name = "test-name" + dashboard.ResourceVersion = "123" + _, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject()) + require.Error(t, err) + require.Equal(t, storage.ErrResourceVersionSetOnCreate, err) + }) + + t.Run("Generate UID and leave deprecated ID empty, if not required", func(t *testing.T) { + dashboard := v0alpha1.Dashboard{} + dashboard.Name = "test-name" + + encodedData, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject()) + require.NoError(t, err) + + newObject, _, err := s.codec.Decode(encodedData, nil, &v0alpha1.Dashboard{}) + require.NoError(t, err) + obj, err := utils.MetaAccessor(newObject) + require.NoError(t, err) + require.NotEmpty(t, obj.GetUID(), "") + require.Empty(t, obj.GetDeprecatedInternalID()) // nolint:staticcheck + require.Empty(t, obj.GetGenerateName()) + require.Empty(t, obj.GetResourceVersion()) + require.Empty(t, obj.GetSelfLink()) + require.Empty(t, obj.GetUpdatedBy()) + require.Equal(t, obj.GetCreatedBy(), "user:user-uid") + updatedTS, err := obj.GetUpdatedTimestamp() + require.NoError(t, err) + require.Empty(t, updatedTS) + }) + + t.Run("Should keep repo info", func(t *testing.T) { + dashboard := v0alpha1.Dashboard{} + dashboard.Name = "test-name" + obj := dashboard.DeepCopyObject() + meta, err := utils.MetaAccessor(obj) + require.NoError(t, err) + now := time.Now() + meta.SetRepositoryInfo(&utils.ResourceRepositoryInfo{ + Name: "test-repo", + Path: "test/path", + Hash: "hash", + Timestamp: &now, + }) + + encodedData, err := s.prepareObjectForStorage(ctx, obj) + require.NoError(t, err) + + newObject, _, err := s.codec.Decode(encodedData, nil, &v0alpha1.Dashboard{}) + require.NoError(t, err) + meta, err = utils.MetaAccessor(newObject) + require.NoError(t, err) + require.Equal(t, meta.GetRepositoryHash(), "hash") + require.Equal(t, meta.GetRepositoryName(), "test-repo") + require.Equal(t, meta.GetRepositoryPath(), "test/path") + ts, err := meta.GetRepositoryTimestamp() + require.NoError(t, err) + parsed, err := time.Parse(time.RFC3339, now.UTC().Format(time.RFC3339)) + require.NoError(t, err) + require.Equal(t, ts, &parsed) + }) + + s.opts.RequireDeprecatedInternalID = true + t.Run("Should generate internal id", func(t *testing.T) { + dashboard := v0alpha1.Dashboard{} + dashboard.Name = "test-name" + + encodedData, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject()) + require.NoError(t, err) + newObject, _, err := s.codec.Decode(encodedData, nil, &v0alpha1.Dashboard{}) + require.NoError(t, err) + obj, err := utils.MetaAccessor(newObject) + require.NoError(t, err) + require.NotEmpty(t, obj.GetDeprecatedInternalID()) // nolint:staticcheck + // must be less than the max number value in javascript to avoid precision loss + require.LessOrEqual(t, obj.GetDeprecatedInternalID(), int64(9007199254740991)) // nolint:staticcheck + }) + + t.Run("Should use deprecated ID if given it", func(t *testing.T) { + dashboard := v0alpha1.Dashboard{} + dashboard.Name = "test-name" + obj := dashboard.DeepCopyObject() + meta, err := utils.MetaAccessor(obj) + require.NoError(t, err) + meta.SetDeprecatedInternalID(1) // nolint:staticcheck + + encodedData, err := s.prepareObjectForStorage(ctx, obj) + require.NoError(t, err) + newObject, _, err := s.codec.Decode(encodedData, nil, &v0alpha1.Dashboard{}) + require.NoError(t, err) + meta, err = utils.MetaAccessor(newObject) + require.NoError(t, err) + require.Equal(t, meta.GetDeprecatedInternalID(), int64(1)) // nolint:staticcheck + }) +} From 8415059290348d4e123379b7107106275e0453ea Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 29 Jan 2025 10:26:17 +0300 Subject: [PATCH 180/894] K8s/OpenAPI: Move openapi snapshots out of the root (#99728) --- .github/CODEOWNERS | 4 - .../tests/apis/openapi_snapshots}/README.md | 0 .../dashboard.grafana.app-v0alpha1.json | 0 .../folder.grafana.app-v0alpha1.json | 0 .../peakq.grafana.app-v0alpha1.json | 0 pkg/tests/apis/{core => }/openapi_test.go | 101 +++++++++--------- 6 files changed, 53 insertions(+), 52 deletions(-) rename {openapi => pkg/tests/apis/openapi_snapshots}/README.md (100%) rename {openapi => pkg/tests/apis/openapi_snapshots}/dashboard.grafana.app-v0alpha1.json (100%) rename {openapi => pkg/tests/apis/openapi_snapshots}/folder.grafana.app-v0alpha1.json (100%) rename {openapi => pkg/tests/apis/openapi_snapshots}/peakq.grafana.app-v0alpha1.json (100%) rename pkg/tests/apis/{core => }/openapi_test.go (54%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e43d98d7724..0ca4d539fdf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -731,10 +731,6 @@ embed.go @grafana/grafana-as-code /public/app/plugins/*gen.go @grafana/grafana-as-code /cue.mod/ @grafana/grafana-as-code -# Rendered OpenAPI from app platform -# Eventually each file owned by the right team, OR a structure with the rendered value under /apis/{group}/openapi -/openapi/ @grafana/grafana-app-platform-squad - # GitHub Workflows and Templates /.github/CODEOWNERS @tolzhabayev /.github/ISSUE_TEMPLATE/ @torkelo @sympatheticmoose diff --git a/openapi/README.md b/pkg/tests/apis/openapi_snapshots/README.md similarity index 100% rename from openapi/README.md rename to pkg/tests/apis/openapi_snapshots/README.md diff --git a/openapi/dashboard.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json similarity index 100% rename from openapi/dashboard.grafana.app-v0alpha1.json rename to pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json diff --git a/openapi/folder.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/folder.grafana.app-v0alpha1.json similarity index 100% rename from openapi/folder.grafana.app-v0alpha1.json rename to pkg/tests/apis/openapi_snapshots/folder.grafana.app-v0alpha1.json diff --git a/openapi/peakq.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/peakq.grafana.app-v0alpha1.json similarity index 100% rename from openapi/peakq.grafana.app-v0alpha1.json rename to pkg/tests/apis/openapi_snapshots/peakq.grafana.app-v0alpha1.json diff --git a/pkg/tests/apis/core/openapi_test.go b/pkg/tests/apis/openapi_test.go similarity index 54% rename from pkg/tests/apis/core/openapi_test.go rename to pkg/tests/apis/openapi_test.go index c861f54f4fb..ec9cda563ca 100644 --- a/pkg/tests/apis/core/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -1,4 +1,4 @@ -package core +package apis import ( "bytes" @@ -18,7 +18,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -32,18 +31,7 @@ func TestIntegrationOpenAPIs(t *testing.T) { t.Skip("skipping integration test") } - check := []schema.GroupVersion{{ - Group: "dashboard.grafana.app", - Version: "v0alpha1", - }, { - Group: "folder.grafana.app", - Version: "v0alpha1", - }, { - Group: "peakq.grafana.app", - Version: "v0alpha1", - }} - - h := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + h := NewK8sTestHelper(t, testinfra.GrafanaOpts{ AppModeProduction: true, EnableFeatureToggles: []string{ featuremgmt.FlagKubernetesFoldersServiceV2, // Will be default on by G12 @@ -73,47 +61,64 @@ func TestIntegrationOpenAPIs(t *testing.T) { require.Equal(t, info.Minor, fmt.Sprintf("%d", v.Minor())) }) - t.Run("build open", func(t *testing.T) { - // Now write each OpenAPI spec to a static file - dir := filepath.Join("..", "..", "..", "..", "openapi") - for _, gv := range check { - path := fmt.Sprintf("/openapi/v3/apis/%s/%s", gv.Group, gv.Version) - rsp := apis.DoRequest(h, apis.RequestParams{ - Method: http.MethodGet, - Path: path, - User: h.Org1.Admin, - }, &apis.AnyResource{}) + dir := "openapi_snapshots" - require.NotNil(t, rsp.Response) - require.Equal(t, 200, rsp.Response.StatusCode, path) + for _, gv := range []schema.GroupVersion{{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + }, { + Group: "folder.grafana.app", + Version: "v0alpha1", + }, { + Group: "peakq.grafana.app", + Version: "v0alpha1", + }} { + VerifyOpenAPISnapshots(t, dir, gv, h) + } +} - var prettyJSON bytes.Buffer - err := json.Indent(&prettyJSON, rsp.Body, "", " ") - require.NoError(t, err) - pretty := prettyJSON.String() +// This function should be moved to oss (it is now a duplicate) +func VerifyOpenAPISnapshots(t *testing.T, dir string, gv schema.GroupVersion, h *K8sTestHelper) { + if gv.Group == "" { + return // skip invalid groups + } + path := fmt.Sprintf("/openapi/v3/apis/%s/%s", gv.Group, gv.Version) + t.Run(path, func(t *testing.T) { + rsp := DoRequest(h, RequestParams{ + Method: http.MethodGet, + Path: path, + User: h.Org1.Admin, + }, &AnyResource{}) - write := false - fpath := filepath.Join(dir, fmt.Sprintf("%s-%s.json", gv.Group, gv.Version)) + require.NotNil(t, rsp.Response) + require.Equal(t, 200, rsp.Response.StatusCode, path) - // nolint:gosec - // We can ignore the gosec G304 warning since this is a test and the function is only called with explicit paths - body, err := os.ReadFile(fpath) - if err == nil { - if !assert.JSONEq(t, string(body), pretty) { - t.Logf("openapi spec has changed: %s", path) - t.Fail() - write = true - } - } else { - t.Errorf("missing openapi spec for: %s", path) + var prettyJSON bytes.Buffer + err := json.Indent(&prettyJSON, rsp.Body, "", " ") + require.NoError(t, err) + pretty := prettyJSON.String() + + write := false + fpath := filepath.Join(dir, fmt.Sprintf("%s-%s.json", gv.Group, gv.Version)) + + // nolint:gosec + // We can ignore the gosec G304 warning since this is a test and the function is only called with explicit paths + body, err := os.ReadFile(fpath) + if err == nil { + if !assert.JSONEq(t, string(body), pretty) { + t.Logf("openapi spec has changed: %s", path) + t.Fail() write = true } + } else { + t.Errorf("missing openapi spec for: %s", path) + write = true + } - if write { - e2 := os.WriteFile(fpath, []byte(pretty), 0644) - if e2 != nil { - t.Errorf("error writing file: %s", e2.Error()) - } + if write { + e2 := os.WriteFile(fpath, []byte(pretty), 0644) + if e2 != nil { + t.Errorf("error writing file: %s", e2.Error()) } } }) From 8e5a4560e8bd13729c171e83b544da9606456bb4 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Wed, 29 Jan 2025 08:36:53 +0000 Subject: [PATCH 181/894] Add workflow that comments when PRs include the `add to what's new` label (#99637) Co-authored-by: Mitch Seaman --- .github/CODEOWNERS | 1 + .github/workflows/add-to-whats-new.yml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 .github/workflows/add-to-whats-new.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0ca4d539fdf..499078f1e7f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -743,6 +743,7 @@ embed.go @grafana/grafana-as-code /.github/pr-checks.json @tolzhabayev /.github/pr-commands.json @tolzhabayev /.github/renovate.json5 @grafana/frontend-ops +/.github/workflows/add-to-whats-new.yml @grafana/docs-tooling /.github/workflows/auto-triager/ @grafana/plugins-platform-frontend /.github/workflows/alerting-swagger-gen.yml @grafana/alerting-backend /.github/workflows/auto-milestone.yml @grafana/grafana-developer-enablement-squad diff --git a/.github/workflows/add-to-whats-new.yml b/.github/workflows/add-to-whats-new.yml new file mode 100644 index 00000000000..900ab9615bd --- /dev/null +++ b/.github/workflows/add-to-whats-new.yml @@ -0,0 +1,16 @@ +name: Add comment about adding a What's new note +on: + pull_request: + types: [labeled] + +jobs: + add-comment: + if: ${{ ! github.event.pull_request.head.repo.fork && contains(github.event.pull_request.labels.*.name, 'add to what''s new') }} + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 + with: + message: | + Since you've added the `Add to what's new` label, consider drafting a [What's new note](https://admin.grafana.com/content-admin/#/collections/whats-new/new) for this feature. From 30c8ac7108fc7224a9a6b76ac65c03e117332823 Mon Sep 17 00:00:00 2001 From: Alexa V <239999+axelavargas@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:13:56 +0100 Subject: [PATCH 182/894] Dashboard: Schema V2 - Remove Dashboard id from spec (#99590) * Dashboard: Schema V2 - Remove Dashboard id from spec * Fix issue with dashboard id used in examples and serializers for schema v2 * Fix snapshot test --- .../schema/dashboard/v2alpha0/dashboard.schema.cue | 8 ++------ .../src/schema/dashboard/v2alpha0/examples.ts | 1 - .../src/schema/dashboard/v2alpha0/types.gen.ts | 3 --- public/app/features/apiserver/types.ts | 2 -- .../dashboard-scene/saving/SaveDashboardAsForm.tsx | 2 +- .../serialization/DashboardSceneSerializer.test.ts | 2 -- .../serialization/DashboardSceneSerializer.ts | 1 - .../transformSceneToSaveModelSchemaV2.test.ts.snap | 1 - .../transformSaveModelSchemaV2ToScene.ts | 8 +++++--- .../transformSceneToSaveModelSchemaV2.ts | 1 - .../dashboard/api/ResponseTransformers.test.ts | 7 +++++-- .../features/dashboard/api/ResponseTransformers.ts | 12 +++++++++--- 12 files changed, 22 insertions(+), 26 deletions(-) diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 4b52f6757b4..5f164f1aded 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -5,10 +5,6 @@ import ( ) DashboardV2Spec: { - // Unique numeric identifier for the dashboard. - // `id` is internal to a specific Grafana instance. `uid` should be used to identify a dashboard across Grafana instances. - id?: int64 - // Title of dashboard. title: string @@ -43,7 +39,7 @@ DashboardV2Spec: { // Configured template variables. variables: [...VariableKind] - elements: [ElementReference.name]: Element + elements: [ElementReference.name]: Element annotations: [...AnnotationQueryKind] @@ -68,7 +64,7 @@ LibraryPanelSpec: { id: number // Title for the library panel in the dashboard title: string - + libraryPanel: LibraryPanelRef } diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts index b96fbd6f362..ed513da75c4 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts @@ -1,7 +1,6 @@ import { DashboardV2Spec } from './types.gen'; export const handyTestingSchema: DashboardV2Spec = { - id: 1, title: 'Default Dashboard', description: 'This is a default dashboard', cursorSync: 'Off', diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index bd6dba3279d..7b7ef1cda07 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -4,9 +4,6 @@ import * as common from '@grafana/schema'; export interface DashboardV2Spec { - // Unique numeric identifier for the dashboard. - // `id` is internal to a specific Grafana instance. `uid` should be used to identify a dashboard across Grafana instances. - id?: number; // Title of dashboard. title: string; // Description of dashboard. diff --git a/public/app/features/apiserver/types.ts b/public/app/features/apiserver/types.ts index 95953d209bc..9290ac779c2 100644 --- a/public/app/features/apiserver/types.ts +++ b/public/app/features/apiserver/types.ts @@ -42,7 +42,6 @@ export const AnnoKeyFolderId = 'grafana.app/folderId'; export const AnnoKeyFolderUrl = 'grafana.app/folderUrl'; export const AnnoKeyMessage = 'grafana.app/message'; export const AnnoKeySlug = 'grafana.app/slug'; -export const AnnoKeyDashboardId = 'grafana.app/dashboardId'; // Identify where values came from export const AnnoKeyRepoName = 'grafana.app/repoName'; @@ -66,7 +65,6 @@ type GrafanaAnnotations = { [AnnoKeyUpdatedBy]?: string; [AnnoKeyFolder]?: string; [AnnoKeySlug]?: string; - [AnnoKeyDashboardId]?: number; [AnnoKeyRepoName]?: string; [AnnoKeyRepoPath]?: string; diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx index 39dc0bca90a..e21fbeb4e8f 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx @@ -138,7 +138,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { // Old folder picker fields value={formValues.folder?.uid} initialTitle={defaultValues!.folder!.title} - dashboardId={changedSaveModel.id ?? undefined} + dashboardId={dashboard.state.id ?? undefined} enableCreateNew /> diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index cc9bbea4cc6..0844b4c454e 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -627,7 +627,6 @@ describe('DashboardSceneSerializer', () => { expect(saveAsModel).toMatchObject({ title: baseOptions.title, description: baseOptions.description, - id: undefined, editable: true, annotations: [], cursorSync: 'Off', @@ -801,7 +800,6 @@ describe('DashboardSceneSerializer', () => { expect(serializer.initialSaveModel).toEqual({ ...saveModel, - id: response.id, }); }); diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts index b185047241c..f1fae45ce02 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts @@ -172,7 +172,6 @@ export class V2DashboardSerializer onSaveComplete(saveModel: DashboardV2Spec, result: SaveDashboardResponseDTO): void { this.initialSaveModel = { ...saveModel, - id: result.id, }; } diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap index dcd651aa5f7..3ba7caea2c7 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap @@ -89,7 +89,6 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model }, }, }, - "id": 1, "layout": { "kind": "GridLayout", "spec": { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index a78771340ff..579f74b44d3 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -59,6 +59,7 @@ import { AnnoKeyUpdatedBy, AnnoKeyUpdatedTimestamp, AnnoKeyDashboardIsSnapshot, + DeprecatedInternalId, } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; @@ -125,6 +126,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo = { //dashboard settings - id: oldDash.id ? oldDash.id : undefined, title: oldDash.title, description: oldDash.description ?? '', cursorSync: getCursorSync(oldDash), diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index 1209f884b47..95034c757ac 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -4,11 +4,11 @@ import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2 import { AnnoKeyCreatedBy, AnnoKeyDashboardGnetId, - AnnoKeyDashboardId, AnnoKeyFolder, AnnoKeySlug, AnnoKeyUpdatedBy, AnnoKeyUpdatedTimestamp, + DeprecatedInternalId, } from 'app/features/apiserver/types'; import { getDefaultDataSourceRef } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2'; import { @@ -353,6 +353,9 @@ describe('ResponseTransformers', () => { [AnnoKeyFolder]: 'folder1', [AnnoKeySlug]: 'dashboard-slug', }, + labels: { + [DeprecatedInternalId]: 123, + }, }, }; @@ -366,8 +369,8 @@ describe('ResponseTransformers', () => { expect(transformed.metadata.annotations?.[AnnoKeyUpdatedTimestamp]).toEqual('2023-01-02T00:00:00Z'); expect(transformed.metadata.annotations?.[AnnoKeyFolder]).toEqual('folder1'); expect(transformed.metadata.annotations?.[AnnoKeySlug]).toEqual('dashboard-slug'); - expect(transformed.metadata.annotations?.[AnnoKeyDashboardId]).toBe(123); expect(transformed.metadata.annotations?.[AnnoKeyDashboardGnetId]).toBe('something-like-a-uid'); + expect(transformed.metadata.labels?.[DeprecatedInternalId]).toBe(123); // Spec const spec = transformed.spec; diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 695157f5c6c..fa352b029c7 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -39,13 +39,13 @@ import { DashboardLink, DataTransformerConfig } from '@grafana/schema/src/raw/da import { AnnoKeyCreatedBy, AnnoKeyDashboardGnetId, - AnnoKeyDashboardId, AnnoKeyDashboardIsSnapshot, AnnoKeyDashboardSnapshotOriginalUrl, AnnoKeyFolder, AnnoKeySlug, AnnoKeyUpdatedBy, AnnoKeyUpdatedTimestamp, + DeprecatedInternalId, } from 'app/features/apiserver/types'; import { TypedVariableModelV2 } from 'app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene'; import { getDefaultDataSourceRef } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2'; @@ -91,6 +91,7 @@ export function ensureV2Response( let accessMeta: DashboardWithAccessInfo['access']; let annotationsMeta: DashboardWithAccessInfo['metadata']['annotations']; + let labelsMeta: DashboardWithAccessInfo['metadata']['labels']; let creationTimestamp; if (isDashboardResource(dto)) { @@ -101,11 +102,13 @@ export function ensureV2Response( [AnnoKeyUpdatedTimestamp]: dto.metadata.annotations?.[AnnoKeyUpdatedTimestamp], [AnnoKeyFolder]: dto.metadata.annotations?.[AnnoKeyFolder], [AnnoKeySlug]: dto.metadata.annotations?.[AnnoKeySlug], - [AnnoKeyDashboardId]: dashboard.id ?? undefined, [AnnoKeyDashboardGnetId]: dashboard.gnetId ?? undefined, [AnnoKeyDashboardIsSnapshot]: dto.metadata.annotations?.[AnnoKeyDashboardIsSnapshot], }; creationTimestamp = dto.metadata.creationTimestamp; + labelsMeta = { + [DeprecatedInternalId]: dto.metadata.labels?.[DeprecatedInternalId], + }; } else { accessMeta = { url: dto.meta.url, @@ -124,11 +127,13 @@ export function ensureV2Response( [AnnoKeyUpdatedTimestamp]: dto.meta.updated, [AnnoKeyFolder]: dto.meta.folderUid, [AnnoKeySlug]: dto.meta.slug, - [AnnoKeyDashboardId]: dashboard.id ?? undefined, [AnnoKeyDashboardGnetId]: dashboard.gnetId ?? undefined, [AnnoKeyDashboardIsSnapshot]: dto.meta.isSnapshot, }; creationTimestamp = dto.meta.created; + labelsMeta = { + [DeprecatedInternalId]: dashboard.id ?? undefined, + }; } if (annotationsMeta?.[AnnoKeyDashboardIsSnapshot]) { @@ -171,6 +176,7 @@ export function ensureV2Response( name: dashboard.uid, resourceVersion: dashboard.version?.toString() || '0', annotations: annotationsMeta, + labels: labelsMeta, }, spec, access: accessMeta, From 978101b7a54f690ff3a596f134bf3dc5aa3a596b Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 29 Jan 2025 09:24:41 +0000 Subject: [PATCH 183/894] ImportDashboards: Use NestedFolderPicker (#99696) --- .../manage-dashboards/components/ImportDashboardForm.tsx | 8 +++----- .../components/ImportDashboardOverview.tsx | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx index 35a241dad27..b4f8d041b82 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx @@ -4,7 +4,7 @@ import { Controller, FieldErrors, UseFormReturn } from 'react-hook-form'; import { selectors } from '@grafana/e2e-selectors'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { Button, Field, FormFieldErrors, FormsOnSubmit, Stack, Input, Legend } from '@grafana/ui'; -import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; +import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { @@ -21,7 +21,6 @@ import { ImportDashboardLibraryPanelsList } from './ImportDashboardLibraryPanels interface Props extends Pick, 'register' | 'control' | 'getValues' | 'watch'> { uidReset: boolean; inputs: DashboardInputs; - initialFolderUid: string; errors: FieldErrors; onCancel: () => void; onUidReset: () => void; @@ -35,7 +34,6 @@ export const ImportDashboardForm = ({ getValues, uidReset, inputs, - initialFolderUid, onUidReset, onCancel, onSubmit, @@ -72,8 +70,8 @@ export const ImportDashboardForm = ({ ( - + render={({ field: { ref, value, onChange, ...field } }) => ( + onChange({ uid, title })} value={value.uid} /> )} name="folder" control={control} diff --git a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx index 96caec35f96..f00e1c4ce55 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx @@ -112,7 +112,6 @@ class ImportDashboardOverviewUnConnected extends PureComponent { onUidReset={this.onUidReset} onSubmit={this.onSubmit} watch={watch} - initialFolderUid={folder.uid} /> )} From a0f27caff2fbf22f6af7b2f4fceb368a741d466a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:46:59 +0000 Subject: [PATCH 184/894] Update dependency eslint to v9.19.0 (#99702) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-eslint-rules/package.json | 2 +- packages/grafana-plugin-configs/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 26 ++++++++++---------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 9b22a995e00..f560411556b 100644 --- a/package.json +++ b/package.json @@ -174,7 +174,7 @@ "esbuild": "0.24.2", "esbuild-loader": "4.2.2", "esbuild-plugin-browserslist": "^0.15.0", - "eslint": "9.18.0", + "eslint": "9.19.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "28.11.0", diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index 7a124984fe3..7e0c87a6117 100644 --- a/packages/grafana-eslint-rules/package.json +++ b/packages/grafana-eslint-rules/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@typescript-eslint/types": "^8.9.0", - "eslint": "9.18.0", + "eslint": "9.19.0", "tslib": "2.8.1" }, "private": true diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index 9da9da184e7..e7d6fc1bb66 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -12,7 +12,7 @@ "@types/eslint": "9.6.1", "@types/webpack-bundle-analyzer": "^4.7.0", "copy-webpack-plugin": "12.0.2", - "eslint": "9.18.0", + "eslint": "9.19.0", "eslint-webpack-plugin": "4.2.0", "fork-ts-checker-webpack-plugin": "9.0.2", "glob": "11.0.1", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 695108bb59f..aee6ccc8bbe 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -107,7 +107,7 @@ "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", "esbuild": "0.24.2", - "eslint": "9.18.0", + "eslint": "9.19.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "28.11.0", diff --git a/yarn.lock b/yarn.lock index f6881a6c123..232388dcf42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2458,10 +2458,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.18.0": - version: 9.18.0 - resolution: "@eslint/js@npm:9.18.0" - checksum: 10/364a7d030dad9dbda1458d8dbcea0199fe7d48bcfefe4b49389df6c45cdc5a2449f70e5d8a794e46ed9fb34af3fe5a3f53e30020d306b6ee791e2a1b2b9fa25f +"@eslint/js@npm:9.19.0": + version: 9.19.0 + resolution: "@eslint/js@npm:9.19.0" + checksum: 10/d8133a83330676d5f0827713af2e9bbf35530631a93520fb59ead6b827a325c54fdd7ad99f2158f895fb393c47bbc55dfdaa945998a647f3b9230f1d5324a626 languageName: node linkType: hard @@ -3307,7 +3307,7 @@ __metadata: dependencies: "@typescript-eslint/types": "npm:^8.9.0" "@typescript-eslint/utils": "npm:^8.9.0" - eslint: "npm:9.18.0" + eslint: "npm:9.19.0" tslib: "npm:2.8.1" languageName: unknown linkType: soft @@ -3534,7 +3534,7 @@ __metadata: "@types/eslint": "npm:9.6.1" "@types/webpack-bundle-analyzer": "npm:^4.7.0" copy-webpack-plugin: "npm:12.0.2" - eslint: "npm:9.18.0" + eslint: "npm:9.19.0" eslint-webpack-plugin: "npm:4.2.0" fork-ts-checker-webpack-plugin: "npm:9.0.2" glob: "npm:11.0.1" @@ -3645,7 +3645,7 @@ __metadata: date-fns: "npm:4.1.0" debounce-promise: "npm:3.1.2" esbuild: "npm:0.24.2" - eslint: "npm:9.18.0" + eslint: "npm:9.19.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" eslint-plugin-jest: "npm:28.11.0" @@ -16177,16 +16177,16 @@ __metadata: languageName: node linkType: hard -"eslint@npm:9.18.0": - version: 9.18.0 - resolution: "eslint@npm:9.18.0" +"eslint@npm:9.19.0": + version: 9.19.0 + resolution: "eslint@npm:9.19.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.12.1" "@eslint/config-array": "npm:^0.19.0" "@eslint/core": "npm:^0.10.0" "@eslint/eslintrc": "npm:^3.2.0" - "@eslint/js": "npm:9.18.0" + "@eslint/js": "npm:9.19.0" "@eslint/plugin-kit": "npm:^0.2.5" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" @@ -16222,7 +16222,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10/85f22991aab4b0809fdfc557ec2bd309062e7211b631674e71827a73c45e44febaa80dedda35150154e331a2d372c3a25e8e5dd4a99dc8a982fe8f7d645d859f + checksum: 10/850d19fd6a34702d1e3d9bdad6aef84a20a5c2de006a8fa6380843384b13944b180232ddd74b8725ffcdf8f296399037f0e8eb4783d5f7393f13c059112b843d languageName: node linkType: hard @@ -17938,7 +17938,7 @@ __metadata: esbuild: "npm:0.24.2" esbuild-loader: "npm:4.2.2" esbuild-plugin-browserslist: "npm:^0.15.0" - eslint: "npm:9.18.0" + eslint: "npm:9.19.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" eslint-plugin-jest: "npm:28.11.0" From e6c2db82e047fd948e470e636645e2ea3d8c3e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Wed, 29 Jan 2025 11:45:24 +0100 Subject: [PATCH 185/894] fix(search): use the right services when unfied search is enabled for folders (#99661) Co-authored-by: Scott Lepper Co-authored-by: joshhunt --- .../components/FolderFilter/FolderFilter.tsx | 37 ++++++++++++++++--- .../manage-dashboards/state/actions.ts | 2 +- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/public/app/core/components/FolderFilter/FolderFilter.tsx b/public/app/core/components/FolderFilter/FolderFilter.tsx index 12a4be1c3c3..5547a9bec91 100644 --- a/public/app/core/components/FolderFilter/FolderFilter.tsx +++ b/public/app/core/components/FolderFilter/FolderFilter.tsx @@ -4,9 +4,10 @@ import { useCallback, useMemo, useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { AsyncMultiSelect, Icon, Button, useStyles2 } from '@grafana/ui'; +import { config } from 'app/core/config'; import { Trans } from 'app/core/internationalization'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { DashboardSearchItemType } from 'app/features/search/types'; +import { getGrafanaSearcher } from 'app/features/search/service/searcher'; import { FolderInfo, PermissionLevelString } from 'app/types'; export interface FolderFilterProps { @@ -66,21 +67,47 @@ async function getFoldersAsOptions( ): Promise>> { setLoading(true); + // Use Unified Storage API behind toggle + if (config.featureToggles.unifiedStorageSearchUI) { + const searcher = getGrafanaSearcher(); + const queryResponse = await searcher.search({ + query: searchString, + kind: ['folder'], + limit: 100, + permission: PermissionLevelString.View, + }); + + const options = queryResponse.view.map((item) => ({ + label: item.name, + value: { uid: item.uid, title: item.name }, + })); + + if (!searchString || 'dashboards'.includes(searchString.toLowerCase())) { + options.unshift({ label: 'Dashboards', value: { uid: 'general', title: 'Dashboards' } }); + } + + setLoading(false); + return options; + } + + // Use existing backend service search const params = { query: searchString, - type: DashboardSearchItemType.DashFolder, + type: 'folder', permission: PermissionLevelString.View, }; - // FIXME: stop using id from search and use UID instead const searchHits = await getBackendSrv().search(params); - const options = searchHits.map((d) => ({ label: d.title, value: { uid: d.uid, title: d.title } })); + const options = searchHits.map((d) => ({ + label: d.title, + value: { uid: d.uid, title: d.title }, + })); + if (!searchString || 'dashboards'.includes(searchString.toLowerCase())) { options.unshift({ label: 'Dashboards', value: { uid: 'general', title: 'Dashboards' } }); } setLoading(false); - return options; } diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index 60fb84442bb..adab7a4746c 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -267,7 +267,7 @@ export function createFolder(payload: any) { export const SLICE_FOLDER_RESULTS_TO = 1000; -export function searchFolders( +export async function searchFolders( query: any, permission?: PermissionLevelString, type: SearchQueryType = SearchQueryType.Folder From 6ea87802edabdb6352df4b469d45bd2b8aa42f07 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:47:15 +0000 Subject: [PATCH 186/894] Update dependency @types/webpack-env to v1.18.8 (#99731) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 232388dcf42..b22a48d9ecc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10322,9 +10322,9 @@ __metadata: linkType: hard "@types/webpack-env@npm:^1.18.4": - version: 1.18.7 - resolution: "@types/webpack-env@npm:1.18.7" - checksum: 10/b07ca300b8e8af9ffad2bfdd6a662adffe49f86710a72cec01e4d10bb99444ed1ce45efbedda66ccd399b26ee22e7eadefd7c118a64047f5ea465b556ba86cf3 + version: 1.18.8 + resolution: "@types/webpack-env@npm:1.18.8" + checksum: 10/f3932f3d6c2530f644cfc898eda1ab8182d6ae57f555c2f0179d813549b639078671b71e4041831fc306c5ebe61f5cdac794fe4ceae281fce8bf67e23661a488 languageName: node linkType: hard From 336449c169d96e34854b035161e935a14dc38c37 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Wed, 29 Jan 2025 11:53:52 +0100 Subject: [PATCH 187/894] Explore: Add `hide_logs_download` and hide button to download logs (#99512) * Explore: Add `disableLogsDownload` and hide button to download logs * change copy * Explore: Change `disableLogsDownload` to `hide_logs_download` * change casing in frontend * also hide from inspector * add test * lint --- conf/defaults.ini | 3 +++ .../setup-grafana/configure-grafana/_index.md | 4 +++ packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + pkg/api/dtos/frontend_settings.go | 1 + pkg/api/frontendsettings.go | 1 + pkg/setting/setting.go | 2 ++ .../explore/Logs/LogsMetaRow.test.tsx | 9 ++++++- .../app/features/explore/Logs/LogsMetaRow.tsx | 14 ++++++----- .../inspector/InspectDataTab.test.tsx | 25 +++++++++++++++++++ .../app/features/inspector/InspectDataTab.tsx | 2 +- 11 files changed, 55 insertions(+), 8 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 55a89dfa205..38ec12ab7e3 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1574,6 +1574,9 @@ enabled = true # set the default offset for the time picker defaultTimeOffset = 1h +# hides the download logs button in Explore +hide_logs_download = false + #################################### Help ############################# [help] # Enable the Help section diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 15247a10908..5c15ff2c0b8 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1934,6 +1934,10 @@ Enable or disable the Explore section. Default is `enabled`. Set a default time offset from now on the time picker. Default is 1 hour. This setting should be expressed as a duration. Examples: 1h (hour), 1d (day), 1w (week), 1M (month). +#### `hide_logs_download` + +Show or hide the button to download logs in Explore. Default is `false`, so that the button will be visible. + ### `[help]` Configures the help section. diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 560f4ba672f..4e57dd61656 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -238,6 +238,7 @@ export interface GrafanaConfig { listScopesEndpoint?: string; reportingStaticContext?: Record; exploreDefaultTimeOffset?: string; + exploreHideLogsDownload?: boolean; // The namespace to use for kubernetes apiserver requests namespace: string; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 6b29cdc0dee..dd179ea36a6 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -203,6 +203,7 @@ export class GrafanaBootConfig implements GrafanaConfig { cloudMigrationPollIntervalMs = 2000; reportingStaticContext?: Record; exploreDefaultTimeOffset = '1h'; + exploreHideLogsDownload: boolean | undefined; /** * Language used in Grafana's UI. This is after the user's preference (or deteceted locale) is resolved to one of diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 6827525be04..30a80ea8b71 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -212,6 +212,7 @@ type FrontendSettingsDTO struct { CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled"` EnableFrontendSandboxForPlugins []string `json:"enableFrontendSandboxForPlugins"` ExploreDefaultTimeOffset string `json:"exploreDefaultTimeOffset"` + ExploreHideLogsDownload bool `json:"ExploreHideLogsDownload"` Auth FrontendSettingsAuthDTO `json:"auth"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 8ec544337b0..ba0f2f436bd 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -247,6 +247,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro LocalFileSystemAvailable: hs.Cfg.LocalFileSystemAvailable, ReportingStaticContext: hs.Cfg.ReportingStaticContext, ExploreDefaultTimeOffset: hs.Cfg.ExploreDefaultTimeOffset, + ExploreHideLogsDownload: hs.Cfg.ExploreHideLogsDownload, DefaultDatasourceManageAlertsUIToggle: hs.Cfg.DefaultDatasourceManageAlertsUIToggle, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 6bb86697f10..d70029b4b20 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -511,6 +511,7 @@ type Cfg struct { // Explore UI ExploreEnabled bool ExploreDefaultTimeOffset string + ExploreHideLogsDownload bool // Help UI HelpEnabled bool @@ -1214,6 +1215,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { } else { cfg.ExploreDefaultTimeOffset = exploreDefaultTimeOffset } + cfg.ExploreHideLogsDownload = explore.Key("hide_logs_download").MustBool(false) help := iniFile.Section("help") cfg.HelpEnabled = help.Key("enabled").MustBool(true) diff --git a/public/app/features/explore/Logs/LogsMetaRow.test.tsx b/public/app/features/explore/Logs/LogsMetaRow.test.tsx index 4774402dbc0..ce63d19b4d0 100644 --- a/public/app/features/explore/Logs/LogsMetaRow.test.tsx +++ b/public/app/features/explore/Logs/LogsMetaRow.test.tsx @@ -5,6 +5,7 @@ import { ComponentProps } from 'react'; import { FieldType, LogLevel, LogsDedupStrategy, standardTransformersRegistry, toDataFrame } from '@grafana/data'; import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize'; +import { config } from '@grafana/runtime'; import { MAX_CHARACTERS } from '../../logs/components/LogRowMessage'; import { logRowsToReadableJson } from '../../logs/utils'; @@ -32,11 +33,12 @@ const defaultProps: LogsMetaRowProps = { clearDetectedFields: jest.fn(), }; -const setup = (propOverrides?: object) => { +const setup = (propOverrides?: object, disableDownload = false) => { const props = { ...defaultProps, ...propOverrides, }; + config.exploreHideLogsDownload = disableDownload; return render(); }; @@ -121,6 +123,11 @@ describe('LogsMetaRow', () => { expect(screen.getByText('Download').closest('button')).toBeInTheDocument(); }); + it('does not render a button to show the download menu if disabled', async () => { + setup({}, true); + expect(screen.queryByText('Download')).toBeNull(); + }); + it('renders a button to show the download menu', async () => { setup(); diff --git a/public/app/features/explore/Logs/LogsMetaRow.tsx b/public/app/features/explore/Logs/LogsMetaRow.tsx index abf22494946..9db3be0ebdd 100644 --- a/public/app/features/explore/Logs/LogsMetaRow.tsx +++ b/public/app/features/explore/Logs/LogsMetaRow.tsx @@ -16,7 +16,7 @@ import { Labels, } from '@grafana/data'; import { DataFrame } from '@grafana/data/'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { Button, Dropdown, Menu, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui'; import { downloadDataFrameAsCsv, downloadLogsModelAsTxt } from '../../inspector/utils/download'; @@ -182,11 +182,13 @@ export const LogsMetaRow = memo( }; })} /> - - - Download - - + {!config.exploreHideLogsDownload && ( + + + Download + + + )}
)} diff --git a/public/app/features/inspector/InspectDataTab.test.tsx b/public/app/features/inspector/InspectDataTab.test.tsx index a19b4667437..3e7340d6733 100644 --- a/public/app/features/inspector/InspectDataTab.test.tsx +++ b/public/app/features/inspector/InspectDataTab.test.tsx @@ -4,6 +4,7 @@ import { ComponentProps } from 'react'; import { Props } from 'react-virtualized-auto-sizer'; import { DataFrame, FieldType } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { InspectDataTab } from './InspectDataTab'; @@ -75,6 +76,8 @@ describe('InspectDataTab', () => { expect(screen.getByText(/Second data frame/i)).toBeInTheDocument(); }); it('should show download logs button if logs data', () => { + const oldConfig = config.exploreHideLogsDownload; + config.exploreHideLogsDownload = false; const dataWithLogs = [ { name: 'Data frame with logs', @@ -91,6 +94,28 @@ describe('InspectDataTab', () => { ] as unknown as DataFrame[]; render(); expect(screen.getByText(/Download logs/i)).toBeInTheDocument(); + config.exploreHideLogsDownload = oldConfig; + }); + it('should not show download logs button if logs data but config disabled', () => { + const oldConfig = config.exploreHideLogsDownload; + config.exploreHideLogsDownload = true; + const dataWithLogs = [ + { + name: 'Data frame with logs', + fields: [ + { name: 'time', type: FieldType.time, values: [100, 200, 300], config: {} }, + { name: 'name', type: FieldType.string, values: ['uniqueA', 'b', 'c'], config: {} }, + { name: 'value', type: FieldType.number, values: [1, 2, 3], config: {} }, + ], + length: 3, + meta: { + preferredVisualisationType: 'logs', + }, + }, + ] as unknown as DataFrame[]; + render(); + expect(screen.queryByText(/Download logs/i)).not.toBeInTheDocument(); + config.exploreHideLogsDownload = oldConfig; }); it('should not show download logs button if no logs data', () => { render(); diff --git a/public/app/features/inspector/InspectDataTab.tsx b/public/app/features/inspector/InspectDataTab.tsx index dcecf5386dc..49bae7ddbed 100644 --- a/public/app/features/inspector/InspectDataTab.tsx +++ b/public/app/features/inspector/InspectDataTab.tsx @@ -223,7 +223,7 @@ export class InspectDataTab extends PureComponent { - {hasLogs && ( + {hasLogs && !config.exploreHideLogsDownload && ( From 9b0078326af13baeb551ae152af01db43a435533 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:11:23 +0000 Subject: [PATCH 188/894] Update dependency ol-ext to v4.0.26 (#99734) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index f560411556b..745160d5801 100644 --- a/package.json +++ b/package.json @@ -357,7 +357,7 @@ "nanoid": "^5.0.4", "node-forge": "^1.3.1", "ol": "7.4.0", - "ol-ext": "4.0.25", + "ol-ext": "4.0.26", "pluralize": "^8.0.0", "prismjs": "1.29.0", "rc-slider": "11.1.8", diff --git a/yarn.lock b/yarn.lock index b22a48d9ecc..50f5b8613b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18012,7 +18012,7 @@ __metadata: node-notifier: "npm:10.0.1" nx: "npm:19.8.2" ol: "npm:7.4.0" - ol-ext: "npm:4.0.25" + ol-ext: "npm:4.0.26" pluralize: "npm:^8.0.0" postcss: "npm:8.5.1" postcss-loader: "npm:8.1.1" @@ -23386,12 +23386,12 @@ __metadata: languageName: node linkType: hard -"ol-ext@npm:4.0.25": - version: 4.0.25 - resolution: "ol-ext@npm:4.0.25" +"ol-ext@npm:4.0.26": + version: 4.0.26 + resolution: "ol-ext@npm:4.0.26" peerDependencies: ol: ">= 5.3.0" - checksum: 10/e3c8282fc67d9511b37c540f97217594a28f16b7ebeee5baf828eb56fdefa024fad6c1f846ac3cfd093f3b1e2cab5b6a56a97087152389fe4e35a2eb9b42f415 + checksum: 10/68ecf27895e8cb0d4b179151836e853a0d8ce224e27448db10d11844afbea0e85781e2c9d6a9c1e65063f5a81d54755f8002c7f9259506a5bddc020024d5b931 languageName: node linkType: hard From 1444051b65af0de6c412a12132083135c7730414 Mon Sep 17 00:00:00 2001 From: Fayzal Ghantiwala <114010985+fayzal-g@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:17:44 +0000 Subject: [PATCH 189/894] Alerting: Feature flag to fetch rules by passing down RBAC namespaces (#99738) New feature flag --- .../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 | 14 ++++++++++++++ 5 files changed, 28 insertions(+) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 0a6775edac1..6ab637d841e 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -253,4 +253,5 @@ export interface FeatureToggles { grafanaAdvisor?: boolean; elasticsearchImprovedParsing?: boolean; datasourceConnectionsTab?: boolean; + fetchRulesUsingPost?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a4baa2652bf..d37b417b34e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1757,6 +1757,14 @@ var ( RequiresDevMode: false, FrontendOnly: true, }, + { + Name: "fetchRulesUsingPost", + Description: "Use a POST request to list rules by passing down the namespaces user has access to", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromAdminPage: true, + HideFromDocs: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 34f40c2585d..6ae52ca70d5 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -234,3 +234,4 @@ queryLibraryDashboards,experimental,@grafana/grafana-frontend-platform,false,fal grafanaAdvisor,experimental,@grafana/plugins-platform-backend,false,false,false elasticsearchImprovedParsing,experimental,@grafana/aws-datasources,false,false,false datasourceConnectionsTab,experimental,@grafana/plugins-platform-backend,false,false,true +fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 06bc4b00564..0a6e5c4d0b4 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -946,4 +946,8 @@ const ( // FlagDatasourceConnectionsTab // Shows defined connections for a data source in the plugins detail page FlagDatasourceConnectionsTab = "datasourceConnectionsTab" + + // FlagFetchRulesUsingPost + // Use a POST request to list rules by passing down the namespaces user has access to + FlagFetchRulesUsingPost = "fetchRulesUsingPost" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 7ef236e5194..1e79164d138 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1659,6 +1659,20 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "fetchRulesUsingPost", + "resourceVersion": "1738148593383", + "creationTimestamp": "2025-01-29T11:03:13Z" + }, + "spec": { + "description": "Use a POST request to list rules by passing down the namespaces user has access to", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "flameGraphItemCollapsing", From ebe2f442bdc2c35b8183e2004338c210aa93abde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Jan 2025 13:32:50 +0100 Subject: [PATCH 190/894] Dashboard: Simplify handling of variables that update on time range change when used in repeats (#99432) * Dashboard: Simplify handling of variables that update on time range change when used in repeats * Update * Update * Update * Update * Update * Update --- .../dashboard-scene/scene/DashboardScene.tsx | 27 - .../scene/RowRepeaterBehavior.test.tsx | 20 - .../scene/RowRepeaterBehavior.ts | 27 +- .../layout-default/DashboardGridItem.test.tsx | 27 - .../layout-default/DashboardGridItem.tsx | 2 +- yarn.lock | 493 +++++++++++++----- 6 files changed, 379 insertions(+), 217 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 3bef71caf21..df925ab3272 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -13,7 +13,6 @@ import { import { config, locationService, RefreshEvent } from '@grafana/runtime'; import { sceneGraph, - SceneGridRow, SceneObject, SceneObjectBase, SceneObjectRef, @@ -72,7 +71,6 @@ import { DashboardControls } from './DashboardControls'; import { DashboardSceneRenderer } from './DashboardSceneRenderer'; import { DashboardSceneUrlSync } from './DashboardSceneUrlSync'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; -import { RowRepeaterBehavior } from './RowRepeaterBehavior'; import { ViewPanelScene } from './ViewPanelScene'; import { isUsingAngularDatasourcePlugin, isUsingAngularPanelPlugin } from './angular/AngularDeprecation'; import { setupKeyboardShortcuts } from './keyboardShortcuts'; @@ -765,31 +763,6 @@ export class DashboardVariableDependency implements SceneVariableDependencyConfi this._dashboard.setState({ panelsPerRow: Number.isInteger(perRow) ? perRow : undefined }); } } - - /** - * Propagate variable changes to repeat row behavior as it does not get it when it's nested under local value - * The first repeated row has the row repeater behavior but it also has a local SceneVariableSet with a local variable value - */ - const layout = this._dashboard.state.body; - if (!(layout instanceof DefaultGridLayoutManager)) { - return; - } - - for (const child of layout.state.grid.state.children) { - if (!(child instanceof SceneGridRow) || !child.state.$behaviors) { - continue; - } - - for (const behavior of child.state.$behaviors) { - if (behavior instanceof RowRepeaterBehavior) { - if (behavior.isWaitingForVariables || (behavior.state.variableName === variable.state.name && hasChanged)) { - behavior.performRepeat(true); - } else if (!behavior.isWaitingForVariables && behavior.state.variableName === variable.state.name) { - behavior.notifyRepeatedPanelsWaitingForVariables(variable); - } - } - } - } } } diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx index 021a1448827..116fc50781d 100644 --- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx @@ -151,26 +151,6 @@ describe('RowRepeaterBehavior', () => { expect(gridStateUpdates.length).toBe(1); }); - - it('Should update panels on refresh if variables load on time range change', async () => { - const { scene, repeatBehavior } = buildScene({ - variableQueryTime: 0, - variableRefresh: VariableRefresh.onTimeRangeChanged, - }); - - const notifyPanelsSpy = jest.spyOn(repeatBehavior, 'notifyRepeatedPanelsWaitingForVariables'); - - activateFullSceneTree(scene); - - expect(notifyPanelsSpy).toHaveBeenCalledTimes(0); - - scene.state.$timeRange?.onRefresh(); - - //make sure notifier is called - expect(notifyPanelsSpy).toHaveBeenCalledTimes(1); - - notifyPanelsSpy.mockRestore(); - }); }); describe('Given scene empty row', () => { diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts index 26ce6387627..990ada92748 100644 --- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts +++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts @@ -9,13 +9,12 @@ import { SceneGridRow, SceneObjectBase, SceneObjectState, - SceneVariable, SceneVariableSet, VariableDependencyConfig, VariableValueSingle, } from '@grafana/scenes'; -import { getMultiVariableValues, getQueryRunnerFor } from '../utils/utils'; +import { getMultiVariableValues } from '../utils/utils'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; import { DashboardRepeatsProcessedEvent } from './types'; @@ -31,10 +30,9 @@ interface RowRepeaterBehaviorState extends SceneObjectState { export class RowRepeaterBehavior extends SceneObjectBase { protected _variableDependency = new VariableDependencyConfig(this, { variableNames: [this.state.variableName], - onVariableUpdateCompleted: () => {}, + onVariableUpdateCompleted: () => this.performRepeat(), }); - public isWaitingForVariables = false; private _prevRepeatValues?: VariableValueSingle[]; private _clonedRows?: SceneGridRow[]; @@ -44,23 +42,6 @@ export class RowRepeaterBehavior extends SceneObjectBase this._activationHandler()); } - public notifyRepeatedPanelsWaitingForVariables(variable: SceneVariable) { - const allRows = [this._getRow(), ...(this._clonedRows ?? [])]; - - for (const row of allRows) { - for (const gridItem of row.state.children) { - if (!(gridItem instanceof DashboardGridItem)) { - continue; - } - - const queryRunner = getQueryRunnerFor(gridItem.state.body); - if (queryRunner) { - queryRunner.variableDependency?.variableUpdateCompleted(variable, false); - } - } - } - } - private _activationHandler() { this.performRepeat(); @@ -126,9 +107,7 @@ export class RowRepeaterBehavior extends SceneObjectBase { expect(repeater.state.repeatedPanels?.length).toBe(5); }); - it('Should update panels on refresh if variables load on time range change', async () => { - const { scene, repeater } = buildPanelRepeaterScene({ - variableQueryTime: 0, - variableRefresh: VariableRefresh.onTimeRangeChanged, - }); - - const notifyPanelsSpy = jest.spyOn(repeater, 'notifyRepeatedPanelsWaitingForVariables'); - - activateFullSceneTree(scene); - - expect(repeater.state.repeatedPanels?.length).toBe(5); - - expect(notifyPanelsSpy).toHaveBeenCalledTimes(0); - - scene.state.$timeRange?.onRefresh(); - - //make sure notifier is called - expect(notifyPanelsSpy).toHaveBeenCalledTimes(1); - - //make sure getQueryRunner is called for each repeated panel - expect(mockGetQueryRunnerFor).toHaveBeenCalledTimes(5); - - notifyPanelsSpy.mockRestore(); - mockGetQueryRunnerFor.mockClear(); - }); - it('Should display a panel when there are no options', async () => { const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 1, numberOfOptions: 0 }); diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx index b2ad2918ca9..b9ce621a2a2 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx @@ -109,7 +109,7 @@ export class DashboardGridItem if (isEqual(this._prevRepeatValues, values)) { // In some cases, like for variables that depend on time range, the panel query runners are waiting for the top level variable to complete // So even when there was no change in the variable value (like in this case) we need to notify the query runners that the variable has completed it's update - this.notifyRepeatedPanelsWaitingForVariables(variable); + // this.notifyRepeatedPanelsWaitingForVariables(variable); return; } diff --git a/yarn.lock b/yarn.lock index 50f5b8613b7..5692f04276e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -254,7 +254,14 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.7, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.7, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": + version: 7.25.9 + resolution: "@babel/helper-plugin-utils@npm:7.25.9" + checksum: 10/e347d87728b1ab10b6976d46403941c8f9008c045ea6d99997a7ffca7b852dc34b6171380f7b17edf94410e0857ff26f3a53d8618f11d73744db86e8ca9b8c64 + languageName: node + linkType: hard + +"@babel/helper-plugin-utils@npm:^7.26.5": version: 7.26.5 resolution: "@babel/helper-plugin-utils@npm:7.26.5" checksum: 10/1cc0fd8514da3bb249bed6c27227696ab5e84289749d7258098701cffc0c599b7f61ec40dd332f8613030564b79899d9826813c96f966330bcfc7145a8377857 @@ -6843,135 +6850,135 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.31.0" +"@rollup/rollup-android-arm-eabi@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.28.1" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-android-arm64@npm:4.31.0" +"@rollup/rollup-android-arm64@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-android-arm64@npm:4.28.1" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-darwin-arm64@npm:4.31.0" +"@rollup/rollup-darwin-arm64@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.28.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-darwin-x64@npm:4.31.0" +"@rollup/rollup-darwin-x64@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.28.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.31.0" +"@rollup/rollup-freebsd-arm64@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.28.1" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-freebsd-x64@npm:4.31.0" +"@rollup/rollup-freebsd-x64@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-freebsd-x64@npm:4.28.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.31.0" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.28.1" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.31.0" +"@rollup/rollup-linux-arm-musleabihf@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.28.1" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.31.0" +"@rollup/rollup-linux-arm64-gnu@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.28.1" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.31.0" +"@rollup/rollup-linux-arm64-musl@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.28.1" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.31.0" +"@rollup/rollup-linux-loongarch64-gnu@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.28.1" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.31.0" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.28.1" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.31.0" +"@rollup/rollup-linux-riscv64-gnu@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.28.1" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.31.0" +"@rollup/rollup-linux-s390x-gnu@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.28.1" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.31.0" +"@rollup/rollup-linux-x64-gnu@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.28.1" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.31.0" +"@rollup/rollup-linux-x64-musl@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.28.1" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.31.0" +"@rollup/rollup-win32-arm64-msvc@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.28.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.31.0" +"@rollup/rollup-win32-ia32-msvc@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.28.1" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.31.0": - version: 4.31.0 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.31.0" +"@rollup/rollup-win32-x64-msvc@npm:4.28.1": + version: 4.28.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.28.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -7977,7 +7984,7 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ast@npm:^1.0.0-beta.11, @swagger-api/apidom-ast@npm:^1.0.0-beta.5": +"@swagger-api/apidom-ast@npm:^1.0.0-beta.11": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.11" dependencies: @@ -7991,7 +7998,21 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-core@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-core@npm:^1.0.0-beta.11, @swagger-api/apidom-core@npm:^1.0.0-beta.5": +"@swagger-api/apidom-ast@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.5" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + unraw: "npm:^3.0.0" + checksum: 10/b2ea32b8ed589a3aff122e9209d5f0c873364bb34b234d13796422d4fce6b9f52fab599ef47956f655316256cf6af821c233117c0ca96a677b867e075b70cb5d + languageName: node + linkType: hard + +"@swagger-api/apidom-core@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-core@npm:^1.0.0-beta.11": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-core@npm:1.0.0-beta.11" dependencies: @@ -8008,7 +8029,24 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-error@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.11, @swagger-api/apidom-error@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.5": +"@swagger-api/apidom-core@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-core@npm:1.0.0-beta.5" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@types/ramda": "npm:~0.30.0" + minim: "npm:~0.23.8" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + short-unique-id: "npm:^5.0.2" + ts-mixer: "npm:^6.0.3" + checksum: 10/c034ef286738b2b5aab525b068fd22e1b54145e3024477abcafde926f3783c280c69e4de23cc28d9cc568a62fb02719a0a89e6fd2011136cf447f42ed66fca55 + languageName: node + linkType: hard + +"@swagger-api/apidom-error@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.11": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-error@npm:1.0.0-beta.11" dependencies: @@ -8017,7 +8055,16 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-json-pointer@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.11, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.3 <1.0.0-rc.0": +"@swagger-api/apidom-error@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-error@npm:1.0.0-beta.5" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + checksum: 10/defb3ba3775be8a511ff01be4ed7d2eca66faf5ab478f65a39845c8981510a0286e622268240de56613fd3ee37de906a7c6947a82aceb214fdd89d0988b972bb + languageName: node + linkType: hard + +"@swagger-api/apidom-json-pointer@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.11": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-json-pointer@npm:1.0.0-beta.11" dependencies: @@ -8031,6 +8078,20 @@ __metadata: languageName: node linkType: hard +"@swagger-api/apidom-json-pointer@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-json-pointer@npm:1.0.0-beta.5" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + checksum: 10/68b3b196de1d2cb86663350ba70f885599a12c21804cd9bc1be1777ee5e99e9f570623599d476d241bda298e0890c34d2c9a516f7a47cf060992107c31e9d9f5 + languageName: node + linkType: hard + "@swagger-api/apidom-ns-api-design-systems@npm:^1.0.0-beta.5": version: 1.0.0-beta.5 resolution: "@swagger-api/apidom-ns-api-design-systems@npm:1.0.0-beta.5" @@ -8094,7 +8155,7 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.5": +"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.11": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.0.0-beta.11" dependencies: @@ -8109,6 +8170,21 @@ __metadata: languageName: node linkType: hard +"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.0.0-beta.5" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/2518b3aa9b7387a90553565132101616dc24c6d1f8aee581f3562bac87403f87887217fdb3cf17b113f685e051980fc66ec25d9cab7e4a0bf4922b4bcd3bc502 + languageName: node + linkType: hard + "@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.0.0-beta.11": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.0.0-beta.11" @@ -8125,7 +8201,23 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.5": +"@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.0.0-beta.5" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.5" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/c439c3679dc6ea1807affa0a2913deea52ff8d63b48722c741458da147e849be9acc0c13955757a7b6904a4e7148cdb7845291da758a085083c61610a96fc36f + languageName: node + linkType: hard + +"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.11": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.0.0-beta.11" dependencies: @@ -8141,6 +8233,22 @@ __metadata: languageName: node linkType: hard +"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.0.0-beta.5" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-json-schema-draft-6": "npm:^1.0.0-beta.5" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/28c8f989e26a453f2ca6c655d6822d63f7bc57137375c460c707caa964a5097566969031f322d772b49de0ce2924aeb69e2c5a6ff66191b457844e68a40f013e + languageName: node + linkType: hard + "@swagger-api/apidom-ns-openapi-2@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-2@npm:^1.0.0-beta.5": version: 1.0.0-beta.5 resolution: "@swagger-api/apidom-ns-openapi-2@npm:1.0.0-beta.5" @@ -8157,7 +8265,7 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.5": +"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.11": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:1.0.0-beta.11" dependencies: @@ -8173,7 +8281,23 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-1@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.5": +"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:1.0.0-beta.5" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.5" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.3" + checksum: 10/5bb9c191a3d79d9a69aa04deb5dd6ce2eaa09700210f5a89fdeb0668ec76eb3f2d4a26950405f001459e59ad199ddd4632251992b9fc1b9fa03955e1ad600810 + languageName: node + linkType: hard + +"@swagger-api/apidom-ns-openapi-3-1@npm:>=1.0.0-beta.11 <1.0.0-rc.0": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.0.0-beta.11" dependencies: @@ -8191,6 +8315,23 @@ __metadata: languageName: node linkType: hard +"@swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.0.0-beta.5" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-json-pointer": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-beta.5" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.3" + checksum: 10/69018147465c78a25efc5e7bc4439561c2d620c65c9cf86bdc503dbad9b40e52db77bb0cb11800a4c34bdc88d462126e3dc355bad290cdfe7c9fbeda391405c8 + languageName: node + linkType: hard + "@swagger-api/apidom-ns-workflows-1@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-workflows-1@npm:^1.0.0-beta.5": version: 1.0.0-beta.5 resolution: "@swagger-api/apidom-ns-workflows-1@npm:1.0.0-beta.5" @@ -8520,6 +8661,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-arm64@npm:1.10.9": + version: 1.10.9 + resolution: "@swc/core-darwin-arm64@npm:1.10.9" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-darwin-x64@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-darwin-x64@npm:1.10.11" @@ -8527,6 +8675,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-x64@npm:1.10.9": + version: 1.10.9 + resolution: "@swc/core-darwin-x64@npm:1.10.9" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@swc/core-linux-arm-gnueabihf@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.10.11" @@ -8534,6 +8689,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm-gnueabihf@npm:1.10.9": + version: 1.10.9 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.10.9" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@swc/core-linux-arm64-gnu@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-linux-arm64-gnu@npm:1.10.11" @@ -8541,6 +8703,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-gnu@npm:1.10.9": + version: 1.10.9 + resolution: "@swc/core-linux-arm64-gnu@npm:1.10.9" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-arm64-musl@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-linux-arm64-musl@npm:1.10.11" @@ -8548,6 +8717,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-musl@npm:1.10.9": + version: 1.10.9 + resolution: "@swc/core-linux-arm64-musl@npm:1.10.9" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@swc/core-linux-x64-gnu@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-linux-x64-gnu@npm:1.10.11" @@ -8555,6 +8731,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-gnu@npm:1.10.9": + version: 1.10.9 + resolution: "@swc/core-linux-x64-gnu@npm:1.10.9" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-x64-musl@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-linux-x64-musl@npm:1.10.11" @@ -8562,6 +8745,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-musl@npm:1.10.9": + version: 1.10.9 + resolution: "@swc/core-linux-x64-musl@npm:1.10.9" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@swc/core-win32-arm64-msvc@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-win32-arm64-msvc@npm:1.10.11" @@ -8569,6 +8759,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-arm64-msvc@npm:1.10.9": + version: 1.10.9 + resolution: "@swc/core-win32-arm64-msvc@npm:1.10.9" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-win32-ia32-msvc@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-win32-ia32-msvc@npm:1.10.11" @@ -8576,6 +8773,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-ia32-msvc@npm:1.10.9": + version: 1.10.9 + resolution: "@swc/core-win32-ia32-msvc@npm:1.10.9" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@swc/core-win32-x64-msvc@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-win32-x64-msvc@npm:1.10.11" @@ -8583,7 +8787,14 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.10.11, @swc/core@npm:^1.7.3": +"@swc/core-win32-x64-msvc@npm:1.10.9": + version: 1.10.9 + resolution: "@swc/core-win32-x64-msvc@npm:1.10.9" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/core@npm:1.10.11": version: 1.10.11 resolution: "@swc/core@npm:1.10.11" dependencies: @@ -8629,6 +8840,52 @@ __metadata: languageName: node linkType: hard +"@swc/core@npm:^1.7.3": + version: 1.10.9 + resolution: "@swc/core@npm:1.10.9" + dependencies: + "@swc/core-darwin-arm64": "npm:1.10.9" + "@swc/core-darwin-x64": "npm:1.10.9" + "@swc/core-linux-arm-gnueabihf": "npm:1.10.9" + "@swc/core-linux-arm64-gnu": "npm:1.10.9" + "@swc/core-linux-arm64-musl": "npm:1.10.9" + "@swc/core-linux-x64-gnu": "npm:1.10.9" + "@swc/core-linux-x64-musl": "npm:1.10.9" + "@swc/core-win32-arm64-msvc": "npm:1.10.9" + "@swc/core-win32-ia32-msvc": "npm:1.10.9" + "@swc/core-win32-x64-msvc": "npm:1.10.9" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.17" + peerDependencies: + "@swc/helpers": "*" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/543e79c249f6052883d656035321d449cf6c0f2ea54f786d5e3b96394d4cf201b293d6c3f897cc604eb145b21cce82f904306931fe9efbc6a50c714a5d5d97f0 + languageName: node + linkType: hard + "@swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -8655,21 +8912,21 @@ __metadata: linkType: hard "@tanstack/react-virtual@npm:^3.5.1, @tanstack/react-virtual@npm:^3.9.0": - version: 3.11.3 - resolution: "@tanstack/react-virtual@npm:3.11.3" + version: 3.11.2 + resolution: "@tanstack/react-virtual@npm:3.11.2" dependencies: - "@tanstack/virtual-core": "npm:3.11.3" + "@tanstack/virtual-core": "npm:3.11.2" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/eb39f8a015f4dc98070f0c18bbb1f9c094b7182133554ef3ee31d2678cd3a66edd28ce854d533e830f88f1f0ad1d5b065de184438a08fe774a9acc1dc62da436 + checksum: 10/a1136da0ec4c2ecbd4f996d8b84f228f0b8d851b15806e01049a160ad1d9b2eef0e0a491035fe017c6f84a0e125334f69ea23b32c180df23614ea4a8eeb7490c languageName: node linkType: hard -"@tanstack/virtual-core@npm:3.11.3": - version: 3.11.3 - resolution: "@tanstack/virtual-core@npm:3.11.3" - checksum: 10/24a3369dd0290d4f19aa1af7d0a6fb1b843741d722c6a5cf786416657bbf978f4f82a0b257eaee867d0798d8334374f5e940868a7b71dc065939fb7eeee19ad1 +"@tanstack/virtual-core@npm:3.11.2": + version: 3.11.2 + resolution: "@tanstack/virtual-core@npm:3.11.2" + checksum: 10/8433044a5c801052ba2e4cdda098cdc8e32adfd3a76ba31af7064bbdda60062fe221a3558096987baa66cd94f528855e887c282cb0f9eb99d3751457c2a62872 languageName: node linkType: hard @@ -8934,9 +9191,9 @@ __metadata: linkType: hard "@types/babel__preset-env@npm:^7": - version: 7.10.0 - resolution: "@types/babel__preset-env@npm:7.10.0" - checksum: 10/7d4d12758d89708afe327079d7d7580e8af3292295f087b8a9a48e12ac1d90aadc18ac3bc00f9b0cbc8778f3ce9fe778801d4d49b7691a75e3f13a901b69fd07 + version: 7.9.7 + resolution: "@types/babel__preset-env@npm:7.9.7" + checksum: 10/624425a84d9149aec04795fed6b1ac2f27dfd5d7976fde479bb1a4d754de34c92cdc28a1a373a5826382a68127b536420a0e090aa5fae522cb62724b7a571cb5 languageName: node linkType: hard @@ -10322,9 +10579,9 @@ __metadata: linkType: hard "@types/webpack-env@npm:^1.18.4": - version: 1.18.8 - resolution: "@types/webpack-env@npm:1.18.8" - checksum: 10/f3932f3d6c2530f644cfc898eda1ab8182d6ae57f555c2f0179d813549b639078671b71e4041831fc306c5ebe61f5cdac794fe4ceae281fce8bf67e23661a488 + version: 1.18.5 + resolution: "@types/webpack-env@npm:1.18.5" + checksum: 10/3c8dd0b23d45e2d33abdfbae7f1d8f75ce23d54588b08943e833f4dba81eb683ac68672a75eccbdba8e008bc1647638803c1bcadc8cdfd1dd7142fa2c3f612de languageName: node linkType: hard @@ -18850,8 +19107,8 @@ __metadata: linkType: hard "i18next@npm:^23.5.1 || ^24.2.0, i18next@npm:^24.0.0": - version: 24.2.2 - resolution: "i18next@npm:24.2.2" + version: 24.2.1 + resolution: "i18next@npm:24.2.1" dependencies: "@babel/runtime": "npm:^7.23.2" peerDependencies: @@ -18859,7 +19116,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/f66ed9e56d9412e59502f5df39163631daf9f1264774732fb21edbd66a528ca7a6b67dc2e2aec95683c6c7956e42c651587a54bd8ee082bd12008880ce6cd326 + checksum: 10/74836c3ca3365155906f95162bf75461f8f82a86034b03fc8efc670e10f610299dc5b51923acc1ab9d52ec7e7e717e44fb95b91ff1560ff45c6bad0c383517af languageName: node linkType: hard @@ -20982,8 +21239,8 @@ __metadata: linkType: hard "knip@npm:^5.10.0": - version: 5.43.6 - resolution: "knip@npm:5.43.6" + version: 5.43.1 + resolution: "knip@npm:5.43.1" dependencies: "@nodelib/fs.walk": "npm:3.0.1" "@snyk/github-codeowners": "npm:1.1.0" @@ -21007,7 +21264,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10/d843ed0f5b56baf5c29257308b0cf1956348cfa9d2b9b627420db023a6ccdaf54450f047fe900b263dc10291f395bc3eceef221dc6050e7fd55fb1fbe4fce3a2 + checksum: 10/068e4145371cf3a4434d07a206eddf8f1d509541482d76252440484562f0b989c11c3efb9c4083d8b5854a90758d3bbcc4a228fe935f6e90ecc9ef2c9f9da8a7 languageName: node linkType: hard @@ -26005,8 +26262,8 @@ __metadata: linkType: hard "react-i18next@npm:^15.0.0": - version: 15.4.0 - resolution: "react-i18next@npm:15.4.0" + version: 15.2.0 + resolution: "react-i18next@npm:15.2.0" dependencies: "@babel/runtime": "npm:^7.25.0" html-parse-stringify: "npm:^3.0.1" @@ -26018,7 +26275,7 @@ __metadata: optional: true react-native: optional: true - checksum: 10/4b3666d819f01cf96a256af4419b26938d314e33c6388eafccc29f67ad02994e5d53e7bf82eac656cade7f7bcd04f4a237f0b293165d7eda91d62e3fde605a38 + checksum: 10/9b2937f7beab763c494d55a801f21bfdbfe98e9509994c350d24fa404ded573f41e8607eeba290c686d5877d34f0ddefe48e9d6876720d5ed0e1243bcdd5dda6 languageName: node linkType: hard @@ -27432,28 +27689,28 @@ __metadata: linkType: hard "rollup@npm:^4.22.4": - version: 4.31.0 - resolution: "rollup@npm:4.31.0" + version: 4.28.1 + resolution: "rollup@npm:4.28.1" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.31.0" - "@rollup/rollup-android-arm64": "npm:4.31.0" - "@rollup/rollup-darwin-arm64": "npm:4.31.0" - "@rollup/rollup-darwin-x64": "npm:4.31.0" - "@rollup/rollup-freebsd-arm64": "npm:4.31.0" - "@rollup/rollup-freebsd-x64": "npm:4.31.0" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.31.0" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.31.0" - "@rollup/rollup-linux-arm64-gnu": "npm:4.31.0" - "@rollup/rollup-linux-arm64-musl": "npm:4.31.0" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.31.0" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.31.0" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.31.0" - "@rollup/rollup-linux-s390x-gnu": "npm:4.31.0" - "@rollup/rollup-linux-x64-gnu": "npm:4.31.0" - "@rollup/rollup-linux-x64-musl": "npm:4.31.0" - "@rollup/rollup-win32-arm64-msvc": "npm:4.31.0" - "@rollup/rollup-win32-ia32-msvc": "npm:4.31.0" - "@rollup/rollup-win32-x64-msvc": "npm:4.31.0" + "@rollup/rollup-android-arm-eabi": "npm:4.28.1" + "@rollup/rollup-android-arm64": "npm:4.28.1" + "@rollup/rollup-darwin-arm64": "npm:4.28.1" + "@rollup/rollup-darwin-x64": "npm:4.28.1" + "@rollup/rollup-freebsd-arm64": "npm:4.28.1" + "@rollup/rollup-freebsd-x64": "npm:4.28.1" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.28.1" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.28.1" + "@rollup/rollup-linux-arm64-gnu": "npm:4.28.1" + "@rollup/rollup-linux-arm64-musl": "npm:4.28.1" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.28.1" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.28.1" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.28.1" + "@rollup/rollup-linux-s390x-gnu": "npm:4.28.1" + "@rollup/rollup-linux-x64-gnu": "npm:4.28.1" + "@rollup/rollup-linux-x64-musl": "npm:4.28.1" + "@rollup/rollup-win32-arm64-msvc": "npm:4.28.1" + "@rollup/rollup-win32-ia32-msvc": "npm:4.28.1" + "@rollup/rollup-win32-x64-msvc": "npm:4.28.1" "@types/estree": "npm:1.0.6" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -27499,7 +27756,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/4f5fac0a0df7878ca810512c283df0e81b21d42fed262943b412c488a30beceb0149a4be36dbf2750b6c5cbfa4d4cf5097a134266f1425a9e213c2a2a09853fc + checksum: 10/4337898d07e646835b52494b43b4ccd6929da87af2b0febc05ab217fd2425cfda05af5efaea6037c1641c90d803eb5b3e491eefdd47b28fda85af4f46a0dad34 languageName: node linkType: hard @@ -30305,9 +30562,9 @@ __metadata: linkType: hard "type-fest@npm:^4.18.2, type-fest@npm:^4.26.1": - version: 4.33.0 - resolution: "type-fest@npm:4.33.0" - checksum: 10/0d179e66fa765bd0a25a785b12dc797f90f2f92bdb8c9c8a789f3fd8e5a4492444e7ef83551b3b8463aeab24fd6195761e26b03174722de636b4b75aa5726fb7 + version: 4.30.2 + resolution: "type-fest@npm:4.30.2" + checksum: 10/c5168b159c366e4fd5b74c7f7b786bed9248c03f67e6e07d52dd5d51354447468fa7c92b9f2142c7fe9279814031f783959370242c3520de848931b65ddb48bb languageName: node linkType: hard @@ -31869,11 +32126,11 @@ __metadata: linkType: hard "yaml@npm:^2.0.0, yaml@npm:^2.3.4": - version: 2.7.0 - resolution: "yaml@npm:2.7.0" + version: 2.6.1 + resolution: "yaml@npm:2.6.1" bin: yaml: bin.mjs - checksum: 10/c8c314c62fbd49244a6a51b06482f6d495b37ab10fa685fcafa1bbaae7841b7233ee7d12cab087bcca5a0b28adc92868b6e437322276430c28d00f1c1732eeec + checksum: 10/cf412f03a33886db0a3aac70bb4165588f4c5b3c6f8fc91520b71491e5537800b6c2c73ed52015617f6e191eb4644c73c92973960a1999779c62a200ee4c231d languageName: node linkType: hard From eb52af2b1430e9522dfd9ebca44c0bb92f5d8be9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 13:08:19 +0000 Subject: [PATCH 191/894] Update dependency @grafana/plugin-e2e to v1.17.0 (#99736) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 745160d5801..0ebd64c648a 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@emotion/eslint-plugin": "11.12.0", "@grafana/eslint-config": "8.0.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", - "@grafana/plugin-e2e": "1.16.3", + "@grafana/plugin-e2e": "1.17.0", "@grafana/tsconfig": "^2.0.0", "@manypkg/get-packages": "^2.2.0", "@playwright/test": "1.50.0", diff --git a/yarn.lock b/yarn.lock index 5692f04276e..f3824f36967 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3554,17 +3554,17 @@ __metadata: languageName: unknown linkType: soft -"@grafana/plugin-e2e@npm:1.16.3": - version: 1.16.3 - resolution: "@grafana/plugin-e2e@npm:1.16.3" +"@grafana/plugin-e2e@npm:1.17.0": + version: 1.17.0 + resolution: "@grafana/plugin-e2e@npm:1.17.0" dependencies: - "@grafana/e2e-selectors": "npm:^11.5.0-219238" + "@grafana/e2e-selectors": "npm:^11.5.0-220285" semver: "npm:^7.5.4" uuid: "npm:^11.0.2" yaml: "npm:^2.3.4" peerDependencies: "@playwright/test": ^1.41.2 - checksum: 10/bc3a109788af576301918a9dc611b34f6136dad26a4dd2a9adbe26f3d47f7503394eed2e1255e677dba46e8ea4108bb0efa952779c484bfad7d8158227ed1cdb + checksum: 10/d24465857228cb9588777f92bd7bee8b5b7042a9375cb20a31b04e9af75b307248d3f17bd421eff0640ff9592612ef66e16f1e461af57ca7c2d680052fc02348 languageName: node linkType: hard @@ -18046,7 +18046,7 @@ __metadata: "@grafana/lezer-logql": "npm:0.2.7" "@grafana/monaco-logql": "npm:^0.0.8" "@grafana/o11y-ds-frontend": "workspace:*" - "@grafana/plugin-e2e": "npm:1.16.3" + "@grafana/plugin-e2e": "npm:1.17.0" "@grafana/plugin-ui": "npm:0.9.6" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" From 514da10f4665c6fb53556b2886914dde54a7f2e4 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Wed, 29 Jan 2025 14:22:23 +0100 Subject: [PATCH 192/894] Revert "Bug: Fix broken ui components when angular is disabled" (#99730) Revert "Bug: Fix broken ui components when angular is disabled (#78208)" This reverts commit 1112e9006b7bfadf87380384b2e27be7ff24f7af. --- public/app/angular/AngularApp.ts | 1 + public/app/app.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/angular/AngularApp.ts b/public/app/angular/AngularApp.ts index d0f2bcff4d8..438cbd1a637 100644 --- a/public/app/angular/AngularApp.ts +++ b/public/app/angular/AngularApp.ts @@ -2,6 +2,7 @@ import 'angular'; import 'angular-route'; import 'angular-sanitize'; import 'angular-bindonce'; +import 'vendor/bootstrap/bootstrap'; import angular from 'angular'; // eslint-disable-line no-duplicate-imports import { extend } from 'lodash'; diff --git a/public/app/app.ts b/public/app/app.ts index 7cd4d4fbd69..d49f27a8732 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -4,7 +4,6 @@ import 'regenerator-runtime/runtime'; import 'whatwg-fetch'; // fetch polyfill needed for PhantomJs rendering import 'file-saver'; import 'jquery'; -import 'vendor/bootstrap/bootstrap'; import _ from 'lodash'; // eslint-disable-line lodash/import-scope import { createElement } from 'react'; From 1087ed623fdcd23dec941fbf646e4a2d1cd21d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Wed, 29 Jan 2025 14:50:49 +0100 Subject: [PATCH 193/894] feat(unified-storage): fetch full path if needed (#99747) --- .../folder/folderimpl/unifiedstore.go | 18 +++++ .../folder/folderimpl/unifiedstore_test.go | 77 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 pkg/services/folder/folderimpl/unifiedstore_test.go diff --git a/pkg/services/folder/folderimpl/unifiedstore.go b/pkg/services/folder/folderimpl/unifiedstore.go index 18c40751185..481ab6c704d 100644 --- a/pkg/services/folder/folderimpl/unifiedstore.go +++ b/pkg/services/folder/folderimpl/unifiedstore.go @@ -352,6 +352,14 @@ func (ss *FolderUnifiedStoreImpl) GetFolders(ctx context.Context, q folder.GetFo if f == nil { return nil, fmt.Errorf("unable to convert unstructured item to legacy folder %w", err) } + if q.WithFullpath || q.WithFullpathUIDs { + parents, err := ss.GetParents(ctx, folder.GetParentsQuery{UID: f.UID, OrgID: q.OrgID}) + if err != nil { + return nil, fmt.Errorf("failed to get parents for folder %s: %w", f.UID, err) + } + // If we don't have a parent, we just return the current folder as the full path + f.Fullpath, f.FullpathUIDs = computeFullPath(append(parents, f)) + } m[f.UID] = f } @@ -529,3 +537,13 @@ func (ss *FolderUnifiedStoreImpl) getK8sContext(ctx context.Context) (context.Co return newCtx, nil, nil } + +func computeFullPath(parents []*folder.Folder) (string, string) { + fullpath := make([]string, len(parents)) + fullpathUIDs := make([]string, len(parents)) + for i, p := range parents { + fullpath[i] = p.Title + fullpathUIDs[i] = p.UID + } + return strings.Join(fullpath, "/"), strings.Join(fullpathUIDs, "/") +} diff --git a/pkg/services/folder/folderimpl/unifiedstore_test.go b/pkg/services/folder/folderimpl/unifiedstore_test.go new file mode 100644 index 00000000000..d25cffa6e27 --- /dev/null +++ b/pkg/services/folder/folderimpl/unifiedstore_test.go @@ -0,0 +1,77 @@ +package folderimpl + +import ( + "testing" + + "github.com/grafana/grafana/pkg/services/folder" + "github.com/stretchr/testify/require" +) + +func TestComputeFullPath(t *testing.T) { + testCases := []struct { + name string + parents []*folder.Folder + wantPath string + wantPathUIDs string + }{ + { + name: "empty slice should return empty paths", + parents: []*folder.Folder{}, + wantPath: "", + wantPathUIDs: "", + }, + { + name: "single element should return single path", + parents: []*folder.Folder{ + { + Title: "Element", + UID: "Element-uid", + }, + }, + wantPath: "Element", + wantPathUIDs: "Element-uid", + }, + { + name: "multiple parents should return hierarchical path", + parents: []*folder.Folder{ + { + Title: "Grandparent", + UID: "grandparent-uid", + }, + { + Title: "Parent", + UID: "parent-uid", + }, + { + Title: "Element", + UID: "Element-uid", + }, + }, + wantPath: "Grandparent/Parent/Element", + wantPathUIDs: "grandparent-uid/parent-uid/Element-uid", + }, + { + name: "should handle special characters in titles", + parents: []*folder.Folder{ + { + Title: "Parent/With/Slashes", + UID: "parent-uid", + }, + { + Title: "Element With Spaces", + UID: "Element-uid", + }, + }, + wantPath: "Parent/With/Slashes/Element With Spaces", + wantPathUIDs: "parent-uid/Element-uid", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotPath, gotPathUIDs := computeFullPath(tc.parents) + require.Equal(t, tc.wantPath, gotPath) + require.Equal(t, tc.wantPathUIDs, gotPathUIDs) + }) + } +} From baaff6296fed089b758fbffdd1d7b8d154ad9958 Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Wed, 29 Jan 2025 14:59:37 +0100 Subject: [PATCH 194/894] Logging: Use slog adapter for go-kit/log with App SDK logger (#99740) * feat: use slog adapter for go-kit/log The adapter library is Apache-2.0, which is compatible with AGPL-3.0 as a dependency. The adapter library outputs a little more info than we'd like, but rather a couple fields too many than it outputting ERROR logs as INFO. * feat: update dependencies * chore: attribute ownership of dependency * refactor: move require * chore: make update-workspace --- go.mod | 1 + go.sum | 2 ++ pkg/infra/log/log.go | 12 ++++++++++-- pkg/storage/unified/apistore/go.mod | 1 + pkg/storage/unified/apistore/go.sum | 2 ++ pkg/storage/unified/resource/go.mod | 1 + pkg/storage/unified/resource/go.sum | 2 ++ 7 files changed, 19 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 9545c61c4c8..9ee6a8c744a 100644 --- a/go.mod +++ b/go.mod @@ -144,6 +144,7 @@ require ( github.com/spyzhov/ajson v0.9.0 // @grafana/grafana-app-platform-squad github.com/stretchr/testify v1.10.0 // @grafana/grafana-backend-group github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf // @grafana/grafana-backend-group + github.com/tjhop/slog-gokit v0.1.3 // @grafana/grafana-app-platform-squad github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f // @grafana/grafana-backend-group github.com/urfave/cli v1.22.16 // indirect; @grafana/grafana-backend-group github.com/urfave/cli/v2 v2.27.1 // @grafana/grafana-backend-group diff --git a/go.sum b/go.sum index 7f98a740960..b1f20abba02 100644 --- a/go.sum +++ b/go.sum @@ -2312,6 +2312,8 @@ github.com/thanos-io/objstore v0.0.0-20240818203309-0363dadfdfb1 h1:z0v9BB/p7s4J github.com/thanos-io/objstore v0.0.0-20240818203309-0363dadfdfb1/go.mod h1:3ukSkG4rIRUGkKM4oIz+BSuUx2e3RlQVVv3Cc3W+Tv4= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o= +github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= diff --git a/pkg/infra/log/log.go b/pkg/infra/log/log.go index 702bbc66f92..844b7d96c4d 100644 --- a/pkg/infra/log/log.go +++ b/pkg/infra/log/log.go @@ -20,6 +20,7 @@ import ( "github.com/go-kit/log/level" "github.com/go-stack/stack" "github.com/mattn/go-isatty" + sloggokit "github.com/tjhop/slog-gokit" "gopkg.in/ini.v1" "github.com/grafana/grafana-app-sdk/logging" @@ -53,6 +54,7 @@ func init() { } logger := level.NewFilter(format(os.Stderr), level.AllowInfo()) root = newManager(logger) + initAppSDKLogger(logger) RegisterContextualLogProvider(func(ctx context.Context) ([]any, bool) { pFromCtx := ctx.Value(logParamsContextKey{}) @@ -61,8 +63,6 @@ func init() { } return nil, false }) - - logging.DefaultLogger = logging.NewSLogLogger(slog.Default().Handler()) } // logManager manage loggers @@ -112,6 +112,8 @@ func (lm *logManager) initialize(loggers []logWithFilters) { lm.loggersByName[name].Swap(&compositeLogger{loggers: ctxLoggers}) } + + initAppSDKLogger(lm.ConcreteLogger) } func (lm *logManager) New(ctx ...any) *ConcreteLogger { @@ -548,3 +550,9 @@ func SetupConsoleLogger(level string) error { return nil } + +func initAppSDKLogger(gkl gokitlog.Logger) { + // We need to allow Debug logs here. go-kit/log does not support sharing the level we're using. + // TODO: Refactor such that we can pass in a level in a more appropriate manner. + logging.DefaultLogger = logging.NewSLogLogger(sloggokit.NewGoKitHandler(gkl, slog.LevelDebug)) +} diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index b95b2815e2b..0a0624c9222 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -296,6 +296,7 @@ require ( github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tjhop/slog-gokit v0.1.3 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 707f92e9578..f8a3eb6fd01 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -1005,6 +1005,8 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o= +github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index b70c1800e01..f7e3c9cf4b5 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -192,6 +192,7 @@ require ( github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/tjhop/slog-gokit v0.1.3 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/ugorji/go/codec v1.2.11 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 7601d0f78c4..c6184792eae 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -789,6 +789,8 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o= +github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= From 03f89a1925608cf46c201a446192be5f98d211c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Wed, 29 Jan 2025 15:02:53 +0100 Subject: [PATCH 195/894] MultiCombobox: Show `placeholder` when there is no options selected (#99743) --- .../src/components/Combobox/MultiCombobox.test.tsx | 11 +++++++++++ .../src/components/Combobox/MultiCombobox.tsx | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx index 66444644ca4..8ed509b26db 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx @@ -61,6 +61,17 @@ describe('MultiCombobox', () => { expect(screen.getByPlaceholderText('Select')).toBeInTheDocument(); }); + it('should not render with placeholder when options selected', async () => { + const options = [ + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + ]; + render(); + const input = screen.getByRole('combobox'); + expect(input).toHaveAttribute('placeholder', ''); + }); + it.each([ ['a', 'b', 'c'], [1, 2, 3], diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 104f638de60..1d5ab5bd6d8 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -276,7 +276,7 @@ export const MultiCombobox = (props: MultiComboboxPro getDropdownProps({ disabled, preventKeyAction: isOpen, - placeholder, + placeholder: visibleItems.length === 0 ? placeholder : '', ref: inputRef, style: { width: inputWidth }, }) From b4802e71167a339a4d97ff1025bf5139a69c7e8a Mon Sep 17 00:00:00 2001 From: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:11:01 +0000 Subject: [PATCH 196/894] CloudMigrations: Update Banner on Cloud Stack (#99741) * add message with flag * message only in cloud --- public/app/features/migrate-to-cloud/MigrateToCloud.tsx | 9 +++++++++ public/locales/en-US/grafana.json | 1 + public/locales/pseudo-LOCALE/grafana.json | 1 + 3 files changed, 11 insertions(+) diff --git a/public/app/features/migrate-to-cloud/MigrateToCloud.tsx b/public/app/features/migrate-to-cloud/MigrateToCloud.tsx index 325574de336..7f6920c0128 100644 --- a/public/app/features/migrate-to-cloud/MigrateToCloud.tsx +++ b/public/app/features/migrate-to-cloud/MigrateToCloud.tsx @@ -33,6 +33,15 @@ export default function MigrateToCloud() { {' '} to learn more about this feature! + {config.cloudMigrationIsTarget && ( + <> +   + + Your self-managed instance of Grafana requires version 11.5+, or 11.2+ with the onPremToCloudMigrations + feature flag enabled. + + + )} Visit our docs to learn more about this feature!", + "message-cloud": "Your self-managed instance of Grafana requires version 11.5+, or 11.2+ with the onPremToCloudMigrations feature flag enabled.", "message-plugins": "Only Community and Commercial signed plugins are eligible for migration. Their latest version will be installed in the cloud instance, please upgrade your plugins before starting the migration process.", "title": "Migrate to Grafana Cloud is in public preview", "title-plugins": "Migration of plugins" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index a485a54fac9..f04a543f927 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1977,6 +1977,7 @@ "public-preview": { "button-text": "Ğįvę ƒęęđþäčĸ", "message": "Ńő ŜĿÅş äřę äväįľäþľę yęŧ. <2>Vįşįŧ őūř đőčş ŧő ľęäřʼn mőřę äþőūŧ ŧĥįş ƒęäŧūřę!", + "message-cloud": "Ÿőūř şęľƒ-mäʼnäģęđ įʼnşŧäʼnčę őƒ Ğřäƒäʼnä řęqūįřęş vęřşįőʼn 11.5+, őř 11.2+ ŵįŧĥ ŧĥę őʼnPřęmŦőCľőūđMįģřäŧįőʼnş ƒęäŧūřę ƒľäģ ęʼnäþľęđ.", "message-plugins": "Øʼnľy Cőmmūʼnįŧy äʼnđ Cőmmęřčįäľ şįģʼnęđ pľūģįʼnş äřę ęľįģįþľę ƒőř mįģřäŧįőʼn. Ŧĥęįř ľäŧęşŧ vęřşįőʼn ŵįľľ þę įʼnşŧäľľęđ įʼn ŧĥę čľőūđ įʼnşŧäʼnčę, pľęäşę ūpģřäđę yőūř pľūģįʼnş þęƒőřę şŧäřŧįʼnģ ŧĥę mįģřäŧįőʼn přőčęşş.", "title": "Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ įş įʼn pūþľįč přęvįęŵ", "title-plugins": "Mįģřäŧįőʼn őƒ pľūģįʼnş" From f39517304b436d68713c8639efa3fcb88ff6fd97 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:53:26 +0000 Subject: [PATCH 197/894] Update dependency @tanstack/react-virtual to v3.11.3 (#99746) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 293 ++++-------------------------------------------------- 1 file changed, 18 insertions(+), 275 deletions(-) diff --git a/yarn.lock b/yarn.lock index f3824f36967..1bc54e8d8a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -254,14 +254,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.7, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": - version: 7.25.9 - resolution: "@babel/helper-plugin-utils@npm:7.25.9" - checksum: 10/e347d87728b1ab10b6976d46403941c8f9008c045ea6d99997a7ffca7b852dc34b6171380f7b17edf94410e0857ff26f3a53d8618f11d73744db86e8ca9b8c64 - languageName: node - linkType: hard - -"@babel/helper-plugin-utils@npm:^7.26.5": +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.7, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": version: 7.26.5 resolution: "@babel/helper-plugin-utils@npm:7.26.5" checksum: 10/1cc0fd8514da3bb249bed6c27227696ab5e84289749d7258098701cffc0c599b7f61ec40dd332f8613030564b79899d9826813c96f966330bcfc7145a8377857 @@ -7984,7 +7977,7 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ast@npm:^1.0.0-beta.11": +"@swagger-api/apidom-ast@npm:^1.0.0-beta.11, @swagger-api/apidom-ast@npm:^1.0.0-beta.5": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.11" dependencies: @@ -7998,21 +7991,7 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ast@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.5" - dependencies: - "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@types/ramda": "npm:~0.30.0" - ramda: "npm:~0.30.0" - ramda-adjunct: "npm:^5.0.0" - unraw: "npm:^3.0.0" - checksum: 10/b2ea32b8ed589a3aff122e9209d5f0c873364bb34b234d13796422d4fce6b9f52fab599ef47956f655316256cf6af821c233117c0ca96a677b867e075b70cb5d - languageName: node - linkType: hard - -"@swagger-api/apidom-core@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-core@npm:^1.0.0-beta.11": +"@swagger-api/apidom-core@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-core@npm:^1.0.0-beta.11, @swagger-api/apidom-core@npm:^1.0.0-beta.5": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-core@npm:1.0.0-beta.11" dependencies: @@ -8029,24 +8008,7 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-core@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-core@npm:1.0.0-beta.5" - dependencies: - "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@types/ramda": "npm:~0.30.0" - minim: "npm:~0.23.8" - ramda: "npm:~0.30.0" - ramda-adjunct: "npm:^5.0.0" - short-unique-id: "npm:^5.0.2" - ts-mixer: "npm:^6.0.3" - checksum: 10/c034ef286738b2b5aab525b068fd22e1b54145e3024477abcafde926f3783c280c69e4de23cc28d9cc568a62fb02719a0a89e6fd2011136cf447f42ed66fca55 - languageName: node - linkType: hard - -"@swagger-api/apidom-error@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.11": +"@swagger-api/apidom-error@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.11, @swagger-api/apidom-error@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.5": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-error@npm:1.0.0-beta.11" dependencies: @@ -8055,16 +8017,7 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-error@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-error@npm:1.0.0-beta.5" - dependencies: - "@babel/runtime-corejs3": "npm:^7.20.7" - checksum: 10/defb3ba3775be8a511ff01be4ed7d2eca66faf5ab478f65a39845c8981510a0286e622268240de56613fd3ee37de906a7c6947a82aceb214fdd89d0988b972bb - languageName: node - linkType: hard - -"@swagger-api/apidom-json-pointer@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.11": +"@swagger-api/apidom-json-pointer@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.11, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.3 <1.0.0-rc.0": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-json-pointer@npm:1.0.0-beta.11" dependencies: @@ -8078,20 +8031,6 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-json-pointer@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-json-pointer@npm:1.0.0-beta.5" - dependencies: - "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@types/ramda": "npm:~0.30.0" - ramda: "npm:~0.30.0" - ramda-adjunct: "npm:^5.0.0" - checksum: 10/68b3b196de1d2cb86663350ba70f885599a12c21804cd9bc1be1777ee5e99e9f570623599d476d241bda298e0890c34d2c9a516f7a47cf060992107c31e9d9f5 - languageName: node - linkType: hard - "@swagger-api/apidom-ns-api-design-systems@npm:^1.0.0-beta.5": version: 1.0.0-beta.5 resolution: "@swagger-api/apidom-ns-api-design-systems@npm:1.0.0-beta.5" @@ -8155,7 +8094,7 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.11": +"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.5": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.0.0-beta.11" dependencies: @@ -8170,21 +8109,6 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.0.0-beta.5" - dependencies: - "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@types/ramda": "npm:~0.30.0" - ramda: "npm:~0.30.0" - ramda-adjunct: "npm:^5.0.0" - ts-mixer: "npm:^6.0.4" - checksum: 10/2518b3aa9b7387a90553565132101616dc24c6d1f8aee581f3562bac87403f87887217fdb3cf17b113f685e051980fc66ec25d9cab7e4a0bf4922b4bcd3bc502 - languageName: node - linkType: hard - "@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.0.0-beta.11": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.0.0-beta.11" @@ -8201,23 +8125,7 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.0.0-beta.5" - dependencies: - "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.5" - "@types/ramda": "npm:~0.30.0" - ramda: "npm:~0.30.0" - ramda-adjunct: "npm:^5.0.0" - ts-mixer: "npm:^6.0.4" - checksum: 10/c439c3679dc6ea1807affa0a2913deea52ff8d63b48722c741458da147e849be9acc0c13955757a7b6904a4e7148cdb7845291da758a085083c61610a96fc36f - languageName: node - linkType: hard - -"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.11": +"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.5": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.0.0-beta.11" dependencies: @@ -8233,22 +8141,6 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.0.0-beta.5" - dependencies: - "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-json-schema-draft-6": "npm:^1.0.0-beta.5" - "@types/ramda": "npm:~0.30.0" - ramda: "npm:~0.30.0" - ramda-adjunct: "npm:^5.0.0" - ts-mixer: "npm:^6.0.4" - checksum: 10/28c8f989e26a453f2ca6c655d6822d63f7bc57137375c460c707caa964a5097566969031f322d772b49de0ce2924aeb69e2c5a6ff66191b457844e68a40f013e - languageName: node - linkType: hard - "@swagger-api/apidom-ns-openapi-2@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-2@npm:^1.0.0-beta.5": version: 1.0.0-beta.5 resolution: "@swagger-api/apidom-ns-openapi-2@npm:1.0.0-beta.5" @@ -8265,7 +8157,7 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.11": +"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.5": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:1.0.0-beta.11" dependencies: @@ -8281,23 +8173,7 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:1.0.0-beta.5" - dependencies: - "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.5" - "@types/ramda": "npm:~0.30.0" - ramda: "npm:~0.30.0" - ramda-adjunct: "npm:^5.0.0" - ts-mixer: "npm:^6.0.3" - checksum: 10/5bb9c191a3d79d9a69aa04deb5dd6ce2eaa09700210f5a89fdeb0668ec76eb3f2d4a26950405f001459e59ad199ddd4632251992b9fc1b9fa03955e1ad600810 - languageName: node - linkType: hard - -"@swagger-api/apidom-ns-openapi-3-1@npm:>=1.0.0-beta.11 <1.0.0-rc.0": +"@swagger-api/apidom-ns-openapi-3-1@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.5": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.0.0-beta.11" dependencies: @@ -8315,23 +8191,6 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.5": - version: 1.0.0-beta.5 - resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.0.0-beta.5" - dependencies: - "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-json-pointer": "npm:^1.0.0-beta.5" - "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-beta.5" - "@types/ramda": "npm:~0.30.0" - ramda: "npm:~0.30.0" - ramda-adjunct: "npm:^5.0.0" - ts-mixer: "npm:^6.0.3" - checksum: 10/69018147465c78a25efc5e7bc4439561c2d620c65c9cf86bdc503dbad9b40e52db77bb0cb11800a4c34bdc88d462126e3dc355bad290cdfe7c9fbeda391405c8 - languageName: node - linkType: hard - "@swagger-api/apidom-ns-workflows-1@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-workflows-1@npm:^1.0.0-beta.5": version: 1.0.0-beta.5 resolution: "@swagger-api/apidom-ns-workflows-1@npm:1.0.0-beta.5" @@ -8661,13 +8520,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.10.9": - version: 1.10.9 - resolution: "@swc/core-darwin-arm64@npm:1.10.9" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-darwin-x64@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-darwin-x64@npm:1.10.11" @@ -8675,13 +8527,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.10.9": - version: 1.10.9 - resolution: "@swc/core-darwin-x64@npm:1.10.9" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@swc/core-linux-arm-gnueabihf@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.10.11" @@ -8689,13 +8534,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.10.9": - version: 1.10.9 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.10.9" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@swc/core-linux-arm64-gnu@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-linux-arm64-gnu@npm:1.10.11" @@ -8703,13 +8541,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.10.9": - version: 1.10.9 - resolution: "@swc/core-linux-arm64-gnu@npm:1.10.9" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - "@swc/core-linux-arm64-musl@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-linux-arm64-musl@npm:1.10.11" @@ -8717,13 +8548,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.10.9": - version: 1.10.9 - resolution: "@swc/core-linux-arm64-musl@npm:1.10.9" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - "@swc/core-linux-x64-gnu@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-linux-x64-gnu@npm:1.10.11" @@ -8731,13 +8555,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.10.9": - version: 1.10.9 - resolution: "@swc/core-linux-x64-gnu@npm:1.10.9" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - "@swc/core-linux-x64-musl@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-linux-x64-musl@npm:1.10.11" @@ -8745,13 +8562,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.10.9": - version: 1.10.9 - resolution: "@swc/core-linux-x64-musl@npm:1.10.9" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - "@swc/core-win32-arm64-msvc@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-win32-arm64-msvc@npm:1.10.11" @@ -8759,13 +8569,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.10.9": - version: 1.10.9 - resolution: "@swc/core-win32-arm64-msvc@npm:1.10.9" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-win32-ia32-msvc@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-win32-ia32-msvc@npm:1.10.11" @@ -8773,13 +8576,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.10.9": - version: 1.10.9 - resolution: "@swc/core-win32-ia32-msvc@npm:1.10.9" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@swc/core-win32-x64-msvc@npm:1.10.11": version: 1.10.11 resolution: "@swc/core-win32-x64-msvc@npm:1.10.11" @@ -8787,14 +8583,7 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.10.9": - version: 1.10.9 - resolution: "@swc/core-win32-x64-msvc@npm:1.10.9" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@swc/core@npm:1.10.11": +"@swc/core@npm:1.10.11, @swc/core@npm:^1.7.3": version: 1.10.11 resolution: "@swc/core@npm:1.10.11" dependencies: @@ -8840,52 +8629,6 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:^1.7.3": - version: 1.10.9 - resolution: "@swc/core@npm:1.10.9" - dependencies: - "@swc/core-darwin-arm64": "npm:1.10.9" - "@swc/core-darwin-x64": "npm:1.10.9" - "@swc/core-linux-arm-gnueabihf": "npm:1.10.9" - "@swc/core-linux-arm64-gnu": "npm:1.10.9" - "@swc/core-linux-arm64-musl": "npm:1.10.9" - "@swc/core-linux-x64-gnu": "npm:1.10.9" - "@swc/core-linux-x64-musl": "npm:1.10.9" - "@swc/core-win32-arm64-msvc": "npm:1.10.9" - "@swc/core-win32-ia32-msvc": "npm:1.10.9" - "@swc/core-win32-x64-msvc": "npm:1.10.9" - "@swc/counter": "npm:^0.1.3" - "@swc/types": "npm:^0.1.17" - peerDependencies: - "@swc/helpers": "*" - dependenciesMeta: - "@swc/core-darwin-arm64": - optional: true - "@swc/core-darwin-x64": - optional: true - "@swc/core-linux-arm-gnueabihf": - optional: true - "@swc/core-linux-arm64-gnu": - optional: true - "@swc/core-linux-arm64-musl": - optional: true - "@swc/core-linux-x64-gnu": - optional: true - "@swc/core-linux-x64-musl": - optional: true - "@swc/core-win32-arm64-msvc": - optional: true - "@swc/core-win32-ia32-msvc": - optional: true - "@swc/core-win32-x64-msvc": - optional: true - peerDependenciesMeta: - "@swc/helpers": - optional: true - checksum: 10/543e79c249f6052883d656035321d449cf6c0f2ea54f786d5e3b96394d4cf201b293d6c3f897cc604eb145b21cce82f904306931fe9efbc6a50c714a5d5d97f0 - languageName: node - linkType: hard - "@swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -8912,21 +8655,21 @@ __metadata: linkType: hard "@tanstack/react-virtual@npm:^3.5.1, @tanstack/react-virtual@npm:^3.9.0": - version: 3.11.2 - resolution: "@tanstack/react-virtual@npm:3.11.2" + version: 3.11.3 + resolution: "@tanstack/react-virtual@npm:3.11.3" dependencies: - "@tanstack/virtual-core": "npm:3.11.2" + "@tanstack/virtual-core": "npm:3.11.3" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/a1136da0ec4c2ecbd4f996d8b84f228f0b8d851b15806e01049a160ad1d9b2eef0e0a491035fe017c6f84a0e125334f69ea23b32c180df23614ea4a8eeb7490c + checksum: 10/eb39f8a015f4dc98070f0c18bbb1f9c094b7182133554ef3ee31d2678cd3a66edd28ce854d533e830f88f1f0ad1d5b065de184438a08fe774a9acc1dc62da436 languageName: node linkType: hard -"@tanstack/virtual-core@npm:3.11.2": - version: 3.11.2 - resolution: "@tanstack/virtual-core@npm:3.11.2" - checksum: 10/8433044a5c801052ba2e4cdda098cdc8e32adfd3a76ba31af7064bbdda60062fe221a3558096987baa66cd94f528855e887c282cb0f9eb99d3751457c2a62872 +"@tanstack/virtual-core@npm:3.11.3": + version: 3.11.3 + resolution: "@tanstack/virtual-core@npm:3.11.3" + checksum: 10/24a3369dd0290d4f19aa1af7d0a6fb1b843741d722c6a5cf786416657bbf978f4f82a0b257eaee867d0798d8334374f5e940868a7b71dc065939fb7eeee19ad1 languageName: node linkType: hard From c0600969e0857037d37bbbea15088eb4274582b1 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:04:12 -0600 Subject: [PATCH 198/894] CI: update changelog generator to use compare API (#99688) update changelog generator to use compare API --- .github/workflows/actions/changelog/index.js | 46 +++++++++++--------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/.github/workflows/actions/changelog/index.js b/.github/workflows/actions/changelog/index.js index 48da9dcf33a..b658caf349c 100644 --- a/.github/workflows/actions/changelog/index.js +++ b/.github/workflows/actions/changelog/index.js @@ -69,8 +69,17 @@ const graphql = async (ghtoken, query, variables) => { }, body: JSON.stringify({ query, variables }), }); - const { data } = await results.json(); - return data; + + const res = await results.json(); + + LOG( + JSON.stringify({ + status: results.status, + text: results.statusText, + }) + ); + + return res.data; }; // Using Github GraphQL API find the timestamp for the given tag/commit hash. @@ -99,20 +108,20 @@ const getCommitishDate = async (name, owner, target) => { // Using Github GraphQL API get a list of PRs between the two "commitish" items. // This resoves the "since" item's timestamp first and iterates over all PRs // till "target" using naïve pagination. -const getHistory = async (name, owner, target, sinceDate) => { - LOG(`Fetching ${owner}/${name} PRs since ${sinceDate} till ${target}`); +const getHistory = async (name, owner, from, to) => { + LOG(`Fetching ${owner}/${name} PRs between ${from} and ${to}`); const query = ` query findCommitsWithAssociatedPullRequests( $name: String! $owner: String! - $target: String! - $sinceDate: GitTimestamp + $from: String! + $to: String! $cursor: String ) { repository(name: $name, owner: $owner) { - object(expression: $target) { - ... on Commit { - history(first: 50, since: $sinceDate, after: $cursor) { + ref(qualifiedName: $from) { + compare(headRef: $to) { + commits(first: 25, after: $cursor) { totalCount pageInfo { hasNextPage @@ -155,13 +164,13 @@ const getHistory = async (name, owner, target, sinceDate) => { const result = await graphql(ghtoken, query, { name, owner, - target, - sinceDate, + from, + to, cursor, }); LOG(`GraphQL: ${JSON.stringify(result)}`); - nodes = [...nodes, ...result.repository.object.history.nodes]; - const { hasNextPage, endCursor } = result.repository.object.history.pageInfo; + nodes = [...nodes, ...result.repository.ref.compare.commits.nodes]; + const { hasNextPage, endCursor } = result.repository.ref.compare.commits.pageInfo; if (!hasNextPage) { break; } @@ -175,11 +184,11 @@ const getHistory = async (name, owner, target, sinceDate) => { // feature, deprecation, breaking change and plugin fixes/enhancements). // // PR grouping relies on Github labels only, not on the PR contents. -const getChangeLogItems = async (name, owner, sinceDate, to) => { +const getChangeLogItems = async (name, owner, from, to) => { // check if a node contains a certain label const hasLabel = ({ labels }, label) => labels.nodes.some(({ name }) => name === label); // get all the PRs between the two "commitish" items - const history = await getHistory(name, owner, to, sinceDate); + const history = await getHistory(name, owner, from, to); const items = history.flatMap((node) => { // discard PRs without a "changelog" label @@ -231,13 +240,10 @@ const previous = process.argv[3] || process.env.INPUT_PREVIOUS || (await getPrev LOG(`Previous tag/commit: ${previous}`); -const sinceDate = await getCommitishDate('grafana', 'grafana', previous); -LOG(`Previous tag/commit timestamp: ${sinceDate}`); - // Get all changelog items from Grafana OSS -const oss = await getChangeLogItems('grafana', 'grafana', sinceDate, target); +const oss = await getChangeLogItems('grafana', 'grafana', previous, target); // Get all changelog items from Grafana Enterprise -const entr = await getChangeLogItems('grafana-enterprise', 'grafana', sinceDate, target); +const entr = await getChangeLogItems('grafana-enterprise', 'grafana', previous, target); LOG(`Found OSS PRs: ${oss.length}`); LOG(`Found Enterprise PRs: ${entr.length}`); From 9b37337f7b1061c9ff6549f1ca7db88ff79900e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:14:41 +0200 Subject: [PATCH 199/894] Update dependency @types/webpack-env to v1.18.8 (#99759) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1bc54e8d8a4..d4270687974 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10322,9 +10322,9 @@ __metadata: linkType: hard "@types/webpack-env@npm:^1.18.4": - version: 1.18.5 - resolution: "@types/webpack-env@npm:1.18.5" - checksum: 10/3c8dd0b23d45e2d33abdfbae7f1d8f75ce23d54588b08943e833f4dba81eb683ac68672a75eccbdba8e008bc1647638803c1bcadc8cdfd1dd7142fa2c3f612de + version: 1.18.8 + resolution: "@types/webpack-env@npm:1.18.8" + checksum: 10/f3932f3d6c2530f644cfc898eda1ab8182d6ae57f555c2f0179d813549b639078671b71e4041831fc306c5ebe61f5cdac794fe4ceae281fce8bf67e23661a488 languageName: node linkType: hard From 90c18099a54cab1356796dca63b9033ec61fffd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Wed, 29 Jan 2025 16:38:25 +0100 Subject: [PATCH 200/894] fix(unified-storage): return folder title in legacy search (#99762) --- .../dashboards/service/dashboard_service.go | 32 +++++++++--- .../service/dashboard_service_test.go | 50 +++++++++++-------- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 8f4953d2723..d56118429d2 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -23,6 +23,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" @@ -1248,16 +1249,31 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb } finalResults := make([]dashboards.DashboardSearchProjection, len(response.Hits)) + // Create a small runtime cache for folders to avoid extra calls to the folder service + foldersMap := make(map[string]*folder.Folder) for i, hit := range response.Hits { + f, ok := foldersMap[hit.Folder] + if !ok { + f, err = dr.folderService.Get(ctx, &folder.GetFolderQuery{ + UID: &hit.Folder, + OrgID: query.OrgId, + SignedInUser: query.SignedInUser, + }) + if err != nil { + return nil, err + } + foldersMap[hit.Folder] = f + } finalResults[i] = dashboards.DashboardSearchProjection{ - ID: hit.Field.GetNestedInt64(search.DASHBOARD_LEGACY_ID), - UID: hit.Name, - OrgID: query.OrgId, - Title: hit.Title, - Slug: slugify.Slugify(hit.Title), - IsFolder: false, - FolderUID: hit.Folder, - Tags: hit.Tags, + ID: hit.Field.GetNestedInt64(search.DASHBOARD_LEGACY_ID), + UID: hit.Name, + OrgID: query.OrgId, + Title: hit.Title, + Slug: slugify.Slugify(hit.Title), + IsFolder: false, + FolderUID: hit.Folder, + FolderTitle: f.Title, + Tags: hit.Tags, } } diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index d7adcd9ad9a..1ae6c4f5b90 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -10,6 +10,9 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/components/simplejson" @@ -28,8 +31,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func TestDashboardService(t *testing.T) { @@ -71,10 +72,10 @@ func TestDashboardService(t *testing.T) { t.Run("Should return validation error if message is too long", func(t *testing.T) { dto.Dashboard = dashboards.NewDashboard("Dash") dto.Message = `Here we go, 500+ characters for testing. I'm sorry that you're - having to read this. I spent too long trying to come up with something clever + having to read this. I spent too long trying to come up with something clever to say or a funny joke. Unforuntately, nothing came to mind. So instead, I'm will share this with you, as a form of payment for having to read this: - https://youtu.be/dQw4w9WgXcQ?si=KeoTIpn9tUtQnOBk! Enjoy :) Now lets see if + https://youtu.be/dQw4w9WgXcQ?si=KeoTIpn9tUtQnOBk! Enjoy :) Now lets see if this test passes or if the result is more exciting than these 500 characters I wrote. Best of luck to the both of us!` _, err := service.SaveDashboard(context.Background(), dto, false) @@ -1327,10 +1328,15 @@ func TestDeleteAllDashboards(t *testing.T) { func TestSearchDashboards(t *testing.T) { fakeStore := dashboards.FakeDashboardStore{} + fakeFolders := foldertest.NewFakeService() + fakeFolders.ExpectedFolder = &folder.Folder{ + Title: "testing-folder-1", + } defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ cfg: setting.NewCfg(), dashboardStore: &fakeStore, + folderService: fakeFolders, } expectedResult := model.HitList{ @@ -1345,15 +1351,17 @@ func TestSearchDashboards(t *testing.T) { "tag1", "tag2", }, + FolderTitle: "testing-folder-1", }, { - UID: "uid2", - OrgID: 1, - Title: "Dashboard 2", - Type: "dash-db", - URI: "db/dashboard-2", - URL: "/d/uid2/dashboard-2", - Tags: []string{}, + UID: "uid2", + OrgID: 1, + Title: "Dashboard 2", + Type: "dash-db", + URI: "db/dashboard-2", + URL: "/d/uid2/dashboard-2", + Tags: []string{}, + FolderTitle: "testing-folder-1", }, } query := dashboards.FindPersistedDashboardsQuery{ @@ -1363,17 +1371,19 @@ func TestSearchDashboards(t *testing.T) { service.features = featuremgmt.WithFeatures() fakeStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{ { - UID: "uid1", - Slug: "dashboard-1", - OrgID: 1, - Title: "Dashboard 1", - Tags: []string{"tag1", "tag2"}, + UID: "uid1", + Slug: "dashboard-1", + OrgID: 1, + Title: "Dashboard 1", + Tags: []string{"tag1", "tag2"}, + FolderTitle: "testing-folder-1", }, { - UID: "uid2", - Slug: "dashboard-2", - OrgID: 1, - Title: "Dashboard 2", + UID: "uid2", + Slug: "dashboard-2", + OrgID: 1, + Title: "Dashboard 2", + FolderTitle: "testing-folder-1", }, }, nil).Once() result, err := service.SearchDashboards(context.Background(), &query) From 7883215c680a5bc9c7782918e77b81d2095c5236 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:40:06 +0000 Subject: [PATCH 201/894] Tempo: Support TraceQL instant metrics queries (#99732) Support TraceQL instant metrics --- .../dataquery/x/TempoDataQuery_types.gen.ts | 9 ++++ .../kinds/dataquery/types_dataquery_gen.go | 13 ++++- pkg/tsdb/tempo/traceql/metrics.go | 40 +++++++++++++++ pkg/tsdb/tempo/traceql/metrics_test.go | 44 ++++++++++++++++ pkg/tsdb/tempo/traceql_query.go | 50 +++++++++++++++---- .../plugins/datasource/tempo/dataquery.cue | 4 ++ .../plugins/datasource/tempo/dataquery.gen.ts | 9 ++++ .../traceql/TempoQueryBuilderOptions.tsx | 20 +++++++- 8 files changed, 177 insertions(+), 12 deletions(-) diff --git a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts index b93945b296d..9a279f1653b 100644 --- a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts @@ -30,6 +30,10 @@ export interface TempoQuery extends common.DataQuery { * @deprecated Define the maximum duration to select traces. Use duration format, for example: 1.2s, 100ms */ maxDuration?: string; + /** + * For metric queries, whether to run instant or range queries + */ + metricsQueryType?: MetricsQueryType; /** * @deprecated Define the minimum duration to select traces. Use duration format, for example: 1.2s, 100ms */ @@ -79,6 +83,11 @@ export const defaultTempoQuery: Partial = { export type TempoQueryType = ('traceql' | 'traceqlSearch' | 'serviceMap' | 'upload' | 'nativeSearch' | 'traceId' | 'clear'); +export enum MetricsQueryType { + Instant = 'instant', + Range = 'range', +} + /** * The state of the TraceQL streaming search query */ diff --git a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go index a709b74c0e6..079185206f1 100644 --- a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go @@ -54,13 +54,15 @@ type TempoQuery struct { TableType *SearchTableType `json:"tableType,omitempty"` // For metric queries, the step size to use Step *string `json:"step,omitempty"` + // For metric queries, how many exemplars to request, 0 means no exemplars + Exemplars *int64 `json:"exemplars,omitempty"` // For mixed data sources the selected datasource is on the query level. // For non mixed scenarios this is undefined. // TODO find a better way to do this ^ that's friendly to schema // TODO this shouldn't be unknown but DataSourceRef | null Datasource any `json:"datasource,omitempty"` - // For metric queries, how many exemplars to request, 0 means no exemplars - Exemplars *int64 `json:"exemplars,omitempty"` + // For metric queries, whether to run instant or range queries + MetricsQueryType *MetricsQueryType `json:"metricsQueryType,omitempty"` } // NewTempoQuery creates a new TempoQuery object. @@ -80,6 +82,13 @@ const ( TempoQueryTypeClear TempoQueryType = "clear" ) +type MetricsQueryType string + +const ( + MetricsQueryTypeRange MetricsQueryType = "range" + MetricsQueryTypeInstant MetricsQueryType = "instant" +) + // The state of the TraceQL streaming search query type SearchStreamingState string diff --git a/pkg/tsdb/tempo/traceql/metrics.go b/pkg/tsdb/tempo/traceql/metrics.go index 06fd22b1fb0..54e5e34c7e6 100644 --- a/pkg/tsdb/tempo/traceql/metrics.go +++ b/pkg/tsdb/tempo/traceql/metrics.go @@ -59,6 +59,46 @@ func TransformMetricsResponse(query *dataquery.TempoQuery, resp tempopb.QueryRan return append(frames, exemplarFrames...) } +func TransformInstantMetricsResponse(query *dataquery.TempoQuery, resp tempopb.QueryInstantResponse) []*data.Frame { + frames := make([]*data.Frame, len(resp.Series)) + + for i, series := range resp.Series { + name, labels := transformLabelsAndGetName(series.Labels) + + labelKeys := make([]string, 0, len(labels)) + labelFields := make([]*data.Field, 0, len(labels)) + for key := range labels { + labelKeys = append(labelKeys, key) + labelFields = append(labelFields, data.NewField(key, nil, []string{})) + } + + timeField := data.NewField("time", nil, []time.Time{}) + valueField := data.NewField("value", labels, []float64{}) + valueField.Config = &data.FieldConfig{ + DisplayName: name, + } + + frame := &data.Frame{ + RefID: name, + Name: name, + Fields: append([]*data.Field{timeField}, append(labelFields, valueField)...), + Meta: &data.FrameMeta{ + PreferredVisualization: data.VisTypeTable, + }, + } + + labelValues := make([]interface{}, len(labels)) + for idx, key := range labelKeys { + labelValues[idx] = strings.Trim(labels[key], "\"") + } + row := append([]interface{}{time.Now()}, append(labelValues, series.GetValue())...) + frame.AppendRow(row...) + + frames[i] = frame + } + return frames +} + func metricsValueToString(value *v1.AnyValue) (string, string) { switch value.GetValue().(type) { case *v1.AnyValue_DoubleValue: diff --git a/pkg/tsdb/tempo/traceql/metrics_test.go b/pkg/tsdb/tempo/traceql/metrics_test.go index f5ac27c3f12..8254af5f8b5 100644 --- a/pkg/tsdb/tempo/traceql/metrics_test.go +++ b/pkg/tsdb/tempo/traceql/metrics_test.go @@ -122,3 +122,47 @@ func TestTransformMetricsResponse_MultipleSeries(t *testing.T) { assert.Equal(t, time.UnixMilli(1638316800000), frames[1].Fields[0].At(0)) assert.Equal(t, 4.56, frames[1].Fields[1].At(0)) } + +func TestTransformInstantMetricsResponse(t *testing.T) { + query := &dataquery.TempoQuery{} + resp := tempopb.QueryInstantResponse{ + Series: []*tempopb.InstantSeries{ + { + Labels: []v1.KeyValue{ + { + Key: "label", + Value: &v1.AnyValue{Value: &v1.AnyValue_StringValue{StringValue: "value"}}, + }, + }, + Value: 123.45, + PromLabels: "label=\"value\"", + }, + }, + } + + frames := TransformInstantMetricsResponse(query, resp) + + assert.Len(t, frames, 1) + frame := frames[0] + + assert.Equal(t, "value", frame.RefID) + assert.Equal(t, "value", frame.Name) + assert.Len(t, frame.Fields, 3) + + timeField := frame.Fields[0] + assert.Equal(t, "time", timeField.Name) + assert.Equal(t, 1, timeField.Len()) + assert.IsType(t, time.Time{}, timeField.At(0)) + + labelField := frame.Fields[1] + assert.Equal(t, "label", labelField.Name) + assert.Equal(t, 1, labelField.Len()) + assert.IsType(t, "", labelField.At(0)) + assert.Equal(t, "value", labelField.At(0)) + + valueField := frame.Fields[2] + assert.Equal(t, "value", valueField.Name) + assert.Equal(t, 1, valueField.Len()) + assert.IsType(t, 0.0, valueField.At(0)) + assert.Equal(t, 123.45, valueField.At(0).(float64)) +} diff --git a/pkg/tsdb/tempo/traceql_query.go b/pkg/tsdb/tempo/traceql_query.go index 35d45b0beee..5265097ad41 100644 --- a/pkg/tsdb/tempo/traceql_query.go +++ b/pkg/tsdb/tempo/traceql_query.go @@ -14,6 +14,7 @@ import ( //nolint:all "github.com/golang/protobuf/jsonpb" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" "github.com/grafana/grafana/pkg/tsdb/tempo/kinds/dataquery" "github.com/grafana/grafana/pkg/tsdb/tempo/traceql" @@ -86,21 +87,40 @@ func (s *Service) runTraceQlQueryMetrics(ctx context.Context, pCtx backend.Plugi return result, nil } - var queryResponse tempopb.QueryRangeResponse - err = jsonpb.Unmarshal(bytes.NewReader(responseBody), &queryResponse) + if isInstantQuery(tempoQuery.MetricsQueryType) { + var queryResponse tempopb.QueryInstantResponse + err = jsonpb.Unmarshal(bytes.NewReader(responseBody), &queryResponse) + if res, err := handleConversionError(ctxLogger, span, err); err != nil { + return res, err + } + + frames := traceql.TransformInstantMetricsResponse(tempoQuery, queryResponse) + result.Frames = frames + } else { + var queryResponse tempopb.QueryRangeResponse + err = jsonpb.Unmarshal(bytes.NewReader(responseBody), &queryResponse) + + if res, err := handleConversionError(ctxLogger, span, err); err != nil { + return res, err + } + + frames := traceql.TransformMetricsResponse(tempoQuery, queryResponse) + result.Frames = frames + } + + ctxLogger.Debug("Successfully performed TraceQL query", "function", logEntrypoint()) + return result, nil +} + +func handleConversionError(ctxLogger log.Logger, span trace.Span, err error) (*backend.DataResponse, error) { if err != nil { ctxLogger.Error("Failed to convert response to type", "error", err, "function", logEntrypoint()) span.RecordError(err) span.SetStatus(codes.Error, err.Error()) return &backend.DataResponse{}, fmt.Errorf("failed to convert response to type: %w", err) } - - frames := traceql.TransformMetricsResponse(tempoQuery, queryResponse) - - result.Frames = frames - ctxLogger.Debug("Successfully performed TraceQL query", "function", logEntrypoint()) - return result, nil + return nil, nil } func (s *Service) performMetricsQuery(ctx context.Context, dsInfo *Datasource, model *dataquery.TempoQuery, query backend.DataQuery, span trace.Span) (*http.Response, []byte, error) { @@ -133,7 +153,12 @@ func (s *Service) performMetricsQuery(ctx context.Context, dsInfo *Datasource, m func (s *Service) createMetricsQuery(ctx context.Context, dsInfo *Datasource, query *dataquery.TempoQuery, start int64, end int64) (*http.Request, error) { ctxLogger := s.logger.FromContext(ctx) - rawUrl := fmt.Sprintf("%s/api/metrics/query_range", dsInfo.URL) + queryType := "query_range" + if isInstantQuery(query.MetricsQueryType) { + queryType = "query" + } + + rawUrl := fmt.Sprintf("%s/api/metrics/%s", dsInfo.URL, queryType) searchUrl, err := url.Parse(rawUrl) if err != nil { ctxLogger.Error("Failed to parse URL", "url", rawUrl, "error", err, "function", logEntrypoint()) @@ -167,6 +192,13 @@ func (s *Service) createMetricsQuery(ctx context.Context, dsInfo *Datasource, qu return req, nil } +func isInstantQuery(metricQueryType *dataquery.MetricsQueryType) bool { + if metricQueryType == nil { + return false + } + return *metricQueryType == dataquery.MetricsQueryTypeInstant +} + func isMetricsQuery(query string) bool { match, _ := regexp.MatchString("\\|\\s*(rate|count_over_time|avg_over_time|max_over_time|min_over_time|quantile_over_time|histogram_over_time|compare)\\s*\\(", query) return match diff --git a/public/app/plugins/datasource/tempo/dataquery.cue b/public/app/plugins/datasource/tempo/dataquery.cue index 345a5d11409..ef5a4526dbb 100644 --- a/public/app/plugins/datasource/tempo/dataquery.cue +++ b/public/app/plugins/datasource/tempo/dataquery.cue @@ -55,10 +55,14 @@ composableKinds: DataQuery: { step?: string // For metric queries, how many exemplars to request, 0 means no exemplars exemplars?: int64 + // For metric queries, whether to run instant or range queries + metricsQueryType?: #MetricsQueryType } @cuetsy(kind="interface") @grafana(TSVeneer="type") #TempoQueryType: "traceql" | "traceqlSearch" | "serviceMap" | "upload" | "nativeSearch" | "traceId" | "clear" @cuetsy(kind="type") + #MetricsQueryType: "range" | "instant" @cuetsy(kind="enum") + // The state of the TraceQL streaming search query #SearchStreamingState: "pending" | "streaming" | "done" | "error" @cuetsy(kind="enum") diff --git a/public/app/plugins/datasource/tempo/dataquery.gen.ts b/public/app/plugins/datasource/tempo/dataquery.gen.ts index 31a2a9f3fbe..757960330c8 100644 --- a/public/app/plugins/datasource/tempo/dataquery.gen.ts +++ b/public/app/plugins/datasource/tempo/dataquery.gen.ts @@ -28,6 +28,10 @@ export interface TempoQuery extends common.DataQuery { * @deprecated Define the maximum duration to select traces. Use duration format, for example: 1.2s, 100ms */ maxDuration?: string; + /** + * For metric queries, whether to run instant or range queries + */ + metricsQueryType?: MetricsQueryType; /** * @deprecated Define the minimum duration to select traces. Use duration format, for example: 1.2s, 100ms */ @@ -77,6 +81,11 @@ export const defaultTempoQuery: Partial = { export type TempoQueryType = ('traceql' | 'traceqlSearch' | 'serviceMap' | 'upload' | 'nativeSearch' | 'traceId' | 'clear'); +export enum MetricsQueryType { + Instant = 'instant', + Range = 'range', +} + /** * The state of the TraceQL streaming search query */ diff --git a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx index f273bc788ca..5834a153594 100644 --- a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx @@ -6,7 +6,7 @@ import { EditorField, EditorRow } from '@grafana/experimental'; import { AutoSizeInput, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { QueryOptionGroup } from '../_importedDependencies/datasources/prometheus/QueryOptionGroup'; -import { SearchTableType } from '../dataquery.gen'; +import { SearchTableType, MetricsQueryType } from '../dataquery.gen'; import { DEFAULT_LIMIT, DEFAULT_SPSS } from '../datasource'; import { TempoQuery } from '../types'; @@ -40,6 +40,10 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query, is query.tableType = SearchTableType.Traces; } + if (!query.hasOwnProperty('metricsQueryType')) { + query.metricsQueryType = MetricsQueryType.Range; + } + const onLimitChange = (e: React.FormEvent) => { onChange({ ...query, limit: parseIntWithFallback(e.currentTarget.value, DEFAULT_LIMIT) }); }; @@ -49,6 +53,9 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query, is const onTableTypeChange = (val: SearchTableType) => { onChange({ ...query, tableType: val }); }; + const onMetricsQueryTypeChange = (val: MetricsQueryType) => { + onChange({ ...query, metricsQueryType: val }); + }; const onStepChange = (e: React.FormEvent) => { onChange({ ...query, step: e.currentTarget.value }); }; @@ -74,6 +81,7 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query, is const collapsedMetricsOptions = [ `Step: ${query.step || 'auto'}`, + `Type: ${query.metricsQueryType === MetricsQueryType.Range ? 'Range' : 'Instant'}`, // `Exemplars: ${query.exemplars !== undefined ? query.exemplars : 'auto'}`, ]; @@ -132,6 +140,16 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query, is value={query.step} /> + + + {/* Date: Wed, 29 Jan 2025 15:46:13 +0000 Subject: [PATCH 202/894] Chore: add lint rule prevent `t` import from `i18next` (#99761) * also handle i18next import * fix violations --- eslint.config.js | 5 +++++ .../features/explore/RichHistory/RichHistoryAddToLibrary.tsx | 2 +- public/app/features/manage-dashboards/utils/validation.ts | 3 +-- .../features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx | 2 +- public/app/features/profile/UserSessions.tsx | 3 +-- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index cf427ade626..8c4910b7310 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -117,6 +117,11 @@ module.exports = [ importNames: ['Trans', 't'], message: 'Please import from app/core/internationalization instead', }, + { + name: 'i18next', + importNames: ['t'], + message: 'Please import from app/core/internationalization instead', + }, ], }, ], diff --git a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx index 260d6f945b6..f735180c11a 100644 --- a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx @@ -1,8 +1,8 @@ -import { t } from 'i18next'; import { useState } from 'react'; import { DataQuery } from '@grafana/schema'; import { Button, Modal } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; import { isQueryLibraryEnabled, useListQueryTemplateQuery } from 'app/features/query-library'; import { getK8sNamespace } from '../../query-library/api/query'; diff --git a/public/app/features/manage-dashboards/utils/validation.ts b/public/app/features/manage-dashboards/utils/validation.ts index b6d78b9b952..7e4b1d9089e 100644 --- a/public/app/features/manage-dashboards/utils/validation.ts +++ b/public/app/features/manage-dashboards/utils/validation.ts @@ -1,5 +1,4 @@ -import { t } from 'i18next'; - +import { t } from 'app/core/internationalization'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { validationSrv } from '../services/ValidationSrv'; diff --git a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx index a5baf03f93e..e21fd83e815 100644 --- a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx +++ b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx @@ -1,7 +1,7 @@ -import { t } from 'i18next'; import { useRef, useEffect } from 'react'; import { useAppNotification } from 'app/core/copy/appNotification'; +import { t } from 'app/core/internationalization'; import { GetSnapshotResponseDto, SnapshotDto } from '../api'; diff --git a/public/app/features/profile/UserSessions.tsx b/public/app/features/profile/UserSessions.tsx index a6ed2d98543..f6cc0538c64 100644 --- a/public/app/features/profile/UserSessions.tsx +++ b/public/app/features/profile/UserSessions.tsx @@ -1,11 +1,10 @@ import { css } from '@emotion/css'; -import { t } from 'i18next'; import { PureComponent } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { Button, Icon, LoadingPlaceholder } from '@grafana/ui'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; -import { Trans } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; import { formatDate } from 'app/core/internationalization/dates'; import { UserSession } from 'app/types'; From 0613ed1f1147d78521af2cf0cfe4deb08a8f4888 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:47:25 +0000 Subject: [PATCH 203/894] Update dependency i18next to v24.2.2 (#99763) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d4270687974..1a4a877ad45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18850,8 +18850,8 @@ __metadata: linkType: hard "i18next@npm:^23.5.1 || ^24.2.0, i18next@npm:^24.0.0": - version: 24.2.1 - resolution: "i18next@npm:24.2.1" + version: 24.2.2 + resolution: "i18next@npm:24.2.2" dependencies: "@babel/runtime": "npm:^7.23.2" peerDependencies: @@ -18859,7 +18859,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/74836c3ca3365155906f95162bf75461f8f82a86034b03fc8efc670e10f610299dc5b51923acc1ab9d52ec7e7e717e44fb95b91ff1560ff45c6bad0c383517af + checksum: 10/f66ed9e56d9412e59502f5df39163631daf9f1264774732fb21edbd66a528ca7a6b67dc2e2aec95683c6c7956e42c651587a54bd8ee082bd12008880ce6cd326 languageName: node linkType: hard From 9f4e8ee206c3715680a562eb6fe88613fcb59cff Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 29 Jan 2025 18:05:40 +0200 Subject: [PATCH 204/894] Query Library: Update API client (#99382) * Add process script * Working version * Use new types * Use new types * Update client * Tweaks * Process multiple specs * Remove 'any' types * Use BASE_URL * Update CODEOWNERS * Fix filename * add openapi * update CODEOWNDER * use JSONeq * Use existing specs * Filter ForAllNamespaces * Add instructions * Switch to tsx * Use openapi-types * Update src path * Expand docs * Update docs * Rename script * codeowners * More docs * Move openapi-types to dev deps * Update error message * Update doc * Fix typo --------- Co-authored-by: Ryan McKinley --- .github/CODEOWNERS | 1 + package.json | 4 +- pkg/tests/apis/openapi_snapshots/README.md | 33 +++- pkg/tests/apis/openapi_test.go | 5 +- .../QueryLibrary/QueryTemplateForm.tsx | 8 +- .../QueryLibrary/QueryTemplatesList.tsx | 9 +- .../QueryTemplatesTable/ActionsCell.tsx | 4 +- .../explore/RichHistory/RichHistory.tsx | 5 +- .../RichHistory/RichHistoryAddToLibrary.tsx | 5 +- .../query-library/api/endpoints.gen.ts | 127 +++++++--------- .../app/features/query-library/api/mappers.ts | 9 +- .../app/features/query-library/api/query.ts | 4 +- .../api/scripts/generate-rtk-apis.ts | 6 +- scripts/process-specs.ts | 143 ++++++++++++++++++ yarn.lock | 8 + 15 files changed, 260 insertions(+), 111 deletions(-) create mode 100644 scripts/process-specs.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 499078f1e7f..82604409275 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -617,6 +617,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /scripts/cleanup-husky.sh @grafana/frontend-ops /scripts/verify-repo-update/ @grafana/grafana-developer-enablement-squad /scripts/generate-rtk-apis.ts @grafana/grafana-frontend-platform +/scripts/process-specs.ts @grafana/grafana-frontend-platform /scripts/generate-alerting-rtk-apis.ts @grafana/alerting-frontend /scripts/levitate-parse-json-report.js @grafana/plugins-platform-frontend /scripts/levitate-show-affected-plugins.js @grafana/plugins-platform-frontend diff --git a/package.json b/package.json index 0ebd64c648a..0456907f286 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,8 @@ "plugin:build:commit": "nx run-many -t build:commit --projects='tag:scope:plugin'", "plugin:build:dev": "nx run-many -t dev --projects='tag:scope:plugin' --maxParallel=100", "generate-icons": "nx run grafana-icons:generate", - "generate-apis": "rtk-query-codegen-openapi ./scripts/generate-rtk-apis.ts" + "process-specs": "npx tsx scripts/process-specs.ts", + "generate-apis": "yarn process-specs && rtk-query-codegen-openapi ./scripts/generate-rtk-apis.ts" }, "grafana": { "whatsNewUrl": "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v11-4/", @@ -215,6 +216,7 @@ "ngtemplate-loader": "2.1.0", "node-notifier": "10.0.1", "nx": "19.8.2", + "openapi-types": "^12.1.3", "postcss": "8.5.1", "postcss-loader": "8.1.1", "postcss-reporter": "7.1.0", diff --git a/pkg/tests/apis/openapi_snapshots/README.md b/pkg/tests/apis/openapi_snapshots/README.md index 93fdfdbeba9..7ca313c4be3 100644 --- a/pkg/tests/apis/openapi_snapshots/README.md +++ b/pkg/tests/apis/openapi_snapshots/README.md @@ -1,3 +1,32 @@ -This folder contains a rendered OpenAPI for each group/version +This folder contains a rendered OpenAPI file for each group/version. The “real” OpenAPI is generated by the running server, but the files here are used to build static frontend clients. -The "real" openapi is generated by the running server, but this is used to build static frontend clients +## Generating RTK API Clients + +The RTK API clients are generated from processed OpenAPI files using the `scripts/process-specs.ts` script. The source files are in `pkg/tests/apis/openapi_snapshots`, and the processed files are stored in the `data/openapi` directory. Spec processing happens as part of `yarn generate-apis` task, but can also be triggered separately (see below). + +To generate or update the RTK API clients: + +1. _If generating or updating an RTK client for the first time_, update `scripts/generate-rtk-apis.js` so `schemaFile` points to the processed spec files, for example: + ```typescript + '../public/app/features/dashboards/api/endpoints.gen.ts': { + schemaFile: '../data/openapi/dashboard.grafana.app-v0alpha1.json', + }, + ``` + +2. Generate or update the OpenAPI spec files by running: + ```bash + go test pkg/tests/apis/openapi_test.go + ``` + _If generating an RTK client for a new API_, also add a new group/version of the API to the `groups` slice. If the API is behind a feature toggle, add the toggle to `EnableFeatureToggles` in `pkg/tests/apis/openapi_test.go`. + + +3. Run: + ```bash + yarn generate-apis + ``` + This command generates (or updates) the spec files in the `data/openapi` directory and generates the RTK API clients. + +If you want to process the OpenAPI files without generating the RTK API clients (for example, if you have a separate `generate-rtk-apis` file), run: +```bash +yarn process-specs +``` diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index ec9cda563ca..a4a7164fa0d 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -63,7 +63,7 @@ func TestIntegrationOpenAPIs(t *testing.T) { dir := "openapi_snapshots" - for _, gv := range []schema.GroupVersion{{ + var groups = []schema.GroupVersion{{ Group: "dashboard.grafana.app", Version: "v0alpha1", }, { @@ -72,7 +72,8 @@ func TestIntegrationOpenAPIs(t *testing.T) { }, { Group: "peakq.grafana.app", Version: "v0alpha1", - }} { + }} + for _, gv := range groups { VerifyOpenAPISnapshots(t, dir, gv, h) } } diff --git a/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx index dfa248707d7..801b9899802 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx @@ -13,7 +13,6 @@ import { useCreateQueryTemplateMutation, useUpdateQueryTemplateMutation } from ' import { AddQueryTemplateCommand, EditQueryTemplateCommand } from 'app/features/query-library/types'; import { convertAddQueryTemplateCommandToDataQuerySpec } from '../../query-library/api/mappers'; -import { getK8sNamespace } from '../../query-library/api/query'; import { useDatasource } from '../QueryLibrary/utils/useDatasource'; import { QueryTemplateRow } from './QueryTemplatesTable/types'; @@ -64,9 +63,7 @@ export const QueryTemplateForm = ({ onCancel, onSave, queryToAdd, templateData } const handleAddQueryTemplate = async (addQueryTemplateCommand: AddQueryTemplateCommand) => { return addQueryTemplate({ - namespace: getK8sNamespace(), - comGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate: - convertAddQueryTemplateCommandToDataQuerySpec(addQueryTemplateCommand), + queryTemplate: convertAddQueryTemplateCommandToDataQuerySpec(addQueryTemplateCommand), }) .unwrap() .then(() => { @@ -89,9 +86,8 @@ export const QueryTemplateForm = ({ onCancel, onSave, queryToAdd, templateData } const handleEditQueryTemplate = async (editQueryTemplateCommand: EditQueryTemplateCommand) => { return editQueryTemplate({ - namespace: getK8sNamespace(), name: editQueryTemplateCommand.uid, - ioK8SApimachineryPkgApisMetaV1Patch: { + patch: { spec: editQueryTemplateCommand.partialSpec, }, }) diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx index a0860add66e..7f238354704 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx @@ -13,7 +13,6 @@ import { QueryTemplate } from 'app/features/query-library/types'; import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { convertDataQueryResponseToQueryTemplates } from '../../query-library/api/mappers'; -import { getK8sNamespace } from '../../query-library/api/query'; import { QueryLibraryProps } from './QueryLibrary'; import { queryLibraryTrackFilterDatasource } from './QueryLibraryAnalyticsEvents'; @@ -25,13 +24,7 @@ import { searchQueryLibrary } from './utils/search'; interface QueryTemplatesListProps extends QueryLibraryProps {} export function QueryTemplatesList(props: QueryTemplatesListProps) { - const { - data: rawData, - isLoading, - error, - } = useListQueryTemplateQuery({ - namespace: getK8sNamespace(), - }); + const { data: rawData, isLoading, error } = useListQueryTemplateQuery({}); const data = useMemo(() => (rawData ? convertDataQueryResponseToQueryTemplates(rawData) : undefined), [rawData]); const [isModalOpen, setIsModalOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx index 7390f030338..88174485be1 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx @@ -9,7 +9,6 @@ import { useDeleteQueryTemplateMutation } from 'app/features/query-library'; import { dispatch } from 'app/store/store'; import { ShowConfirmModalEvent } from 'app/types/events'; -import { getK8sNamespace } from '../../../query-library/api/query'; import ExploreRunQueryButton from '../../ExploreRunQueryButton'; import { useQueriesDrawerContext } from '../../QueriesDrawer/QueriesDrawerContext'; import { @@ -38,8 +37,7 @@ function ActionsCell({ queryTemplate, rootDatasourceUid, queryUid }: ActionsCell const performDelete = (queryUid: string) => { deleteQueryTemplate({ name: queryUid, - namespace: getK8sNamespace(), - ioK8SApimachineryPkgApisMetaV1DeleteOptions: {}, + deleteOptions: {}, }); dispatch(notifyApp(createSuccessNotification(t('explore.query-library.query-deleted', 'Query deleted')))); queryLibaryTrackDeleteQuery(); diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index 38a7cb66dcb..b614401cde6 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -17,7 +17,6 @@ import { RichHistoryQuery } from 'app/types/explore'; import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider'; import { useListQueryTemplateQuery } from '../../query-library'; -import { getK8sNamespace } from '../../query-library/api/query'; import { Tabs, useQueriesDrawerContext } from '../QueriesDrawer/QueriesDrawerContext'; import { i18n } from '../QueriesDrawer/utils'; import { QueryLibrary } from '../QueryLibrary/QueryLibrary'; @@ -99,9 +98,7 @@ export function RichHistory(props: RichHistoryProps) { .map((eDs) => listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name) .filter((name): name is string => !!name); - const { data } = useListQueryTemplateQuery({ - namespace: getK8sNamespace(), - }); + const { data } = useListQueryTemplateQuery({}); const queryTemplatesCount = data?.items?.length ?? 0; const QueryLibraryTab: TabConfig = { diff --git a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx index f735180c11a..e6aa0e25143 100644 --- a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx @@ -5,7 +5,6 @@ import { Button, Modal } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { isQueryLibraryEnabled, useListQueryTemplateQuery } from 'app/features/query-library'; -import { getK8sNamespace } from '../../query-library/api/query'; import { queryLibraryTrackAddFromQueryHistory, queryLibraryTrackAddFromQueryHistoryAddModalShown, @@ -17,9 +16,7 @@ type Props = { }; export const RichHistoryAddToLibrary = ({ query }: Props) => { - const { refetch } = useListQueryTemplateQuery({ - namespace: getK8sNamespace(), - }); + const { refetch } = useListQueryTemplateQuery({}); const [isOpen, setIsOpen] = useState(false); const [hasBeenSaved, setHasBeenSaved] = useState(false); diff --git a/public/app/features/query-library/api/endpoints.gen.ts b/public/app/features/query-library/api/endpoints.gen.ts index 9d7a680ed24..133cad6a83e 100644 --- a/public/app/features/query-library/api/endpoints.gen.ts +++ b/public/app/features/query-library/api/endpoints.gen.ts @@ -8,7 +8,7 @@ const injectedRtkApi = api endpoints: (build) => ({ listQueryTemplate: build.query({ query: (queryArg) => ({ - url: `/apis/peakq.grafana.app/v0alpha1/namespaces/${queryArg['namespace']}/querytemplates`, + url: `/querytemplates`, params: { pretty: queryArg.pretty, allowWatchBookmarks: queryArg.allowWatchBookmarks, @@ -27,9 +27,9 @@ const injectedRtkApi = api }), createQueryTemplate: build.mutation({ query: (queryArg) => ({ - url: `/apis/peakq.grafana.app/v0alpha1/namespaces/${queryArg['namespace']}/querytemplates`, + url: `/querytemplates`, method: 'POST', - body: queryArg.comGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate, + body: queryArg.queryTemplate, params: { pretty: queryArg.pretty, dryRun: queryArg.dryRun, @@ -41,9 +41,9 @@ const injectedRtkApi = api }), deleteQueryTemplate: build.mutation({ query: (queryArg) => ({ - url: `/apis/peakq.grafana.app/v0alpha1/namespaces/${queryArg['namespace']}/querytemplates/${queryArg.name}`, + url: `/querytemplates/${queryArg.name}`, method: 'DELETE', - body: queryArg.ioK8SApimachineryPkgApisMetaV1DeleteOptions, + body: queryArg.deleteOptions, params: { pretty: queryArg.pretty, dryRun: queryArg.dryRun, @@ -57,9 +57,9 @@ const injectedRtkApi = api }), updateQueryTemplate: build.mutation({ query: (queryArg) => ({ - url: `/apis/peakq.grafana.app/v0alpha1/namespaces/${queryArg['namespace']}/querytemplates/${queryArg.name}`, + url: `/querytemplates/${queryArg.name}`, method: 'PATCH', - body: queryArg.ioK8SApimachineryPkgApisMetaV1Patch, + body: queryArg.patch, params: { pretty: queryArg.pretty, dryRun: queryArg.dryRun, @@ -74,11 +74,8 @@ const injectedRtkApi = api overrideExisting: false, }); export { injectedRtkApi as generatedQueryLibraryApi }; -export type ListQueryTemplateApiResponse = - /** status 200 OK */ ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplateList; +export type ListQueryTemplateApiResponse = /** status 200 OK */ QueryTemplateList; export type ListQueryTemplateApiArg = { - /** object name and auth scope, such as for teams and projects */ - namespace: string; /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ pretty?: string; /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ @@ -123,12 +120,10 @@ export type ListQueryTemplateApiArg = { watch?: boolean; }; export type CreateQueryTemplateApiResponse = /** status 200 OK */ - | ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate - | /** status 201 Created */ ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate - | /** status 202 Accepted */ ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate; + | QueryTemplate + | /** status 201 Created */ QueryTemplate + | /** status 202 Accepted */ QueryTemplate; export type CreateQueryTemplateApiArg = { - /** object name and auth scope, such as for teams and projects */ - namespace: string; /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ pretty?: string; /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ @@ -137,16 +132,12 @@ export type CreateQueryTemplateApiArg = { fieldManager?: string; /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ fieldValidation?: string; - comGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate: ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate; + queryTemplate: QueryTemplate; }; -export type DeleteQueryTemplateApiResponse = /** status 200 OK */ - | IoK8SApimachineryPkgApisMetaV1Status - | /** status 202 Accepted */ IoK8SApimachineryPkgApisMetaV1Status; +export type DeleteQueryTemplateApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; export type DeleteQueryTemplateApiArg = { /** name of the QueryTemplate */ name: string; - /** object name and auth scope, such as for teams and projects */ - namespace: string; /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ pretty?: string; /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ @@ -159,16 +150,14 @@ export type DeleteQueryTemplateApiArg = { orphanDependents?: boolean; /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ propagationPolicy?: string; - ioK8SApimachineryPkgApisMetaV1DeleteOptions: IoK8SApimachineryPkgApisMetaV1DeleteOptions; + deleteOptions: DeleteOptions; }; export type UpdateQueryTemplateApiResponse = /** status 200 OK */ - | ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate - | /** status 201 Created */ ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate; + | QueryTemplate + | /** status 201 Created */ QueryTemplate; export type UpdateQueryTemplateApiArg = { /** name of the QueryTemplate */ name: string; - /** object name and auth scope, such as for teams and projects */ - namespace: string; /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ pretty?: string; /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ @@ -179,17 +168,17 @@ export type UpdateQueryTemplateApiArg = { fieldValidation?: string; /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ force?: boolean; - ioK8SApimachineryPkgApisMetaV1Patch: IoK8SApimachineryPkgApisMetaV1Patch; + patch: Patch; }; -export type IoK8SApimachineryPkgApisMetaV1Time = string; -export type IoK8SApimachineryPkgApisMetaV1FieldsV1 = object; -export type IoK8SApimachineryPkgApisMetaV1ManagedFieldsEntry = { +export type Time = string; +export type FieldsV1 = object; +export type ManagedFieldsEntry = { /** APIVersion defines the version of this resource that this field set applies to. The format is "group/version" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted. */ apiVersion?: string; /** FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: "FieldsV1" */ fieldsType?: string; /** FieldsV1 holds the first JSON version format as described in the "FieldsV1" type. */ - fieldsV1?: IoK8SApimachineryPkgApisMetaV1FieldsV1; + fieldsV1?: FieldsV1; /** Manager is an identifier of the workflow managing these fields. */ manager?: string; /** Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'. */ @@ -197,9 +186,9 @@ export type IoK8SApimachineryPkgApisMetaV1ManagedFieldsEntry = { /** Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource. */ subresource?: string; /** Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over. */ - time?: IoK8SApimachineryPkgApisMetaV1Time; + time?: Time; }; -export type IoK8SApimachineryPkgApisMetaV1OwnerReference = { +export type OwnerReference = { /** API version of the referent. */ apiVersion: string; /** If true, AND if the owner has the "foregroundDeletion" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs "delete" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned. */ @@ -213,7 +202,7 @@ export type IoK8SApimachineryPkgApisMetaV1OwnerReference = { /** UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ uid: string; }; -export type IoK8SApimachineryPkgApisMetaV1ObjectMeta = { +export type ObjectMeta = { /** Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations */ annotations?: { [key: string]: string; @@ -221,13 +210,13 @@ export type IoK8SApimachineryPkgApisMetaV1ObjectMeta = { /** CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ - creationTimestamp?: IoK8SApimachineryPkgApisMetaV1Time; + creationTimestamp?: Time; /** Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only. */ deletionGracePeriodSeconds?: number; /** DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested. Populated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ - deletionTimestamp?: IoK8SApimachineryPkgApisMetaV1Time; + deletionTimestamp?: Time; /** Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list. */ finalizers?: string[]; /** GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server. @@ -243,7 +232,7 @@ export type IoK8SApimachineryPkgApisMetaV1ObjectMeta = { [key: string]: string; }; /** ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like "ci-cd". The set of fields is always in the version that the workflow used when modifying the object. */ - managedFields?: IoK8SApimachineryPkgApisMetaV1ManagedFieldsEntry[]; + managedFields?: ManagedFieldsEntry[]; /** Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ name?: string; /** Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty. @@ -251,7 +240,7 @@ export type IoK8SApimachineryPkgApisMetaV1ObjectMeta = { Must be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces */ namespace?: string; /** List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller. */ - ownerReferences?: IoK8SApimachineryPkgApisMetaV1OwnerReference[]; + ownerReferences?: OwnerReference[]; /** An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources. Populated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */ @@ -263,7 +252,7 @@ export type IoK8SApimachineryPkgApisMetaV1ObjectMeta = { Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ uid?: string; }; -export type ComGithubGrafanaGrafanaPluginSdkGoExperimentalApisDataV0Alpha1DataQuery = { +export type DataQuery = { /** The datasource */ datasource?: { /** The apiserver version */ @@ -336,13 +325,13 @@ export type ComGithubGrafanaGrafanaPluginSdkGoExperimentalApisDataV0Alpha1DataQu }; [key: string]: any; }; -export type ComGithubGrafanaGrafanaPkgApisQueryV0Alpha1TemplatePosition = { +export type TemplatePosition = { /** End is the byte offset of the end of the variable. */ end: number; /** Start is the byte offset within TargetKey's property of the variable. It is the start location for replacements). */ start: number; }; -export type ComGithubGrafanaGrafanaPkgApisQueryV0Alpha1TemplateVariableReplacement = { +export type TemplateVariableReplacement = { /** How values should be interpolated Possible enum values: @@ -356,48 +345,48 @@ export type ComGithubGrafanaGrafanaPkgApisQueryV0Alpha1TemplateVariableReplaceme /** Path is the location of the property within a target. The format for this is not figured out yet (Maybe JSONPath?). Idea: ["string", int, "string"] where int indicates array offset */ path: string; /** Positions is a list of where to perform the interpolation within targets during render. The first string is the Idx of the target as a string, since openAPI does not support ints as map keys */ - position?: ComGithubGrafanaGrafanaPkgApisQueryV0Alpha1TemplatePosition; + position?: TemplatePosition; }; -export type ComGithubGrafanaGrafanaPkgApisQueryV0Alpha1TemplateTarget = { +export type TemplateTarget = { /** DataType is the returned Dataplane type from the query. */ dataType?: string; /** Query target */ - properties: ComGithubGrafanaGrafanaPluginSdkGoExperimentalApisDataV0Alpha1DataQuery; + properties: DataQuery; /** Variables that will be replaced in the query */ variables: { - [key: string]: ComGithubGrafanaGrafanaPkgApisQueryV0Alpha1TemplateVariableReplacement[]; + [key: string]: TemplateVariableReplacement[]; }; }; -export type ComGithubGrafanaGrafanaPkgApimachineryApisCommonV0Alpha1Unstructured = { +export type Unstructured = { [key: string]: any; }; -export type ComGithubGrafanaGrafanaPkgApisQueryV0Alpha1TemplateTemplateVariable = { +export type TemplateTemplateVariable = { /** DefaultValue is the value to be used when there is no selected value during render. */ defaultValues?: string[]; /** Key is the name of the variable. */ key: string; /** ValueListDefinition is the object definition used by the FE to get a list of possible values to select for render. */ - valueListDefinition?: ComGithubGrafanaGrafanaPkgApimachineryApisCommonV0Alpha1Unstructured; + valueListDefinition?: Unstructured; }; -export type ComGithubGrafanaGrafanaPkgApisQueryV0Alpha1TemplateQueryTemplate = { +export type TemplateQueryTemplate = { /** Longer description for why it is interesting */ description?: string; /** Output variables */ - targets: ComGithubGrafanaGrafanaPkgApisQueryV0Alpha1TemplateTarget[]; + targets: TemplateTarget[]; /** A display name */ title?: string; /** The variables that can be used to render */ - vars?: ComGithubGrafanaGrafanaPkgApisQueryV0Alpha1TemplateTemplateVariable[]; + vars?: TemplateTemplateVariable[]; }; -export type ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate = { +export type QueryTemplate = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: IoK8SApimachineryPkgApisMetaV1ObjectMeta; - spec?: ComGithubGrafanaGrafanaPkgApisQueryV0Alpha1TemplateQueryTemplate; + metadata?: ObjectMeta; + spec?: TemplateQueryTemplate; }; -export type IoK8SApimachineryPkgApisMetaV1ListMeta = { +export type ListMeta = { /** continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. */ continue?: string; /** remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact. */ @@ -407,15 +396,15 @@ export type IoK8SApimachineryPkgApisMetaV1ListMeta = { /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ selfLink?: string; }; -export type ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplateList = { +export type QueryTemplateList = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; - items?: ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate[]; + items?: QueryTemplate[]; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: IoK8SApimachineryPkgApisMetaV1ListMeta; + metadata?: ListMeta; }; -export type IoK8SApimachineryPkgApisMetaV1StatusCause = { +export type StatusCause = { /** The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional. Examples: @@ -427,9 +416,9 @@ export type IoK8SApimachineryPkgApisMetaV1StatusCause = { /** A machine-readable description of the cause of the error. If this value is empty there is no information available. */ reason?: string; }; -export type IoK8SApimachineryPkgApisMetaV1StatusDetails = { +export type StatusDetails = { /** The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes. */ - causes?: IoK8SApimachineryPkgApisMetaV1StatusCause[]; + causes?: StatusCause[]; /** The group attribute of the resource associated with the status StatusReason. */ group?: string; /** The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ @@ -441,31 +430,31 @@ export type IoK8SApimachineryPkgApisMetaV1StatusDetails = { /** UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ uid?: string; }; -export type IoK8SApimachineryPkgApisMetaV1Status = { +export type Status = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; /** Suggested HTTP return code for this status, 0 if not set. */ code?: number; /** Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type. */ - details?: IoK8SApimachineryPkgApisMetaV1StatusDetails; + details?: StatusDetails; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; /** A human-readable description of the status of this operation. */ message?: string; /** Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - metadata?: IoK8SApimachineryPkgApisMetaV1ListMeta; + metadata?: ListMeta; /** A machine-readable description of why this operation is in the "Failure" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it. */ reason?: string; /** Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status */ status?: string; }; -export type IoK8SApimachineryPkgApisMetaV1Preconditions = { +export type Preconditions = { /** Specifies the target ResourceVersion */ resourceVersion?: string; /** Specifies the target UID. */ uid?: string; }; -export type IoK8SApimachineryPkgApisMetaV1DeleteOptions = { +export type DeleteOptions = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ @@ -479,8 +468,8 @@ export type IoK8SApimachineryPkgApisMetaV1DeleteOptions = { /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ orphanDependents?: boolean; /** Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned. */ - preconditions?: IoK8SApimachineryPkgApisMetaV1Preconditions; + preconditions?: Preconditions; /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ propagationPolicy?: string; }; -export type IoK8SApimachineryPkgApisMetaV1Patch = object; +export type Patch = object; diff --git a/public/app/features/query-library/api/mappers.ts b/public/app/features/query-library/api/mappers.ts index 01b3f870e2e..2d95b48465f 100644 --- a/public/app/features/query-library/api/mappers.ts +++ b/public/app/features/query-library/api/mappers.ts @@ -2,10 +2,7 @@ import { v4 as uuidv4 } from 'uuid'; import { AddQueryTemplateCommand, QueryTemplate } from '../types'; -import { - ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate, - ListQueryTemplateApiResponse, -} from './endpoints.gen'; +import { ListQueryTemplateApiResponse, QueryTemplate as QT } from './endpoints.gen'; import { API_VERSION, QueryTemplateKinds } from './query'; import { CREATED_BY_KEY } from './types'; @@ -30,9 +27,7 @@ export const convertDataQueryResponseToQueryTemplates = (result: ListQueryTempla }); }; -export const convertAddQueryTemplateCommandToDataQuerySpec = ( - addQueryTemplateCommand: AddQueryTemplateCommand -): ComGithubGrafanaGrafanaPkgApisPeakqV0Alpha1QueryTemplate => { +export const convertAddQueryTemplateCommandToDataQuerySpec = (addQueryTemplateCommand: AddQueryTemplateCommand): QT => { const { title, targets } = addQueryTemplateCommand; return { apiVersion: API_VERSION, diff --git a/public/app/features/query-library/api/query.ts b/public/app/features/query-library/api/query.ts index 1de6d303408..c2ce2bea47c 100644 --- a/public/app/features/query-library/api/query.ts +++ b/public/app/features/query-library/api/query.ts @@ -23,7 +23,7 @@ export const getK8sNamespace = () => config.namespace; * * @alpha */ -export const BASE_URL = `/apis/${API_VERSION}/namespaces/${getK8sNamespace()}/querytemplates`; +export const BASE_URL = `/apis/${API_VERSION}/namespaces/${getK8sNamespace()}`; interface QueryLibraryBackendRequest extends BackendSrvRequest { body?: BackendSrvRequest['data']; @@ -32,7 +32,7 @@ interface QueryLibraryBackendRequest extends BackendSrvRequest { export const baseQuery: BaseQueryFn = async (requestOptions) => { try { const responseObservable = getBackendSrv().fetch({ - url: `${requestOptions.url ?? ''}`, + url: `${BASE_URL}/${requestOptions.url ?? ''}`, showErrorAlert: true, method: requestOptions.method || 'GET', data: requestOptions.body, diff --git a/public/app/features/query-library/api/scripts/generate-rtk-apis.ts b/public/app/features/query-library/api/scripts/generate-rtk-apis.ts index 8b1c90b60c6..1db96f698ea 100644 --- a/public/app/features/query-library/api/scripts/generate-rtk-apis.ts +++ b/public/app/features/query-library/api/scripts/generate-rtk-apis.ts @@ -1,12 +1,12 @@ /** * To generate query library k8s APIs, run: - * `npx rtk-query-codegen-openapi ./public/app/features/query-library/api/scripts/generate-rtk-apis.ts` from the root of the repo + * `yarn process-specs && npx rtk-query-codegen-openapi ./public/app/features/query-library/api/scripts/generate-rtk-apis.ts` from the root of the repo */ import { ConfigFile } from '@rtk-query/codegen-openapi'; import { accessSync } from 'fs'; -const schemaFile = '../../../../../../data/query-library/openapi.json'; +const schemaFile = '../../../../../../data/openapi/peakq.grafana.app-v0alpha1.json'; try { // Check we have the OpenAPI before generating query library RTK APIs, @@ -15,7 +15,7 @@ try { } catch (e) { console.error('\nCould not find OpenAPI definition.\n'); console.error( - 'Please visit /openapi/v3/apis/peakq.grafana.app/v0alpha1 and save the OpenAPI definition to data/query-library/openapi.json\n' + 'Please run go test pkg/tests/apis/openapi_test.go to generate the OpenAPI definition, then try running this script again.\n' ); throw e; } diff --git a/scripts/process-specs.ts b/scripts/process-specs.ts new file mode 100644 index 00000000000..b101f80b74e --- /dev/null +++ b/scripts/process-specs.ts @@ -0,0 +1,143 @@ +import fs from 'fs'; +import { OpenAPIV3 } from 'openapi-types'; +import path from 'path'; + +/** + * Process an OpenAPI spec to remove k8s metadata from names and paths: + * - Remove paths containing "/watch/" as they're deprecated. + * - Remove 'ForAllNamespaces' endpoints + * - Remove the prefix: "/apis///namespaces/{namespace}" from paths. + * - Filter out `namespace` from path parameters. + * - Update all $ref fields to remove k8s metadata from schema names. + * - Simplify schema names in "components.schemas". + */ +function processOpenAPISpec(spec: OpenAPIV3.Document) { + // Create a deep copy of the spec to avoid mutating the original + const newSpec = JSON.parse(JSON.stringify(spec)); + + // Process 'paths' property + const newPaths: Record = {}; + for (const [path, pathItem] of Object.entries(newSpec.paths)) { + // Remove 'watch' paths as they're deprecated / remove empty path items + if (path.includes('/watch/') || !pathItem) { + continue; + } + // Remove the specified part from the path key + const newPathKey = path.replace(/^\/apis\/[^\/]+\/[^\/]+\/namespaces\/\{namespace}/, ''); + + // Process each method in the path (e.g., get, post) + const newPathItem: Record = {}; + for (const method of Object.keys(pathItem)) { + // Filter out the 'namespace' param + if (method === 'parameters' && Array.isArray(pathItem.parameters)) { + pathItem.parameters = pathItem.parameters?.filter((param) => 'name' in param && param.name !== 'namespace'); + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const operation = pathItem[method as keyof OpenAPIV3.PathItemObject]; + + if ( + typeof operation === 'object' && + operation !== null && + 'operationId' in operation && + operation.operationId?.includes('ForAllNamespaces') + ) { + continue; + } + + updateRefs(operation); + + newPathItem[method] = operation; + } + + newPaths[newPathKey] = newPathItem; + } + newSpec.paths = newPaths; + + // Process 'components.schemas', i.e., type definitions + const newSchemas: Record = {}; + for (const schemaKey of Object.keys(newSpec.components.schemas)) { + const newKey = simplifySchemaName(schemaKey); + + const schemaObject = newSpec.components.schemas[schemaKey]; + updateRefs(schemaObject); + + newSchemas[newKey] = schemaObject; + } + newSpec.components.schemas = newSchemas; + + return newSpec; +} + +/** + * Recursively update all $ref fields to remove k8s metadata from names + */ +function updateRefs(obj: unknown) { + if (Array.isArray(obj)) { + for (const item of obj) { + updateRefs(item); + } + } else if (typeof obj === 'object' && obj !== null) { + if ('$ref' in obj && typeof obj.$ref === 'string') { + const refParts = obj.$ref.split('/'); + const lastRefPart = refParts[refParts.length - 1]; + const newRefName = simplifySchemaName(lastRefPart); + obj.$ref = `#/components/schemas/${newRefName}`; + } + for (const key in obj) { + if (key !== '$ref') { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + updateRefs(obj[key as keyof typeof obj]); + } + } + } +} + +/** + * Simplify a schema name by removing the version prefix if present. + * For example, 'io.k8s.apimachinery.pkg.apis.meta.v1.Time' becomes 'Time'. + */ +function simplifySchemaName(schemaName: string) { + const parts = schemaName.split('.'); + + // Regex to match version segments like 'v1', 'v1beta1', 'v0alpha1', etc. + const versionRegex = /^v\d+[a-zA-Z0-9]*$/; + const versionIndex = parts.findIndex((part) => versionRegex.test(part)); + + if (versionIndex !== -1 && versionIndex + 1 < parts.length) { + return parts.slice(versionIndex + 1).join('.'); + } else { + return schemaName; + } +} + +const sourceDir = path.resolve(__dirname, '../pkg/tests/apis/openapi_snapshots'); +const outputDir = path.resolve(__dirname, '../data/openapi'); + +// Create the output directory if it doesn't exist +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); +} + +const files = fs.readdirSync(sourceDir).filter((file: string) => file.endsWith('.json')); + +for (const file of files) { + const inputPath = path.join(sourceDir, file); + const outputPath = path.join(outputDir, file); + + console.log(`Processing file "${file}"...`); + + const fileContent = fs.readFileSync(inputPath, 'utf-8'); + + let inputSpec; + try { + inputSpec = JSON.parse(fileContent); + } catch (err) { + console.error(`Invalid JSON file "${file}". Skipping this file.`); + continue; + } + + const outputSpec = processOpenAPISpec(inputSpec); + fs.writeFileSync(outputPath, JSON.stringify(outputSpec, null, 2), 'utf-8'); + console.log(`Processing completed for file "${file}".`); +} diff --git a/yarn.lock b/yarn.lock index 1a4a877ad45..28181b0a8fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18013,6 +18013,7 @@ __metadata: nx: "npm:19.8.2" ol: "npm:7.4.0" ol-ext: "npm:4.0.26" + openapi-types: "npm:^12.1.3" pluralize: "npm:^8.0.0" postcss: "npm:8.5.1" postcss-loader: "npm:8.1.1" @@ -23503,6 +23504,13 @@ __metadata: languageName: node linkType: hard +"openapi-types@npm:^12.1.3": + version: 12.1.3 + resolution: "openapi-types@npm:12.1.3" + checksum: 10/9d1d7ed848622b63d0a4c3f881689161b99427133054e46b8e3241e137f1c78bb0031c5d80b420ee79ac2e91d2e727ffd6fc13c553d1b0488ddc8ad389dcbef8 + languageName: node + linkType: hard + "opener@npm:^1.5.1, opener@npm:^1.5.2": version: 1.5.2 resolution: "opener@npm:1.5.2" From be9a7ce9089e94e4f8a8fe1a6e6416ab3288d324 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:12:47 +0000 Subject: [PATCH 205/894] Update dependency knip to v5.43.6 (#99766) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 28181b0a8fa..b315ac4d2b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20983,8 +20983,8 @@ __metadata: linkType: hard "knip@npm:^5.10.0": - version: 5.43.1 - resolution: "knip@npm:5.43.1" + version: 5.43.6 + resolution: "knip@npm:5.43.6" dependencies: "@nodelib/fs.walk": "npm:3.0.1" "@snyk/github-codeowners": "npm:1.1.0" @@ -21008,7 +21008,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10/068e4145371cf3a4434d07a206eddf8f1d509541482d76252440484562f0b989c11c3efb9c4083d8b5854a90758d3bbcc4a228fe935f6e90ecc9ef2c9f9da8a7 + checksum: 10/d843ed0f5b56baf5c29257308b0cf1956348cfa9d2b9b627420db023a6ccdaf54450f047fe900b263dc10291f395bc3eceef221dc6050e7fd55fb1fbe4fce3a2 languageName: node linkType: hard From 07601bee6f0a8bcb536880361a1a18559639feb0 Mon Sep 17 00:00:00 2001 From: Scott Lepper Date: Wed, 29 Jan 2025 11:48:19 -0500 Subject: [PATCH 206/894] [search] title search wildcard (#99769) --- pkg/services/dashboards/service/dashboard_service.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index d56118429d2..bc193b82b18 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1699,17 +1699,9 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex request.Options.Fields = append(request.Options.Fields, req...) } - // note: this does not allow for partial matching - // - // partial matching will be allowed through the api layer for the frontend, - // but is currently not needed by other services in the backend if query.Title != "" { - req := []*resource.Requirement{{ - Key: resource.SEARCH_FIELD_TITLE_SORT, // use title sort to prevent issues with `-` in the title & how bleve searches - Operator: string(selection.In), - Values: []string{strings.ToLower(query.Title)}, - }} - request.Options.Fields = append(request.Options.Fields, req...) + // allow wildcard search + request.Query = "*" + strings.ToLower(query.Title) + "*" } if len(query.Tags) > 0 { From 8f60308e7383a005fca341eb30c059920a5fc3dc Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 29 Jan 2025 17:29:16 +0000 Subject: [PATCH 207/894] Chore: centralise `getK8sNamespace` (#99767) * create new public/app/api folder, expose getK8sNamespace * rename to getAPINamespace --- .github/CODEOWNERS | 1 + public/app/api/utils.ts | 3 +++ .../contact-points/useContactPoints.ts | 17 +++++++---------- .../contact-points/useNotificationTemplates.ts | 13 +++++++------ .../components/mute-timings/useMuteTimings.tsx | 17 +++++++---------- .../useNotificationPolicyRoute.ts | 11 ++++++----- .../features/alerting/unified/home/Insights.tsx | 4 ++-- .../alerting/unified/utils/k8s/utils.ts | 5 ----- public/app/features/apiserver/client.ts | 4 +++- .../features/dashboard/services/SnapshotSrv.ts | 4 +++- public/app/features/query-library/api/query.ts | 7 +++---- public/app/features/query-library/api/user.ts | 5 +++-- public/app/features/scopes/internal/api.ts | 6 ++++-- public/app/features/search/service/unified.ts | 4 +++- 14 files changed, 52 insertions(+), 49 deletions(-) create mode 100644 public/app/api/utils.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 82604409275..1f5d5f25b5e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -455,6 +455,7 @@ playwright.config.ts @grafana/plugins-platform-frontend # public folder +/public/app/api/ @grafana/grafana-frontend-platform /public/app/core/ @grafana/grafana-frontend-platform /public/app/core/components/TimePicker/ @grafana/grafana-frontend-platform /public/app/core/components/Layers/ @grafana/dataviz-squad diff --git a/public/app/api/utils.ts b/public/app/api/utils.ts new file mode 100644 index 00000000000..51ba395ad43 --- /dev/null +++ b/public/app/api/utils.ts @@ -0,0 +1,3 @@ +import { config } from '@grafana/runtime'; + +export const getAPINamespace = () => config.namespace; diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts b/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts index a052f34013e..314a64aa852 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts @@ -12,17 +12,14 @@ import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } f import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks'; import { cloudNotifierTypes } from 'app/features/alerting/unified/utils/cloud-alertmanager-notifier-types'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; -import { - getK8sNamespace, - isK8sEntityProvisioned, - shouldUseK8sApi, -} from 'app/features/alerting/unified/utils/k8s/utils'; +import { isK8sEntityProvisioned, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; import { GrafanaManagedContactPoint, GrafanaManagedReceiverConfig, Receiver, } from 'app/plugins/datasource/alertmanager/types'; +import { getAPINamespace } from '../../../../../api/utils'; import { alertmanagerApi } from '../../api/alertmanagerApi'; import { onCallApi } from '../../api/onCallApi'; import { useAsync } from '../../hooks/useAsync'; @@ -117,7 +114,7 @@ const useK8sContactPoints = (...[hookParams, queryOptions]: Parameters { - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); const useK8sApi = shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME); const grafanaResponse = useGetContactPointsListQuery(undefined, { @@ -239,7 +236,7 @@ const useGetGrafanaContactPoint = ( { name }: { name: string }, queryOptions?: Parameters[1] ) => { - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); const useK8sApi = shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME); const k8sResponse = useReadNamespacedReceiverQuery( @@ -314,7 +311,7 @@ export function useDeleteContactPoint({ alertmanager }: BaseAlertmanagerArgs) { const [deleteReceiver] = useDeleteNamespacedReceiverMutation(); const deleteFromK8sAPI = useAsync(async ({ name, resourceVersion }: DeleteContactPointArgs) => { - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); await deleteReceiver({ name, namespace, @@ -407,7 +404,7 @@ export const useCreateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => ? await createOnCallIntegrations(contactPoint) : contactPoint; - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); const contactPointToUse = grafanaContactPointToK8sReceiver(contactPointWithMaybeOnCall); return createGrafanaContactPoint({ @@ -455,7 +452,7 @@ export const useUpdateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => ? await createOnCallIntegrations(contactPoint) : contactPoint; - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); const contactPointToUse = grafanaContactPointToK8sReceiver(receiverWithPotentialOnCall, id, resourceVersion); return replaceGrafanaContactPoint({ diff --git a/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts index 160a28001a6..91739aeca61 100644 --- a/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts +++ b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts @@ -1,6 +1,7 @@ import { useEffect } from 'react'; import { Validate } from 'react-hook-form'; +import { getAPINamespace } from '../../../../../api/utils'; import { AlertManagerCortexConfig } from '../../../../../plugins/datasource/alertmanager/types'; import { alertmanagerApi } from '../../api/alertmanagerApi'; import { templatesApi } from '../../api/templateApi'; @@ -16,7 +17,7 @@ import { updateNotificationTemplateAction, } from '../../reducers/alertmanager/notificationTemplates'; import { K8sAnnotations, PROVENANCE_NONE } from '../../utils/k8s/constants'; -import { getAnnotation, getK8sNamespace, shouldUseK8sApi } from '../../utils/k8s/utils'; +import { getAnnotation, shouldUseK8sApi } from '../../utils/k8s/utils'; import { ensureDefine } from '../../utils/templates'; import { TemplateFormValues } from '../receivers/TemplateForm'; @@ -46,7 +47,7 @@ export function useNotificationTemplates({ alertmanager }: BaseAlertmanagerArgs) const k8sApiSupported = shouldUseK8sApi(alertmanager); const k8sApiTemplatesRequestState = useListNamespacedTemplateGroupQuery( - { namespace: getK8sNamespace() }, + { namespace: getAPINamespace() }, { skip: !k8sApiSupported, selectFromResult: (state) => ({ @@ -131,7 +132,7 @@ export function useGetNotificationTemplate({ alertmanager, uid }: GetTemplatePar // What are pros and cons of each? useEffect(() => { if (k8sApiSupported) { - fetchTemplate({ namespace: getK8sNamespace(), name: uid }); + fetchTemplate({ namespace: getAPINamespace(), name: uid }); } else { fetchAmConfig(alertmanager); } @@ -164,7 +165,7 @@ export function useCreateNotificationTemplate({ alertmanager }: BaseAlertmanager const content = ensureDefine(templateValues.title, templateValues.content); return createNamespacedTemplateGroup({ - namespace: getK8sNamespace(), + namespace: getAPINamespace(), comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup: { spec: { title: templateValues.title, content }, metadata: {}, @@ -195,7 +196,7 @@ export function useUpdateNotificationTemplate({ alertmanager }: BaseAlertmanager const content = ensureDefine(patch.title, patch.content); return replaceNamespacedTemplateGroup({ - namespace: getK8sNamespace(), + namespace: getAPINamespace(), name: template.uid, comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup: { spec: { title: patch.title, content }, @@ -218,7 +219,7 @@ export function useDeleteNotificationTemplate({ alertmanager }: BaseAlertmanager const deleteUsingK8sApi = useAsync(({ uid }: { uid: string }) => { return deleteNamespacedTemplateGroup({ - namespace: getK8sNamespace(), + namespace: getAPINamespace(), name: uid, ioK8SApimachineryPkgApisMetaV1DeleteOptions: {}, }).unwrap(); diff --git a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx index dd365218c47..fb873f967b3 100644 --- a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx @@ -10,13 +10,10 @@ import { import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; -import { - getK8sNamespace, - isK8sEntityProvisioned, - shouldUseK8sApi, -} from 'app/features/alerting/unified/utils/k8s/utils'; +import { isK8sEntityProvisioned, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; +import { getAPINamespace } from '../../../../../api/utils'; import { useAsync } from '../../hooks/useAsync'; import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig'; import { @@ -114,7 +111,7 @@ export const useMuteTimings = ({ alertmanager, skip }: BaseAlertmanagerArgs & Sk return; } if (useK8sApi) { - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); getGrafanaTimeIntervals({ namespace }); } else { getAlertmanagerTimeIntervals(alertmanager); @@ -140,7 +137,7 @@ export const useCreateMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { const [updateConfiguration] = useProduceNewAlertmanagerConfiguration(); const addToK8sAPI = useAsync(({ interval }: CreateUpdateMuteTimingArgs) => { - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); return createGrafanaTimeInterval({ namespace, @@ -202,7 +199,7 @@ export const useGetMuteTiming = ({ alertmanager, name: nameToFind }: BaseAlertma useEffect(() => { if (useK8sApi) { - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); getGrafanaTimeInterval({ namespace, fieldSelector: `spec.name=${nameToFind}` }, true); } else { getAlertmanagerTimeInterval(alertmanager, true); @@ -228,7 +225,7 @@ export const useUpdateMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { const updateToK8sAPI = useAsync( async ({ interval, originalName }: CreateUpdateMuteTimingArgs & { originalName: string }) => { - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); return replaceGrafanaTimeInterval({ name: originalName, @@ -267,7 +264,7 @@ export const useDeleteMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { }); const deleteFromK8sAPI = useAsync(async ({ name }: DeleteMuteTimingArgs) => { - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); await deleteGrafanaTimeInterval({ name, namespace, diff --git a/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts b/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts index 4b1e2243c8e..f927fc49d70 100644 --- a/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts +++ b/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts @@ -4,6 +4,7 @@ import memoize from 'micro-memoize'; import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks'; import { MatcherOperator, ROUTES_META_SYMBOL, Route } from 'app/plugins/datasource/alertmanager/types'; +import { getAPINamespace } from '../../../../../api/utils'; import { alertmanagerApi } from '../../api/alertmanagerApi'; import { useAsync } from '../../hooks/useAsync'; import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig'; @@ -21,7 +22,7 @@ import { import { FormAmRoute } from '../../types/amroutes'; import { addUniqueIdentifierToRoute } from '../../utils/amroutes'; import { PROVENANCE_NONE, ROOT_ROUTE_NAME } from '../../utils/k8s/constants'; -import { getK8sNamespace, isK8sEntityProvisioned, shouldUseK8sApi } from '../../utils/k8s/utils'; +import { isK8sEntityProvisioned, shouldUseK8sApi } from '../../utils/k8s/utils'; import { INHERITABLE_KEYS, InheritableProperties } from '../../utils/notification-policies'; import { InsertPosition, @@ -45,7 +46,7 @@ export const useNotificationPolicyRoute = ({ alertmanager }: BaseAlertmanagerArg const k8sApiSupported = shouldUseK8sApi(alertmanager); const k8sRouteQuery = useListNamespacedRoutingTreeQuery( - { namespace: getK8sNamespace() }, + { namespace: getAPINamespace() }, { skip: skip || !k8sApiSupported, selectFromResult: (result) => { @@ -92,7 +93,7 @@ export function useUpdateExistingNotificationPolicy({ alertmanager }: BaseAlertm const [listNamespacedRoutingTree] = useLazyListNamespacedRoutingTreeQuery(); const updateUsingK8sApi = useAsync(async (update: Partial) => { - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); const result = await listNamespacedRoutingTree({ namespace }); const [rootTree] = result.data ? k8sRoutesToRoutesMemoized(result.data.items) : []; @@ -128,7 +129,7 @@ export function useDeleteNotificationPolicy({ alertmanager }: BaseAlertmanagerAr const [updatedNamespacedRoute] = useReplaceNamespacedRoutingTreeMutation(); const deleteFromK8sApi = useAsync(async (id: string) => { - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); const result = await listNamespacedRoutingTree({ namespace }); const [rootTree] = result.data ? k8sRoutesToRoutesMemoized(result.data.items) : []; @@ -173,7 +174,7 @@ export function useAddNotificationPolicy({ alertmanager }: BaseAlertmanagerArgs) referenceRouteIdentifier: string; insertPosition: InsertPosition; }) => { - const namespace = getK8sNamespace(); + const namespace = getAPINamespace(); const result = await listNamespacedRoutingTree({ namespace }); const [rootTree] = result.data ? k8sRoutesToRoutesMemoized(result.data.items) : []; diff --git a/public/app/features/alerting/unified/home/Insights.tsx b/public/app/features/alerting/unified/home/Insights.tsx index f98cd51622e..9d85797751b 100644 --- a/public/app/features/alerting/unified/home/Insights.tsx +++ b/public/app/features/alerting/unified/home/Insights.tsx @@ -16,7 +16,7 @@ import { } from '@grafana/scenes'; import { Icon, Text, Tooltip } from '@grafana/ui'; -import { config } from '../../../../core/config'; +import { getAPINamespace } from '../../../../api/utils'; import { SectionFooter } from '../insights/SectionFooter'; import { SectionSubheader } from '../insights/SectionSubheader'; import { getActiveGrafanaAlertsScene } from '../insights/grafana/Active'; @@ -95,7 +95,7 @@ export const PANEL_STYLES = { minHeight: 300 }; const THIS_WEEK_TIME_RANGE = new SceneTimeRange({ from: 'now-1w', to: 'now' }); -const namespace = config.namespace; +const namespace = getAPINamespace(); export const INSTANCE_ID = namespace.includes('stacks-') ? namespace.replace('stacks-', '') : undefined; diff --git a/public/app/features/alerting/unified/utils/k8s/utils.ts b/public/app/features/alerting/unified/utils/k8s/utils.ts index 2857552ecc5..0aec7c1dd52 100644 --- a/public/app/features/alerting/unified/utils/k8s/utils.ts +++ b/public/app/features/alerting/unified/utils/k8s/utils.ts @@ -3,11 +3,6 @@ import { IoK8SApimachineryPkgApisMetaV1ObjectMeta } from 'app/features/alerting/ import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; -/** - * Get the correct namespace to use when using the K8S API. - */ -export const getK8sNamespace = () => config.namespace; - /** * Should we call the kubernetes-style API for managing alertmanager entities? * diff --git a/public/app/features/apiserver/client.ts b/public/app/features/apiserver/client.ts index ba647d4cf6c..90a5004b8b0 100644 --- a/public/app/features/apiserver/client.ts +++ b/public/app/features/apiserver/client.ts @@ -3,6 +3,8 @@ import { Observable, from, retry, catchError, filter, map, mergeMap } from 'rxjs import { config, getBackendSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; +import { getAPINamespace } from '../../api/utils'; + import { ListOptions, ListOptionsFieldSelector, @@ -29,7 +31,7 @@ export class ScopedResourceClient implements readonly url: string; constructor(gvr: GroupVersionResource, namespaced = true) { - const ns = namespaced ? `namespaces/${config.namespace}/` : ''; + const ns = namespaced ? `namespaces/${getAPINamespace()}/` : ''; this.url = `/apis/${gvr.group}/${gvr.version}/${ns}${gvr.resource}`; } diff --git a/public/app/features/dashboard/services/SnapshotSrv.ts b/public/app/features/dashboard/services/SnapshotSrv.ts index 5467542ab2b..1ca4894de22 100644 --- a/public/app/features/dashboard/services/SnapshotSrv.ts +++ b/public/app/features/dashboard/services/SnapshotSrv.ts @@ -4,6 +4,8 @@ import { config, getBackendSrv, FetchResponse } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { getAPINamespace } from '../../../api/utils'; + // Used in the snapshot list export interface Snapshot { key: string; @@ -91,7 +93,7 @@ class K8sAPI implements DashboardSnapshotSrv { readonly url: string; constructor() { - this.url = `/apis/${this.apiVersion}/namespaces/${config.namespace}/dashboardsnapshots`; + this.url = `/apis/${this.apiVersion}/namespaces/${getAPINamespace()}/dashboardsnapshots`; } async create(cmd: SnapshotCreateCommand) { diff --git a/public/app/features/query-library/api/query.ts b/public/app/features/query-library/api/query.ts index c2ce2bea47c..610c1556387 100644 --- a/public/app/features/query-library/api/query.ts +++ b/public/app/features/query-library/api/query.ts @@ -1,9 +1,10 @@ import { BaseQueryFn } from '@reduxjs/toolkit/query/react'; import { lastValueFrom } from 'rxjs'; -import { config } from '@grafana/runtime'; import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime/src/services/backendSrv'; +import { getAPINamespace } from '../../../api/utils'; + /** * @alpha */ @@ -16,14 +17,12 @@ export enum QueryTemplateKinds { QueryTemplate = 'QueryTemplate', } -export const getK8sNamespace = () => config.namespace; - /** * Query Library is an experimental feature. API (including the URL path) will likely change. * * @alpha */ -export const BASE_URL = `/apis/${API_VERSION}/namespaces/${getK8sNamespace()}`; +export const BASE_URL = `/apis/${API_VERSION}/namespaces/${getAPINamespace()}`; interface QueryLibraryBackendRequest extends BackendSrvRequest { body?: BackendSrvRequest['data']; diff --git a/public/app/features/query-library/api/user.ts b/public/app/features/query-library/api/user.ts index 640b04aa1dd..6b8ba4c9437 100644 --- a/public/app/features/query-library/api/user.ts +++ b/public/app/features/query-library/api/user.ts @@ -1,6 +1,7 @@ import { getBackendSrv } from '@grafana/runtime'; -import { getK8sNamespace } from './query'; +import { getAPINamespace } from '../../../api/utils'; + import { UserDataQueryResponse } from './types'; /** @@ -11,7 +12,7 @@ export const API_VERSION = 'iam.grafana.app/v0alpha1'; /** * @alpha */ -const BASE_URL = `apis/${API_VERSION}/namespaces/${getK8sNamespace()}/display`; +const BASE_URL = `apis/${API_VERSION}/namespaces/${getAPINamespace()}/display`; export async function getUserInfo(url?: string): Promise { const userInfo = await getBackendSrv().get(`${BASE_URL}${url}`); diff --git a/public/app/features/scopes/internal/api.ts b/public/app/features/scopes/internal/api.ts index b1f1ceaf869..1b0dae4cce9 100644 --- a/public/app/features/scopes/internal/api.ts +++ b/public/app/features/scopes/internal/api.ts @@ -1,13 +1,15 @@ import { Scope, ScopeDashboardBinding, ScopeNode, ScopeSpec } from '@grafana/data'; -import { config, getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv } from '@grafana/runtime'; import { ScopedResourceClient } from 'app/features/apiserver/client'; +import { getAPINamespace } from '../../../api/utils'; + import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types'; import { getBasicScope, mergeScopes } from './utils'; const group = 'scope.grafana.app'; const version = 'v0alpha1'; -const namespace = config.namespace ?? 'default'; +const namespace = getAPINamespace(); const nodesEndpoint = `/apis/${group}/${version}/namespaces/${namespace}/find/scope_node_children`; const dashboardsEndpoint = `/apis/${group}/${version}/namespaces/${namespace}/find/scope_dashboard_bindings`; diff --git a/public/app/features/search/service/unified.ts b/public/app/features/search/service/unified.ts index 3ed44146c35..700cf566314 100644 --- a/public/app/features/search/service/unified.ts +++ b/public/app/features/search/service/unified.ts @@ -4,6 +4,8 @@ import { DataFrame, DataFrameView, getDisplayProcessor, SelectableValue, toDataF import { config, getBackendSrv } from '@grafana/runtime'; import { TermCount } from 'app/core/components/TagFilter/TagFilter'; +import { getAPINamespace } from '../../../api/utils'; + import { DashboardQueryResult, GrafanaSearcher, @@ -18,7 +20,7 @@ import { replaceCurrentFolderQuery } from './utils'; // and that it can not serve any search requests. We are temporarily using the old SQL Search API as a fallback when that happens. const loadingFrameName = 'Loading'; -const searchURI = `apis/dashboard.grafana.app/v0alpha1/namespaces/${config.namespace}/search`; +const searchURI = `apis/dashboard.grafana.app/v0alpha1/namespaces/${getAPINamespace()}/search`; export type SearchHit = { resource: string; // dashboards | folders From 243ce03b93792c464f59a0949427f034a20bd71a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:29:43 +0000 Subject: [PATCH 208/894] Update dependency @types/babel__preset-env to v7.10.0 (#99768) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b315ac4d2b0..9550b4ecfe0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8934,9 +8934,9 @@ __metadata: linkType: hard "@types/babel__preset-env@npm:^7": - version: 7.9.7 - resolution: "@types/babel__preset-env@npm:7.9.7" - checksum: 10/624425a84d9149aec04795fed6b1ac2f27dfd5d7976fde479bb1a4d754de34c92cdc28a1a373a5826382a68127b536420a0e090aa5fae522cb62724b7a571cb5 + version: 7.10.0 + resolution: "@types/babel__preset-env@npm:7.10.0" + checksum: 10/7d4d12758d89708afe327079d7d7580e8af3292295f087b8a9a48e12ac1d90aadc18ac3bc00f9b0cbc8778f3ce9fe778801d4d49b7691a75e3f13a901b69fd07 languageName: node linkType: hard From 7d4895c3c9601049943be9a888cd2931abe6abbe Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 29 Jan 2025 18:53:30 +0100 Subject: [PATCH 209/894] Alerting: Use exponential backoff in the remote Alertmanager readiness check (#99756) * Alerting: Use exponential backoff in the remote Alertmanager readiness check * fix capitalized error * remove unnecessary 'for' * refactor, use time.After() instead of channel --- pkg/services/ngalert/remote/alertmanager.go | 14 +-- .../ngalert/remote/client/alertmanager.go | 106 +++++++++--------- 2 files changed, 56 insertions(+), 64 deletions(-) diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 82b222713ef..5928c8f6dcb 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -232,19 +232,15 @@ func (am *Alertmanager) ApplyConfig(ctx context.Context, config *models.AlertCon } func (am *Alertmanager) checkReadiness(ctx context.Context) error { - ready, err := am.amClient.IsReadyWithBackoff(ctx) + err := am.amClient.IsReadyWithBackoff(ctx) if err != nil { return err } - if ready { - am.log.Debug("Alertmanager readiness check successful") - am.metrics.LastReadinessCheck.SetToCurrentTime() - am.ready = true - return nil - } - - return notifier.ErrAlertmanagerNotReady + am.log.Debug("Alertmanager readiness check successful") + am.metrics.LastReadinessCheck.SetToCurrentTime() + am.ready = true + return nil } // CompareAndSendConfiguration checks whether a given configuration is being used by the remote Alertmanager. diff --git a/pkg/services/ngalert/remote/client/alertmanager.go b/pkg/services/ngalert/remote/client/alertmanager.go index bc228884460..d7cf67ec41a 100644 --- a/pkg/services/ngalert/remote/client/alertmanager.go +++ b/pkg/services/ngalert/remote/client/alertmanager.go @@ -66,69 +66,65 @@ func (am *Alertmanager) GetAuthedClient() client.Requester { } // IsReadyWithBackoff executes a readiness check against the `/-/ready` Alertmanager endpoint. -// If it takes more than 10s to get a response back - we abort the check. -func (am *Alertmanager) IsReadyWithBackoff(ctx context.Context) (bool, error) { - ctx, cancel := context.WithCancel(ctx) +// It uses exponential backoff (100ms * 2^attempts) with a 10s timeout. +func (am *Alertmanager) IsReadyWithBackoff(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - readyURL := am.url.JoinPath(alertmanagerAPIMountPath, alertmanagerReadyPath) - - attempt := func() (int, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, readyURL.String(), nil) - if err != nil { - return 0, fmt.Errorf("error creating the readiness request: %w", err) - } - - res, err := am.httpClient.Do(req) - if err != nil { - return 0, fmt.Errorf("error performing the readiness check: %w", err) - } - - defer func() { - if err := res.Body.Close(); err != nil { - am.logger.Warn("Error closing response body", "err", err) - } - }() - - return res.StatusCode, nil - } - - var attempts int - ticker := time.NewTicker(100 * time.Millisecond) - deadlineCh := time.After(10 * time.Second) - defer ticker.Stop() - - for { + var wait time.Duration + for attempt := 1; ; attempt++ { select { - case <-ticker.C: - attempts++ - status, err := attempt() + case <-ctx.Done(): + return fmt.Errorf("readiness check timed out") + case <-time.After(wait): + wait = time.Duration(100<= 400 && status < 500 { - if status == http.StatusNotAcceptable { - // Mimir returns a 406 when the Alertmanager for the tenant is not running. - // This is expected if the Grafana Alertmanager configuration is default or not promoted. - // We can still use the endpoints to store and retrieve configuration/state. - am.logger.Debug("Remote Alertmanager not initialized for tenant", "attempt", attempts, "status", status) - return true, nil - } - - am.logger.Debug("Ready check failed with non-retriable status code", "attempt", attempts, "status", status) - return false, fmt.Errorf("ready check failed with non-retriable status code %d", status) - } - am.logger.Debug("Ready check failed, status code is not 200", "attempt", attempts, "status", status, "err", err) - continue + if status == http.StatusOK { + return nil } - return true, nil - case <-deadlineCh: - cancel() - return false, fmt.Errorf("ready check timed out after %d attempts", attempts) + if status == http.StatusNotAcceptable { + // Mimir returns a 406 when the Alertmanager for the tenant is not running. + // This is expected if the Grafana Alertmanager configuration is default or not promoted. + // We can still use the endpoints to store and retrieve configuration/state. + am.logger.Debug("Remote Alertmanager not initialized for tenant") + return nil + } + + if status >= 400 && status < 500 { + return fmt.Errorf("readiness check failed on attempt %d with non-retriable status code %d", attempt, status) + } + am.logger.Debug("Readiness check failed, status code is not 200", "attempt", attempt, "status", status, "err", err) } } } + +func (am *Alertmanager) checkReadiness(ctx context.Context) (int, error) { + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + am.url.JoinPath(alertmanagerAPIMountPath, alertmanagerReadyPath).String(), + nil, + ) + if err != nil { + return 0, fmt.Errorf("error creating the readiness request: %w", err) + } + + res, err := am.httpClient.Do(req) + if err != nil { + return 0, fmt.Errorf("error performing the readiness check: %w", err) + } + + defer func() { + if err := res.Body.Close(); err != nil { + am.logger.Warn("Error closing response body", "err", err) + } + }() + + return res.StatusCode, nil +} From 49bd8a608ec941f38bad58c7e3a1b368ca5ab3e1 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 29 Jan 2025 18:54:41 +0100 Subject: [PATCH 210/894] Alerting: Fix fieldSelector encoding (#99751) Co-authored-by: Sonia Aguilar --- .../mute-timings/useMuteTimings.tsx | 9 +++++-- .../alerting/unified/utils/k8s/utils.test.ts | 27 +++++++++++++++++++ .../alerting/unified/utils/k8s/utils.ts | 8 ++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 public/app/features/alerting/unified/utils/k8s/utils.test.ts diff --git a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx index fb873f967b3..edf6fd8fd05 100644 --- a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx @@ -10,7 +10,11 @@ import { import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; -import { isK8sEntityProvisioned, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; +import { + encodeFieldSelector, + isK8sEntityProvisioned, + shouldUseK8sApi, +} from 'app/features/alerting/unified/utils/k8s/utils'; import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; import { getAPINamespace } from '../../../../../api/utils'; @@ -200,7 +204,8 @@ export const useGetMuteTiming = ({ alertmanager, name: nameToFind }: BaseAlertma useEffect(() => { if (useK8sApi) { const namespace = getAPINamespace(); - getGrafanaTimeInterval({ namespace, fieldSelector: `spec.name=${nameToFind}` }, true); + const entityName = encodeFieldSelector(nameToFind); + getGrafanaTimeInterval({ namespace, fieldSelector: `spec.name=${entityName}` }, true); } else { getAlertmanagerTimeInterval(alertmanager, true); } diff --git a/public/app/features/alerting/unified/utils/k8s/utils.test.ts b/public/app/features/alerting/unified/utils/k8s/utils.test.ts new file mode 100644 index 00000000000..a04a1ea16ec --- /dev/null +++ b/public/app/features/alerting/unified/utils/k8s/utils.test.ts @@ -0,0 +1,27 @@ +import { encodeFieldSelector } from './utils'; + +describe('encodeFieldSelector', () => { + it('should escape backslashes', () => { + expect(encodeFieldSelector('some\\value')).toBe('some\\\\value'); + }); + + it('should escape equal signs', () => { + expect(encodeFieldSelector('key=value')).toBe('key\\=value'); + }); + + it('should handle strings with no backslashes or equal signs', () => { + expect(encodeFieldSelector('simplevalue')).toBe('simplevalue'); + }); + + it('should handle strings with multiple equal signs', () => { + expect(encodeFieldSelector('key=value=another=value')).toBe('key\\=value\\=another\\=value'); + }); + + it('should escape commas', () => { + expect(encodeFieldSelector('value,another')).toBe('value\\,another'); + }); + + it('should escape mixed special characters', () => { + expect(encodeFieldSelector('foo=bar,bar=baz,qux\\foo')).toBe('foo\\=bar\\,bar\\=baz\\,qux\\\\foo'); + }); +}); diff --git a/public/app/features/alerting/unified/utils/k8s/utils.ts b/public/app/features/alerting/unified/utils/k8s/utils.ts index 0aec7c1dd52..69444f02b2c 100644 --- a/public/app/features/alerting/unified/utils/k8s/utils.ts +++ b/public/app/features/alerting/unified/utils/k8s/utils.ts @@ -43,3 +43,11 @@ export const canAdminEntity = (k8sEntity: EntityToCheck) => export const canDeleteEntity = (k8sEntity: EntityToCheck) => getAnnotation(k8sEntity, K8sAnnotations.AccessDelete) === 'true'; + +/** + * Escape \ and = characters for field selectors. + * The Kubernetes API Machinery will decode those automatically. + */ +export const encodeFieldSelector = (value: string): string => { + return value.replaceAll(/\\/g, '\\\\').replaceAll(/\=/g, '\\=').replaceAll(/,/g, '\\,'); +}; From 1048e23872e3d5df42afe184b4bb0cf4025da0f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:57:54 +0000 Subject: [PATCH 211/894] Update dependency react-i18next to v15.4.0 (#99772) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9550b4ecfe0..02276433807 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26013,8 +26013,8 @@ __metadata: linkType: hard "react-i18next@npm:^15.0.0": - version: 15.2.0 - resolution: "react-i18next@npm:15.2.0" + version: 15.4.0 + resolution: "react-i18next@npm:15.4.0" dependencies: "@babel/runtime": "npm:^7.25.0" html-parse-stringify: "npm:^3.0.1" @@ -26026,7 +26026,7 @@ __metadata: optional: true react-native: optional: true - checksum: 10/9b2937f7beab763c494d55a801f21bfdbfe98e9509994c350d24fa404ded573f41e8607eeba290c686d5877d34f0ddefe48e9d6876720d5ed0e1243bcdd5dda6 + checksum: 10/4b3666d819f01cf96a256af4419b26938d314e33c6388eafccc29f67ad02994e5d53e7bf82eac656cade7f7bcd04f4a237f0b293165d7eda91d62e3fde605a38 languageName: node linkType: hard From 1795a2b4e3a88d0fcdf562d6fdfffbd47ef8bdab Mon Sep 17 00:00:00 2001 From: Kristina Date: Wed, 29 Jan 2025 13:02:56 -0600 Subject: [PATCH 212/894] Bar Gauge: Add extra padding for scrollbar (#99722) * Add extra padding in bar gauge if scroll exists * Add thin scroll bars, and fix test * add comment about height calculation --- .../src/components/BarGauge/BarGauge.test.tsx | 1 + .../src/components/BarGauge/BarGauge.tsx | 29 ++++++++++++++++--- .../components/VizRepeater/VizRepeater.tsx | 1 + .../plugins/panel/bargauge/BarGaugePanel.tsx | 6 +++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx index c8b76542ba5..df45a86942a 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx @@ -56,6 +56,7 @@ function getProps(propOverrides?: Partial): Props { theme, orientation: VizOrientation.Horizontal, namePlacement: BarGaugeNamePlacement.Auto, + isOverflow: false, }; Object.assign(props, propOverrides); diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index 1048d86253a..043578ff10a 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -34,6 +34,7 @@ const MAX_VALUE_WIDTH = 150; const TITLE_LINE_HEIGHT = 1.5; const VALUE_LINE_HEIGHT = 1; const VALUE_LEFT_PADDING = 10; +const VALUE_RIGHT_OVERFLOW_PADDING = 15; export interface Props extends Themeable2 { height: number; @@ -52,6 +53,7 @@ export interface Props extends Themeable2 { alignmentFactors?: DisplayValueAlignmentFactors; valueDisplayMode?: BarGaugeValueMode; namePlacement?: BarGaugeNamePlacement; + isOverflow: boolean; } export class BarGauge extends PureComponent { @@ -73,6 +75,7 @@ export class BarGauge extends PureComponent { }, itemSpacing: 8, showUnfilled: true, + isOverflow: false, }; render() { @@ -145,6 +148,7 @@ export class BarGauge extends PureComponent { text, valueDisplayMode, theme, + isOverflow, } = this.props; const { valueHeight, valueWidth, maxBarHeight, maxBarWidth, wrapperWidth, wrapperHeight } = calculateBarAndValueDimensions(this.props); @@ -160,7 +164,15 @@ export class BarGauge extends PureComponent { const valueColor = getTextValueColor(this.props); const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value; - const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation, text); + const valueStyles = getValueStyles( + valueToBaseSizeOn, + valueColor, + valueWidth, + valueHeight, + orientation, + isOverflow, + text + ); const containerStyles: CSSProperties = { width: `${wrapperWidth}px`, @@ -486,7 +498,7 @@ export function getValuePercent(value: number, minValue: number, maxValue: numbe * Only exported to for unit test */ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles { - const { displayMode, field, value, alignmentFactors, orientation, theme, text } = props; + const { displayMode, field, value, alignmentFactors, orientation, theme, text, isOverflow } = props; const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props); const minValue = field.min ?? GAUGE_DEFAULT_MINIMUM; @@ -496,7 +508,15 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles const barColor = value.color ?? FALLBACK_COLOR; const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value; - const valueStyles = getValueStyles(valueToBaseSizeOn, textColor, valueWidth, valueHeight, orientation, text); + const valueStyles = getValueStyles( + valueToBaseSizeOn, + textColor, + valueWidth, + valueHeight, + orientation, + isOverflow, + text + ); const isBasic = displayMode === 'basic'; const wrapperStyles: CSSProperties = { @@ -663,6 +683,7 @@ function getValueStyles( width: number, height: number, orientation: VizOrientation, + isOverflow: boolean, text?: VizTextDisplayOptions ): CSSProperties { const styles: CSSProperties = { @@ -688,7 +709,7 @@ function getValueStyles( calculateFontSize(formattedValueString, textWidth - VALUE_LEFT_PADDING * 2, height, VALUE_LINE_HEIGHT); styles.justifyContent = `flex-end`; styles.paddingLeft = `${VALUE_LEFT_PADDING}px`; - styles.paddingRight = `${VALUE_LEFT_PADDING}px`; + styles.paddingRight = `${VALUE_LEFT_PADDING + (isOverflow ? VALUE_RIGHT_OVERFLOW_PADDING : 0)}px`; // Need to remove the left padding from the text width constraints textWidth -= VALUE_LEFT_PADDING; } diff --git a/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx b/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx index fab514bde4b..7fe09d92777 100644 --- a/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx +++ b/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx @@ -181,6 +181,7 @@ export class VizRepeater extends PureComponent { const { value, alignmentFactors, orientation, width, height, count } = valueProps; const { field, display, view, colIndex } = value; const { openMenu, targetClassName } = menuProps; + const spacing = this.getItemSpacing(); + // check if the total height is bigger than the visualization height, if so, there will be scrollbars for overflow + const isOverflow = (height + spacing) * count - spacing > this.props.height; let processor: DisplayProcessor | undefined = undefined; if (view && isNumber(colIndex)) { @@ -45,7 +48,7 @@ export class BarGaugePanel extends PureComponent { text={options.text} display={processor} theme={config.theme2} - itemSpacing={this.getItemSpacing()} + itemSpacing={spacing} displayMode={options.displayMode} onClick={openMenu} className={targetClassName} @@ -53,6 +56,7 @@ export class BarGaugePanel extends PureComponent { showUnfilled={options.showUnfilled} valueDisplayMode={options.valueMode} namePlacement={options.namePlacement} + isOverflow={isOverflow} /> ); }; From 3954a1948c21236e6b0ca91e95e6189e2e6e2b15 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 29 Jan 2025 13:44:09 -0600 Subject: [PATCH 213/894] Data links: Remove messaging around deprecating compact Explore URLs (#99780) --- .../components/DataLinks/DataLinkEditor.tsx | 7 +---- .../DataLinksListItem.test.tsx | 11 ------- .../DataLinksListItem.tsx | 21 +++---------- .../grafana-ui/src/utils/dataLinks.test.ts | 31 ------------------- packages/grafana-ui/src/utils/dataLinks.ts | 5 --- 5 files changed, 5 insertions(+), 70 deletions(-) delete mode 100644 packages/grafana-ui/src/utils/dataLinks.test.ts diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx index cdd5e64eddf..e87d01fbea2 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx @@ -4,7 +4,6 @@ import { memo, ChangeEvent } from 'react'; import { VariableSuggestion, GrafanaTheme2, DataLink } from '@grafana/data'; import { useStyles2 } from '../../themes/index'; -import { isCompactUrl } from '../../utils/dataLinks'; import { t, Trans } from '../../utils/i18n'; import { Field } from '../Forms/Field'; import { Input } from '../Input/Input'; @@ -55,11 +54,7 @@ export const DataLinkEditor = memo(({ index, value, onChange, suggestions, isLas - + diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx index 1ab1c27d4cd..b44576d53f7 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx @@ -63,17 +63,6 @@ describe('DataLinksListItem', () => { expect(screen.getByText(/http:\/\/localhost\:3000/i)).toBeInTheDocument(); expect(screen.getByTitle(/http:\/\/localhost\:3000/i)).toBeInTheDocument(); }); - - it('that is a explore compact url, then the title should be a warning', () => { - const link = { - ...baseLink, - url: 'http://localhost:3000/explore?orgId=1&left=[%22now-1h%22,%22now%22,%22gdev-loki%22,{%22expr%22:%22{place=%22luna%22}%22,%22refId%22:%22A%22}]', - }; - setupTestContext({ link }); - - expect(screen.getByText(/http:\/\/localhost\:3000/i)).toBeInTheDocument(); - expect(screen.getByText(/Explore data link may not work in the future. Please edit./i)).toBeInTheDocument(); - }); }); describe('when link is missing title', () => { diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx index 1c12a41dd2a..9bddc35da71 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx @@ -4,12 +4,10 @@ import { Draggable } from '@hello-pangea/dnd'; import { DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../themes'; -import { isCompactUrl } from '../../../utils'; import { t } from '../../../utils/i18n'; import { Badge } from '../../Badge/Badge'; import { Icon } from '../../Icon/Icon'; import { IconButton } from '../../IconButton/IconButton'; -import { Tooltip } from '../../Tooltip/Tooltip'; export interface DataLinksListItemProps { index: number; @@ -29,8 +27,6 @@ export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: Da const hasTitle = title.trim() !== ''; const hasUrl = url.trim() !== ''; - const isCompactExploreUrl = isCompactUrl(url); - return ( {(provided) => ( @@ -41,17 +37,12 @@ export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: Da key={index} >
-
+
{hasTitle ? title : 'Data link title not provided'}
- -
- {hasUrl ? url : 'Data link url not provided'} -
-
+
+ {hasUrl ? url : 'Data link url not provided'} +
{oneClick && ( @@ -91,10 +82,6 @@ const getDataLinkListItemStyles = (theme: GrafanaTheme2) => { flexGrow: 1, maxWidth: `calc(100% - 100px)`, }), - errored: css({ - color: theme.colors.error.text, - fontStyle: 'italic', - }), notConfigured: css({ fontStyle: 'italic', }), diff --git a/packages/grafana-ui/src/utils/dataLinks.test.ts b/packages/grafana-ui/src/utils/dataLinks.test.ts deleted file mode 100644 index a4f5ae1a61b..00000000000 --- a/packages/grafana-ui/src/utils/dataLinks.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { isCompactUrl } from './dataLinks'; - -describe('Datalinks', () => { - it('isCompactUrl matches compact URL with segments', () => { - expect( - isCompactUrl( - 'http://localhost:3000/explore?orgId=1&left=[%22now-1h%22,%22now%22,%22gdev-loki%22,{%22expr%22:%22{place=%22luna%22}%22,%22refId%22:%22A%22}]' - ) - ).toEqual(true); - }); - - it('isCompactUrl matches compact URL without segments', () => { - expect(isCompactUrl('http://localhost:3000/explore?orgId=1&left=[%22now-1h%22,%22now%22,%22gdev-loki%22]')).toEqual( - true - ); - }); - - it('isCompactUrl matches compact URL with right pane', () => { - expect( - isCompactUrl('http://localhost:3000/explore?orgId=1&right=[%22now-1h%22,%22now%22,%22gdev-loki%22]') - ).toEqual(true); - }); - - it('isCompactUrl does not match non-compact url', () => { - expect( - isCompactUrl( - 'http://localhost:3000/explore?orgId=1&left={"datasource":"test[datasource]","queries":[{"refId":"A","datasource":{"type":"prometheus","uid":"gdev-prometheus"}}],"range":{"from":"now-1h","to":"now"}}' - ) - ).toEqual(false); - }); -}); diff --git a/packages/grafana-ui/src/utils/dataLinks.ts b/packages/grafana-ui/src/utils/dataLinks.ts index 35fe1cffdf7..f889cf5a2a0 100644 --- a/packages/grafana-ui/src/utils/dataLinks.ts +++ b/packages/grafana-ui/src/utils/dataLinks.ts @@ -29,8 +29,3 @@ export const actionModelToContextMenuItems: (actions: ActionModel[]) => MenuItem }; }); }; - -export const isCompactUrl = (url: string) => { - const compactExploreUrlRegex = /\/explore\?.*&(left|right)=\[(.*\,){2,}(.*){1}\]/; - return compactExploreUrlRegex.test(url); -}; From 83d4f6e868cd2445c8faea4da674077bd2968f2f Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Wed, 29 Jan 2025 21:36:57 +0100 Subject: [PATCH 214/894] Remove e2e benchmark (#99695) --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 0456907f286..1e7dde232a2 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,6 @@ "e2e:enterprise": "./e2e/start-and-run-suite enterprise", "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", - "build-benchmark": "NODE_ENV=dev nx exec -- webpack --config scripts/webpack/webpack.dev.js --env benchmark=1", - "e2e:playwright:benchmark": "yarn build-benchmark && ./e2e/plugin-e2e/start-and-benchmark", "e2e:playwright": "yarn playwright test", "e2e:playwright:server": "yarn e2e:plugin:build && ./e2e/plugin-e2e/start-and-run-suite", "e2e:storybook": "PORT=9001 ./e2e/run-suite storybook true", From b820fd6bef3b38a6d8985a9751a6f201d59f152f Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Wed, 29 Jan 2025 16:00:45 -0500 Subject: [PATCH 215/894] Alerting: Fix Alertmanager configuration updates (#99610) * Alerting: Fix Alertmanager configuration updates Alertmanager configuration updates would behave inconsistently when performing no-op updates with `mysql` as the store. In particular this bug manifested as a failure to reload the provisioned alertmanager configuration components with no changes to the configuration itself. This would result in a 500 error with mysql store only. The core issue is that we were relying on the number of rows affected by the update query to determine if the configuration was found in the db or not. While this behavior works for certain sql dialects, mysql does not return the number of rows matched by the update query but rather the number of rows actually updated. Also discovered and fixed the mismatched `xorm` tag for the `CreatedAt` field to match the actual column name in the db. References: https://dev.mysql.com/doc/refman/8.4/en/update.html --- pkg/services/ngalert/models/alertmanager.go | 2 +- pkg/services/ngalert/store/alertmanager.go | 17 +++++++++++- .../ngalert/store/alertmanager_test.go | 26 +++++++++++++++++++ .../api/alerting/api_provisioning_test.go | 14 ++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/pkg/services/ngalert/models/alertmanager.go b/pkg/services/ngalert/models/alertmanager.go index 0b69f682c8a..98e86587625 100644 --- a/pkg/services/ngalert/models/alertmanager.go +++ b/pkg/services/ngalert/models/alertmanager.go @@ -9,7 +9,7 @@ type AlertConfiguration struct { AlertmanagerConfiguration string ConfigurationHash string ConfigurationVersion string - CreatedAt int64 `xorm:"created"` + CreatedAt int64 `xorm:"created_at"` Default bool OrgID int64 `xorm:"org_id"` } diff --git a/pkg/services/ngalert/store/alertmanager.go b/pkg/services/ngalert/store/alertmanager.go index fc07e7a5f35..33e62f46fb9 100644 --- a/pkg/services/ngalert/store/alertmanager.go +++ b/pkg/services/ngalert/store/alertmanager.go @@ -110,9 +110,24 @@ func (st DBstore) SaveAlertmanagerConfigurationWithCallback(ctx context.Context, // UpdateAlertmanagerConfiguration replaces an alertmanager configuration with optimistic locking. It assumes that an existing revision of the configuration exists in the store, and will return an error otherwise. func (st *DBstore) UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error { return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + newConfigHash := fmt.Sprintf("%x", md5.Sum([]byte(cmd.AlertmanagerConfiguration))) + // check for no-op update + if newConfigHash == cmd.FetchedConfigurationHash { + // double check that the configuration with this hash is in the db + ok, err := sess.Table("alert_configuration"). + Where("org_id = ? AND configuration_hash = ?", cmd.OrgID, cmd.FetchedConfigurationHash). + Exist() + if err != nil { + return err + } + if !ok { + return ErrVersionLockedObjectNotFound + } + return nil + } config := models.AlertConfiguration{ AlertmanagerConfiguration: cmd.AlertmanagerConfiguration, - ConfigurationHash: fmt.Sprintf("%x", md5.Sum([]byte(cmd.AlertmanagerConfiguration))), + ConfigurationHash: newConfigHash, ConfigurationVersion: cmd.ConfigurationVersion, Default: cmd.Default, OrgID: cmd.OrgID, diff --git a/pkg/services/ngalert/store/alertmanager_test.go b/pkg/services/ngalert/store/alertmanager_test.go index 4433760c8e1..4f27e2b6e83 100644 --- a/pkg/services/ngalert/store/alertmanager_test.go +++ b/pkg/services/ngalert/store/alertmanager_test.go @@ -114,6 +114,32 @@ func TestIntegrationAlertmanagerStore(t *testing.T) { require.ErrorIs(t, err, ErrVersionLockedObjectNotFound) }) + + t.Run("UpdateAlertmanagerConfiguration doesn't update the db if the update is a no-op", func(t *testing.T) { + _, configMD5 := setupConfig(t, "my-config", store) + + originalConfig, err := store.GetLatestAlertmanagerConfiguration(context.Background(), 1) + require.NoError(t, err) + cmd := buildSaveConfigCmd(t, "my-config", 1) + cmd.FetchedConfigurationHash = configMD5 + err = store.UpdateAlertmanagerConfiguration(context.Background(), &cmd) + require.NoError(t, err) + config, err := store.GetLatestAlertmanagerConfiguration(context.Background(), 1) + require.NoError(t, err) + require.Equal(t, "my-config", config.AlertmanagerConfiguration) + require.Equal(t, configMD5, config.ConfigurationHash) + // CreatedAt should not have changed as we didn't touch the config in the DB + require.Equal(t, originalConfig.CreatedAt, config.CreatedAt) + }) + t.Run("UpdateAlertmanagerConfiguration fails if the config doesn't exist and the hashes in the cmd match", func(t *testing.T) { + configRaw := "my-non-existent-config" + configHash := fmt.Sprintf("%x", md5.Sum([]byte(configRaw))) + cmd := buildSaveConfigCmd(t, configRaw, 1) + cmd.FetchedConfigurationHash = configHash + err := store.UpdateAlertmanagerConfiguration(context.Background(), &cmd) + require.Error(t, err) + require.EqualError(t, err, ErrVersionLockedObjectNotFound.Error()) + }) } func TestIntegrationAlertmanagerHash(t *testing.T) { diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index ec9cda827b8..f4bc4dab890 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -903,6 +903,9 @@ func TestIntegrationExportFileProvision(t *testing.T) { require.Len(t, export.MuteTimings, 1) require.YAMLEq(t, expectedYaml, exportRaw) }) + t.Run("reloading provisioning should not fail", func(t *testing.T) { + apiClient.ReloadAlertingFileProvisioning(t) + }) }) } @@ -1004,6 +1007,17 @@ func TestIntegrationExportFileProvisionContactPoints(t *testing.T) { var export definitions.AlertingFileExport require.NoError(t, yaml.Unmarshal([]byte(exportRaw), &export)) + // verify the file exported matches the file provisioned thing + require.Len(t, export.ContactPoints, 1) + require.YAMLEq(t, string(expectedYaml), exportRaw) + }) + t.Run("reloading provisioning should not change things", func(t *testing.T) { + apiClient.ReloadAlertingFileProvisioning(t) + + exportRaw := apiClient.ExportReceiver(t, "cp_1_$escaped", "yaml", true) + var export definitions.AlertingFileExport + require.NoError(t, yaml.Unmarshal([]byte(exportRaw), &export)) + // verify the file exported matches the file provisioned thing require.Len(t, export.ContactPoints, 1) require.YAMLEq(t, string(expectedYaml), exportRaw) From cf177776bfcfb22664d15097caa35e690e20aae5 Mon Sep 17 00:00:00 2001 From: Garret Wyman Date: Wed, 29 Jan 2025 17:12:16 -0500 Subject: [PATCH 216/894] Alerting: Adding color option for slack receiver (#99615) --- docs/sources/administration/provisioning/index.md | 1 + .../file-provisioning/index.md | 2 ++ go.mod | 2 +- go.sum | 4 ++-- .../ngalert/api/tooling/definitions/contact_points.go | 1 + pkg/services/ngalert/models/receivers_test.go | 2 +- .../notifier/channels_config/available_channels.go | 8 ++++++++ pkg/storage/unified/apistore/go.mod | 2 +- pkg/storage/unified/apistore/go.sum | 4 ++-- pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/go.sum | 4 ++-- 11 files changed, 22 insertions(+), 10 deletions(-) diff --git a/docs/sources/administration/provisioning/index.md b/docs/sources/administration/provisioning/index.md index 5c164a708aa..b06979b4880 100644 --- a/docs/sources/administration/provisioning/index.md +++ b/docs/sources/administration/provisioning/index.md @@ -479,6 +479,7 @@ The following sections detail the supported settings and secure settings for eac | mentionGroups | | | mentionChannel | | | token | yes | +| color | | #### Alert notification `victorops` diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index b7eb4b841fa..850600cc074 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -511,6 +511,8 @@ settings: # endpointUrl: https://custom_url/api/chat.postMessage # + color: {{ if eq .Status "firing" }}#D63232{{ else }}#36a64f{{ end }} + # title: | {{ template "slack.default.title" . }} text: | diff --git a/go.mod b/go.mod index 9ee6a8c744a..3b872f030f5 100644 --- a/go.mod +++ b/go.mod @@ -69,7 +69,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group 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-20250117230852-a5e8136407d4 // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index b1f20abba02..57b33a896ee 100644 --- a/go.sum +++ b/go.sum @@ -1498,8 +1498,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 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/alerting v0.0.0-20250117230852-a5e8136407d4 h1:616HUg7WVyLJLtVvk2pZ845M4Gk0fswgTwEZSuAZbJU= -github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce h1:lilqLsOGzo+0SuyXjaN5XRVJbnkJRB0bXMoIlYHTIPE= +github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= diff --git a/pkg/services/ngalert/api/tooling/definitions/contact_points.go b/pkg/services/ngalert/api/tooling/definitions/contact_points.go index bb38195c780..1e523a8cbe6 100644 --- a/pkg/services/ngalert/api/tooling/definitions/contact_points.go +++ b/pkg/services/ngalert/api/tooling/definitions/contact_points.go @@ -227,6 +227,7 @@ type SlackIntegration struct { MentionChannel *string `json:"mentionChannel,omitempty" yaml:"mentionChannel,omitempty" hcl:"mention_channel"` MentionUsers *string `json:"mentionUsers,omitempty" yaml:"mentionUsers,omitempty" hcl:"mention_users"` MentionGroups *string `json:"mentionGroups,omitempty" yaml:"mentionGroups,omitempty" hcl:"mention_groups"` + Color *string `json:"color,omitempty" yaml:"color,omitempty" hcl:"color"` } type TelegramIntegration struct { diff --git a/pkg/services/ngalert/models/receivers_test.go b/pkg/services/ngalert/models/receivers_test.go index a47387c58b5..d83f346f0ae 100644 --- a/pkg/services/ngalert/models/receivers_test.go +++ b/pkg/services/ngalert/models/receivers_test.go @@ -331,7 +331,7 @@ func TestReceiver_Fingerprint(t *testing.T) { completelyDifferentReceiver.Integrations[0].Config = IntegrationConfig{Type: completelyDifferentReceiver.Integrations[0].Config.Type} // Remove all fields except Type. t.Run("stable across code changes", func(t *testing.T) { - expectedFingerprint := "a3402fdaba03030c" // If this is a valid fingerprint generation change, update the expected value. + expectedFingerprint := "c0c82936be34b183" // If this is a valid fingerprint generation change, update the expected value. assert.Equal(t, expectedFingerprint, baseReceiver.Fingerprint()) }) t.Run("stable across clones", func(t *testing.T) { diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels.go b/pkg/services/ngalert/notifier/channels_config/available_channels.go index 07df8e78251..595ac33c447 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels.go @@ -691,6 +691,14 @@ func GetAvailableNotifiers() []*NotifierPlugin { Placeholder: "Slack endpoint url", PropertyName: "endpointUrl", }, + { + Label: "Color", + Element: ElementTypeInput, + InputType: InputTypeText, + Description: "Templated color of the slack message", + Placeholder: alertingTemplates.DefaultMessageColor, + PropertyName: "color", + }, { // New in 8.0. Label: "Title", Element: ElementTypeInput, diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 0a0624c9222..ede76c6bdfb 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -170,7 +170,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4 // indirect + github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce // indirect github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index f8a3eb6fd01..6a81ac11767 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -547,8 +547,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/alerting v0.0.0-20250117230852-a5e8136407d4 h1:616HUg7WVyLJLtVvk2pZ845M4Gk0fswgTwEZSuAZbJU= -github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce h1:lilqLsOGzo+0SuyXjaN5XRVJbnkJRB0bXMoIlYHTIPE= +github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index f7e3c9cf4b5..13ee3bd6f3b 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -115,7 +115,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4 // indirect + github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/grafana-app-sdk/logging v0.29.0 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index c6184792eae..4674123e79e 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -403,8 +403,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 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-20250117230852-a5e8136407d4 h1:616HUg7WVyLJLtVvk2pZ845M4Gk0fswgTwEZSuAZbJU= -github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce h1:lilqLsOGzo+0SuyXjaN5XRVJbnkJRB0bXMoIlYHTIPE= +github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= From 8e9e3b8362a3007a726296a8c1a426cca64ab8dd Mon Sep 17 00:00:00 2001 From: Scott Lepper Date: Wed, 29 Jan 2025 17:31:59 -0500 Subject: [PATCH 217/894] [search] title phrase (#99753) [search] rename title_sort to title_phrase --- .../folderimpl/folder_unifiedstorage.go | 4 +-- .../folderimpl/folder_unifiedstorage_test.go | 2 +- pkg/storage/unified/resource/document.go | 22 +++++++-------- pkg/storage/unified/resource/document_test.go | 2 +- pkg/storage/unified/resource/resource.pb.go | 8 +++--- pkg/storage/unified/resource/resource.proto | 8 +++--- pkg/storage/unified/search/bleve.go | 22 +++++++++++---- pkg/storage/unified/search/bleve_mappings.go | 7 +++-- pkg/storage/unified/search/bleve_test.go | 28 +++++++++---------- .../testdata/doc/dashboard-aaa-out.json | 2 +- .../search/testdata/doc/folder-aaa-out.json | 2 +- .../search/testdata/doc/folder-bbb-out.json | 2 +- .../search/testdata/doc/playlist-aaa-out.json | 2 +- .../search/testdata/doc/report-aaa-out.json | 2 +- 14 files changed, 62 insertions(+), 51 deletions(-) diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index ae1f579efd0..d32641d68f2 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -240,9 +240,9 @@ func (s *Service) getFolderByTitleFromApiServer(ctx context.Context, orgID int64 Key: folderkey, Fields: []*resource.Requirement{ { - Key: resource.SEARCH_FIELD_TITLE_SORT, + Key: resource.SEARCH_FIELD_TITLE_PHRASE, Operator: string(selection.In), - Values: []string{strings.ToLower(title)}, + Values: []string{title}, }, }, Labels: []*resource.Requirement{}, diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index a19c83f209e..7377c18bc2f 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -567,7 +567,7 @@ func (r resourceClientMock) Search(ctx context.Context, in *resource.ResourceSea } if len(in.Options.Fields) > 0 && - in.Options.Fields[0].Key == resource.SEARCH_FIELD_TITLE_SORT && + in.Options.Fields[0].Key == resource.SEARCH_FIELD_TITLE_PHRASE && in.Options.Fields[0].Operator == "in" && len(in.Options.Fields[0].Values) > 0 && in.Options.Fields[0].Values[0] == "foo" { diff --git a/pkg/storage/unified/resource/document.go b/pkg/storage/unified/resource/document.go index ce5ecdf0d16..a868719263d 100644 --- a/pkg/storage/unified/resource/document.go +++ b/pkg/storage/unified/resource/document.go @@ -63,7 +63,7 @@ type IndexableDocument struct { Title string `json:"title,omitempty"` // internal sort field for title ( don't set this directly ) - TitleSort string `json:"title_sort,omitempty"` + TitlePhrase string `json:"title_phrase,omitempty"` // A generic description -- helpful in global search Description string `json:"description,omitempty"` @@ -163,15 +163,15 @@ func NewIndexableDocument(key *ResourceKey, rv int64, obj utils.GrafanaMetaAcces } } doc := &IndexableDocument{ - Key: key, - RV: rv, - Name: key.Name, - Title: title, // We always want *something* to display - TitleSort: strings.ToLower(title), // Lowercase for case-insensitive sorting - Labels: obj.GetLabels(), - Folder: obj.GetFolder(), - CreatedBy: obj.GetCreatedBy(), - UpdatedBy: obj.GetUpdatedBy(), + Key: key, + RV: rv, + Name: key.Name, + Title: title, // We always want *something* to display + TitlePhrase: strings.ToLower(title), // Lowercase for case-insensitive sorting + Labels: obj.GetLabels(), + Folder: obj.GetFolder(), + CreatedBy: obj.GetCreatedBy(), + UpdatedBy: obj.GetUpdatedBy(), } doc.RepoInfo, _ = obj.GetRepositoryInfo() ts := obj.GetCreationTimestamp() @@ -253,7 +253,7 @@ const SEARCH_FIELD_NAMESPACE = "namespace" const SEARCH_FIELD_NAME = "name" const SEARCH_FIELD_RV = "rv" const SEARCH_FIELD_TITLE = "title" -const SEARCH_FIELD_TITLE_SORT = "title_sort" +const SEARCH_FIELD_TITLE_PHRASE = "title_phrase" // filtering/sorting on title by full phrase const SEARCH_FIELD_DESCRIPTION = "description" const SEARCH_FIELD_TAGS = "tags" const SEARCH_FIELD_LABELS = "labels" // All labels, not a specific one diff --git a/pkg/storage/unified/resource/document_test.go b/pkg/storage/unified/resource/document_test.go index 21ef906c6c0..593d1fbffd9 100644 --- a/pkg/storage/unified/resource/document_test.go +++ b/pkg/storage/unified/resource/document_test.go @@ -35,7 +35,7 @@ func TestStandardDocumentBuilder(t *testing.T) { }, "rv": 10, "title": "test playlist unified storage", - "title_sort": "test playlist unified storage", + "title_phrase": "test playlist unified storage", "created": 1717236672000, "createdBy": "user:ABC", "updatedBy": "user:XYZ", diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index 651f9a5e4ba..9a41f4c4e5a 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -1284,12 +1284,12 @@ type ListOptions struct { // Group+Namespace+Resource (not name) Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // (best effort) Match label - // Allowed to send more results than actually match because the filter will be appled - // to the resutls agin in the client. That time with the full field selector + // Allowed to send more results than actually match because the filter will be applied + // to the results again in the client. That time with the full field selector Labels []*Requirement `protobuf:"bytes,2,rep,name=labels,proto3" json:"labels,omitempty"` // (best effort) fields matcher - // Allowed to send more results than actually match because the filter will be appled - // to the resutls agin in the client. That time with the full field selector + // Allowed to send more results than actually match because the filter will be applied + // to the results again in the client. That time with the full field selector Fields []*Requirement `protobuf:"bytes,3,rep,name=fields,proto3" json:"fields,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index 005b7e6ab4e..ae7d067ff5a 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -209,13 +209,13 @@ message ListOptions { ResourceKey key = 1; // (best effort) Match label - // Allowed to send more results than actually match because the filter will be appled - // to the resutls agin in the client. That time with the full field selector + // Allowed to send more results than actually match because the filter will be applied + // to the results again in the client. That time with the full field selector repeated Requirement labels = 2; // (best effort) fields matcher - // Allowed to send more results than actually match because the filter will be appled - // to the resutls agin in the client. That time with the full field selector + // Allowed to send more results than actually match because the filter will be applied + // to the results again in the client. That time with the full field selector repeated Requirement fields = 3; } diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 145bd025cd7..bf9748cf5bc 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -684,9 +684,11 @@ func getSortFields(req *resource.ResourceSearchRequest) []string { // fields that we went to sort by the full text var textSortFields = map[string]string{ - resource.SEARCH_FIELD_TITLE: resource.SEARCH_FIELD_TITLE + "_sort", + resource.SEARCH_FIELD_TITLE: resource.SEARCH_FIELD_TITLE_PHRASE, } +const lowerCase = "phrase" + // Convert a "requirement" into a bleve query func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *resource.ErrorResult) { switch selection.Operator(req.Operator) { @@ -696,14 +698,14 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r } if len(req.Values[0]) == 1 { - q := query.NewMatchQuery(req.Values[0]) + q := query.NewMatchQuery(filterValue(req.Key, req.Values[0])) q.FieldVal = prefix + req.Key return q, nil } conjuncts := []query.Query{} for _, v := range req.Values { - q := query.NewMatchQuery(v) + q := query.NewMatchQuery(filterValue(req.Key, v)) q.FieldVal = prefix + req.Key conjuncts = append(conjuncts, q) } @@ -720,14 +722,14 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r return query.NewMatchAllQuery(), nil } if len(req.Values) == 1 { - q := query.NewMatchQuery(req.Values[0]) + q := query.NewMatchQuery(filterValue(req.Key, req.Values[0])) q.FieldVal = prefix + req.Key return q, nil } disjuncts := []query.Query{} for _, v := range req.Values { - q := query.NewMatchQuery(v) + q := query.NewMatchQuery(filterValue(req.Key, v)) q.FieldVal = prefix + req.Key disjuncts = append(disjuncts, q) } @@ -739,7 +741,7 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r var mustNotQueries []query.Query for _, value := range req.Values { - mustNotQueries = append(mustNotQueries, bleve.NewMatchQuery(value)) + mustNotQueries = append(mustNotQueries, bleve.NewMatchQuery(filterValue(req.Key, value))) } boolQuery.AddMustNot(mustNotQueries...) @@ -754,6 +756,14 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r ) } +// filterValue will convert the value to lower case if the field is a phrase field +func filterValue(field string, v string) string { + if strings.HasSuffix(field, lowerCase) { + return strings.ToLower(v) + } + return v +} + func (b *bleveIndex) hitsToTable(ctx context.Context, selectFields []string, hits search.DocumentMatchCollection, explain bool) (*resource.ResourceTable, error) { _, span := b.tracing.Start(ctx, tracingPrexfixBleve+"hitsToTable") defer span.End() diff --git a/pkg/storage/unified/search/bleve_mappings.go b/pkg/storage/unified/search/bleve_mappings.go index a1ec7d9fd6b..027fd87b947 100644 --- a/pkg/storage/unified/search/bleve_mappings.go +++ b/pkg/storage/unified/search/bleve_mappings.go @@ -24,11 +24,12 @@ func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentM } mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_NAME, nameMapping) - // for sorting by title - titleSortMapping := bleve.NewKeywordFieldMapping() - mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE_SORT, titleSortMapping) + // for filtering/sorting by title full phrase + titlePhraseMapping := bleve.NewKeywordFieldMapping() + mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE_PHRASE, titlePhraseMapping) // for searching by title + // TODO: do we still need this since we have SEARCH_FIELD_TITLE_PHRASE? titleSearchMapping := bleve.NewTextFieldMapping() mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE, titleSearchMapping) diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index 4535b276c0a..66acc8d9a25 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -78,9 +78,9 @@ func TestBleveBackend(t *testing.T) { Group: "dashboard.grafana.app", Resource: "dashboards", }, - Title: "aaa (dash)", - TitleSort: "aaa (dash)", - Folder: "xxx", + Title: "aaa (dash)", + TitlePhrase: "aaa (dash)", + Folder: "xxx", Fields: map[string]any{ DASHBOARD_PANEL_TYPES: []string{"timeseries", "table"}, DASHBOARD_ERRORS_TODAY: 25, @@ -106,9 +106,9 @@ func TestBleveBackend(t *testing.T) { Group: "dashboard.grafana.app", Resource: "dashboards", }, - Title: "bbb (dash)", - TitleSort: "bbb (dash)", - Folder: "xxx", + Title: "bbb (dash)", + TitlePhrase: "bbb (dash)", + Folder: "xxx", Fields: map[string]any{ DASHBOARD_PANEL_TYPES: []string{"timeseries"}, DASHBOARD_ERRORS_TODAY: 40, @@ -134,10 +134,10 @@ func TestBleveBackend(t *testing.T) { Group: "dashboard.grafana.app", Resource: "dashboards", }, - Name: "ccc", - Title: "ccc (dash)", - TitleSort: "ccc (dash)", - Folder: "zzz", + Name: "ccc", + Title: "ccc (dash)", + TitlePhrase: "ccc (dash)", + Folder: "zzz", RepoInfo: &utils.ResourceRepositoryInfo{ Name: "repo2", Path: "path/in/repo2.yaml", @@ -332,8 +332,8 @@ func TestBleveBackend(t *testing.T) { Group: "folder.grafana.app", Resource: "folders", }, - Title: "zzz (folder)", - TitleSort: "zzz (folder)", + Title: "zzz (folder)", + TitlePhrase: "zzz (folder)", RepoInfo: &utils.ResourceRepositoryInfo{ Name: "repo-1", Path: "path/to/folder.json", @@ -349,8 +349,8 @@ func TestBleveBackend(t *testing.T) { Group: "folder.grafana.app", Resource: "folders", }, - Title: "yyy (folder)", - TitleSort: "yyy (folder)", + Title: "yyy (folder)", + TitlePhrase: "yyy (folder)", Labels: map[string]string{ "region": "west", }, diff --git a/pkg/storage/unified/search/testdata/doc/dashboard-aaa-out.json b/pkg/storage/unified/search/testdata/doc/dashboard-aaa-out.json index 04278a03fde..9fa0a53b2c1 100644 --- a/pkg/storage/unified/search/testdata/doc/dashboard-aaa-out.json +++ b/pkg/storage/unified/search/testdata/doc/dashboard-aaa-out.json @@ -8,7 +8,7 @@ "name": "aaa", "rv": 1234, "title": "Test title", - "title_sort": "test title", + "title_phrase": "test title", "description": "test description", "tags": [ "a", diff --git a/pkg/storage/unified/search/testdata/doc/folder-aaa-out.json b/pkg/storage/unified/search/testdata/doc/folder-aaa-out.json index 85809ce618f..86cb4f5df16 100644 --- a/pkg/storage/unified/search/testdata/doc/folder-aaa-out.json +++ b/pkg/storage/unified/search/testdata/doc/folder-aaa-out.json @@ -8,7 +8,7 @@ "name": "aaa", "rv": 1234, "title": "test-aaa", - "title_sort": "test-aaa", + "title_phrase": "test-aaa", "created": 1730490142000, "createdBy": "user:1", "repo": { diff --git a/pkg/storage/unified/search/testdata/doc/folder-bbb-out.json b/pkg/storage/unified/search/testdata/doc/folder-bbb-out.json index dec666e5d42..5d268564f5b 100644 --- a/pkg/storage/unified/search/testdata/doc/folder-bbb-out.json +++ b/pkg/storage/unified/search/testdata/doc/folder-bbb-out.json @@ -8,7 +8,7 @@ "name": "bbb", "rv": 1234, "title": "test-bbb", - "title_sort": "test-bbb", + "title_phrase": "test-bbb", "created": 1730490142000, "createdBy": "user:1", "repo": { diff --git a/pkg/storage/unified/search/testdata/doc/playlist-aaa-out.json b/pkg/storage/unified/search/testdata/doc/playlist-aaa-out.json index a4fc4117a9a..c0f12814338 100644 --- a/pkg/storage/unified/search/testdata/doc/playlist-aaa-out.json +++ b/pkg/storage/unified/search/testdata/doc/playlist-aaa-out.json @@ -8,7 +8,7 @@ "name": "aaa", "rv": 1234, "title": "Test AAA", - "title_sort": "test aaa", + "title_phrase": "test aaa", "created": 1731336353000, "createdBy": "user:t000000001", "repo": { diff --git a/pkg/storage/unified/search/testdata/doc/report-aaa-out.json b/pkg/storage/unified/search/testdata/doc/report-aaa-out.json index ad38a85e3c8..6b193267072 100644 --- a/pkg/storage/unified/search/testdata/doc/report-aaa-out.json +++ b/pkg/storage/unified/search/testdata/doc/report-aaa-out.json @@ -8,7 +8,7 @@ "name": "aaa", "rv": 1234, "title": "Test AAA", - "title_sort": "test aaa", + "title_phrase": "test aaa", "labels": { "grafana.app/deprecatedInternalID": "123" }, From 70073427041e15c353e0d467b714527584765aea Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 29 Jan 2025 17:48:26 -0500 Subject: [PATCH 218/894] Alerting: k8s receivers api encrypt existing unencrypted secureFields on update (#99784) * apply security patch: v11.5.x/305-202501232115.patch commit 874ce8d12caad3742857ca86d2da7d5f81f3f825 Author: Matt Jacobson Date: Thu Jan 23 16:14:28 2025 -0500 linting commit c4b6d9194cc8b79e252e562a27a2d09a42d7a5e8 Author: Matt Jacobson Date: Thu Jan 23 14:56:35 2025 -0500 CVE-2024-11741 - victorops url --- go.mod | 2 +- go.sum | 4 +- go.work.sum | 1 + .../api/tooling/definitions/contact_points.go | 2 +- .../channels_config/available_channels.go | 1 + .../available_channels_test.go | 2 +- pkg/services/ngalert/notifier/receiver_svc.go | 8 +++ .../ngalert/notifier/receiver_svc_test.go | 54 ++++++++++++++++++- pkg/storage/unified/apistore/go.mod | 2 +- pkg/storage/unified/apistore/go.sum | 4 +- pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/go.sum | 4 +- 12 files changed, 74 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 3b872f030f5..d8bde533370 100644 --- a/go.mod +++ b/go.mod @@ -69,7 +69,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group 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-20250128163937-4446935bbcce // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index 57b33a896ee..42231876a94 100644 --- a/go.sum +++ b/go.sum @@ -1498,8 +1498,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 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/alerting v0.0.0-20250128163937-4446935bbcce h1:lilqLsOGzo+0SuyXjaN5XRVJbnkJRB0bXMoIlYHTIPE= -github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a h1:44E+I3EPdh/W02Uyfyig86EJKPjvzcF3y0A+FEi1fBk= +github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= diff --git a/go.work.sum b/go.work.sum index cad88eb884c..c335966892c 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1502,6 +1502,7 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20250115195200-209e052dba64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:V63rh3udd7sqXJeaG+nGUmViwVnM/bY6t8U9Tols2GU= github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= diff --git a/pkg/services/ngalert/api/tooling/definitions/contact_points.go b/pkg/services/ngalert/api/tooling/definitions/contact_points.go index 1e523a8cbe6..9df9b8ee444 100644 --- a/pkg/services/ngalert/api/tooling/definitions/contact_points.go +++ b/pkg/services/ngalert/api/tooling/definitions/contact_points.go @@ -268,7 +268,7 @@ type ThreemaIntegration struct { type VictoropsIntegration struct { DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` - URL string `json:"url" yaml:"url" hcl:"url"` + URL Secret `json:"url" yaml:"url" hcl:"url"` MessageType *string `json:"messageType,omitempty" yaml:"messageType,omitempty" hcl:"message_type"` Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels.go b/pkg/services/ngalert/notifier/channels_config/available_channels.go index 595ac33c447..e1748a7550b 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels.go @@ -388,6 +388,7 @@ func GetAvailableNotifiers() []*NotifierPlugin { Placeholder: "VictorOps url", PropertyName: "url", Required: true, + Secure: true, }, { // New in 8.0. Label: "Message Type", diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels_test.go b/pkg/services/ngalert/notifier/channels_config/available_channels_test.go index 5240c177873..5e305cb1224 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels_test.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels_test.go @@ -15,7 +15,7 @@ func TestGetSecretKeysForContactPointType(t *testing.T) { {receiverType: "kafka", expectedSecretFields: []string{"password"}}, {receiverType: "email", expectedSecretFields: []string{}}, {receiverType: "pagerduty", expectedSecretFields: []string{"integrationKey"}}, - {receiverType: "victorops", expectedSecretFields: []string{}}, + {receiverType: "victorops", expectedSecretFields: []string{"url"}}, {receiverType: "oncall", expectedSecretFields: []string{"password", "authorization_credentials"}}, {receiverType: "pushover", expectedSecretFields: []string{"apiToken", "userKey"}}, {receiverType: "slack", expectedSecretFields: []string{"token", "url"}}, diff --git a/pkg/services/ngalert/notifier/receiver_svc.go b/pkg/services/ngalert/notifier/receiver_svc.go index bd4852660aa..a4ba285d311 100644 --- a/pkg/services/ngalert/notifier/receiver_svc.go +++ b/pkg/services/ngalert/notifier/receiver_svc.go @@ -485,6 +485,14 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive return nil, err } + // We re-encrypt the existing receiver to ensure any unencrypted secure fields that are correctly encrypted, note this should NOT re-encrypt secure fields that are already encrypted. + // This is rare, but can happen if a receiver is created with unencrypted secure fields and then the secure option is added later. + // Preferably, this would be handled by receiver config versions and migrations but for now this is a good safety net. + err = existing.Encrypt(rs.encryptor(ctx)) + if err != nil { + return nil, err + } + span.AddEvent("Loaded current receiver", trace.WithAttributes( attribute.String("concurrency_token", revision.ConcurrencyToken), attribute.String("receiver", existing.Name), diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index d8c27a5d0c7..d8b5d019237 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -583,6 +583,44 @@ func TestReceiverService_Update(t *testing.T) { ), rm.Encrypted(models.Base64Enrypt)), expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceNone}, }, + { + name: "encrypts previously unencrypted secure fields", + user: writer, + receiver: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations( + models.CopyIntegrationWith(slackIntegration, im.AddSetting("token", "unencryptedValue"))), + ), + existing: util.Pointer(models.CopyReceiverWith(baseReceiver, rm.WithIntegrations( + models.CopyIntegrationWith(slackIntegration, + im.AddSetting("token", "unencryptedValue"), // This will get encrypted. + ), + ))), + expectedUpdate: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations( + models.CopyIntegrationWith(slackIntegration, + im.AddSecureSetting("token", "dW5lbmNyeXB0ZWRWYWx1ZQ==")), + ), rm.Encrypted(models.Base64Enrypt)), + expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceNone}, + }, + { + // This test is important for covering the rare case when an existing field is marked as secure. + // The UI will receive the field as secure and, if unchanged, will pass it back on update as a secureField instead of a Setting. + name: "encrypts previously unencrypted secure fields when passed in as secureFields", + user: writer, + receiver: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations( + models.CopyIntegrationWith(slackIntegration, im.AddSetting("newField", "newValue"))), + ), + secureFields: map[string][]string{slackIntegration.UID: {"token"}}, + existing: util.Pointer(models.CopyReceiverWith(baseReceiver, rm.WithIntegrations( + models.CopyIntegrationWith(slackIntegration, + im.AddSetting("token", "unencryptedValue"), // This will get encrypted. + ), + ))), + expectedUpdate: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations( + models.CopyIntegrationWith(slackIntegration, + im.AddSetting("newField", "newValue"), + im.AddSecureSetting("token", "dW5lbmNyeXB0ZWRWYWx1ZQ==")), + ), rm.Encrypted(models.Base64Enrypt)), + expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceNone}, + }, { name: "doesn't copy existing unsecure fields", user: writer, @@ -684,8 +722,22 @@ func TestReceiverService_Update(t *testing.T) { sut := createReceiverServiceSut(t, &secretsService) if tc.existing != nil { - created, err := sut.CreateReceiver(context.Background(), tc.existing, tc.user.GetOrgID(), tc.user) + // Create route after receivers as they will be referenced. + revision, err := sut.cfgStore.Get(context.Background(), tc.user.GetOrgID()) require.NoError(t, err) + result, err := revision.CreateReceiver(tc.existing) + require.NoError(t, err) + + created, err := PostableApiReceiverToReceiver(result, tc.existing.Provenance) + require.NoError(t, err) + err = sut.cfgStore.Save(context.Background(), revision, tc.user.GetOrgID()) + require.NoError(t, err) + + for _, integration := range created.Integrations { + target := definitions.EmbeddedContactPoint{UID: integration.UID} + err = sut.provisioningStore.SetProvenance(context.Background(), &target, tc.user.GetOrgID(), created.Provenance) + require.NoError(t, err) + } if tc.version == "" { tc.version = created.Version diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index ede76c6bdfb..32b82f51bdc 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -170,7 +170,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce // indirect + github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a // indirect github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 6a81ac11767..d25df5fb16f 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -547,8 +547,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/alerting v0.0.0-20250128163937-4446935bbcce h1:lilqLsOGzo+0SuyXjaN5XRVJbnkJRB0bXMoIlYHTIPE= -github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a h1:44E+I3EPdh/W02Uyfyig86EJKPjvzcF3y0A+FEi1fBk= +github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 13ee3bd6f3b..1f87f8bc70e 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -115,7 +115,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce // indirect + github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/grafana-app-sdk/logging v0.29.0 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 4674123e79e..0e30c13985c 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -403,8 +403,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 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-20250128163937-4446935bbcce h1:lilqLsOGzo+0SuyXjaN5XRVJbnkJRB0bXMoIlYHTIPE= -github.com/grafana/alerting v0.0.0-20250128163937-4446935bbcce/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a h1:44E+I3EPdh/W02Uyfyig86EJKPjvzcF3y0A+FEi1fBk= +github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= From 2d491a9367f1867cc1d21d0da7dd72520c71ab35 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Wed, 29 Jan 2025 16:44:42 -0700 Subject: [PATCH 219/894] K8s: Implement folder search (#99781) --- pkg/api/folder_bench_test.go | 2 +- .../dashboards/service/search/search.go | 6 +- pkg/services/folder/folderimpl/folder.go | 12 + .../folderimpl/folder_unifiedstorage.go | 99 +++++++++ .../folderimpl/folder_unifiedstorage_test.go | 206 ++++++++++++++++++ pkg/services/folder/foldertest/foldertest.go | 7 + pkg/services/folder/model.go | 9 + pkg/services/folder/service.go | 5 + pkg/services/search/service.go | 23 +- pkg/services/search/service_test.go | 3 + 10 files changed, 368 insertions(+), 4 deletions(-) diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index 0448eaddde1..c1114a3a48d 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -490,7 +490,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog SQLStore: sc.db, Features: features, QuotaService: quotaSrv, - SearchService: search.ProvideService(sc.cfg, sc.db, starSvc, dashboardSvc), + SearchService: search.ProvideService(sc.cfg, sc.db, starSvc, dashboardSvc, folderServiceWithFlagOn, features), folderService: folderServiceWithFlagOn, DashboardService: dashboardSvc, } diff --git a/pkg/services/dashboards/service/search/search.go b/pkg/services/dashboards/service/search/search.go index bb8a6dddb5e..a10f08954f2 100644 --- a/pkg/services/dashboards/service/search/search.go +++ b/pkg/services/dashboards/service/search/search.go @@ -31,7 +31,7 @@ func ParseResults(result *resource.ResourceSearchResponse, offset int64) (*v0alp } titleIDX := 0 - folderIDX := 1 + folderIDX := -1 tagsIDX := -1 scoreIDX := 0 explainIDX := 0 @@ -80,9 +80,11 @@ func ParseResults(result *resource.ResourceSearchResponse, offset int64) (*v0alp Resource: row.Key.Resource, // folders | dashboards Name: row.Key.Name, // The Grafana UID Title: string(row.Cells[titleIDX]), - Folder: string(row.Cells[folderIDX]), Field: fields, } + if folderIDX > 0 && row.Cells[folderIDX] != nil { + hit.Folder = string(row.Cells[folderIDX]) + } if tagsIDX > 0 && row.Cells[tagsIDX] != nil { _ = json.Unmarshal(row.Cells[tagsIDX], &hit.Tags) } diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index cafa0e38c91..b97ca614cc5 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -34,6 +34,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/publicdashboards" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/store/entity" @@ -167,6 +168,17 @@ func (s *Service) DBMigration(db db.DB) { s.log.Debug("syncing dashboard and folder tables finished") } +func (s *Service) SearchFolders(ctx context.Context, q folder.SearchFoldersQuery) (model.HitList, error) { + if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) { + // TODO: + // - implement filtering by alerting folders and k6 folders (see the dashboards store `FindDashboards` method for reference) + // - implement fallback on search client in unistore to go to legacy store (will need to read from dashboard store) + return s.searchFoldersFromApiServer(ctx, q) + } + + return nil, fmt.Errorf("cannot be called on the legacy folder service") +} + func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) { if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) { return s.getFoldersFromApiServer(ctx, q) diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index d32641d68f2..fed599819a1 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -3,6 +3,7 @@ package folderimpl import ( "context" "fmt" + "strconv" "strings" "time" @@ -20,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" @@ -28,9 +30,11 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/util" ) @@ -169,6 +173,101 @@ func (s *Service) getFromApiServer(ctx context.Context, q *folder.GetFolderQuery return f, err } +// searchFoldesFromApiServer uses the search grpc connection to search folders and returns the hit list +func (s *Service) searchFoldersFromApiServer(ctx context.Context, query folder.SearchFoldersQuery) (model.HitList, error) { + if query.OrgID == 0 { + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + query.OrgID = requester.GetOrgID() + } + + request := &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: s.k8sclient.getNamespace(query.OrgID), + Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, + Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + }, + Fields: []*resource.Requirement{}, + Labels: []*resource.Requirement{}, + }, + Limit: 100000} + + if len(query.UIDs) > 0 { + request.Options.Fields = []*resource.Requirement{{ + Key: resource.SEARCH_FIELD_NAME, + Operator: string(selection.In), + Values: query.UIDs, + }} + } else if len(query.IDs) > 0 { + values := make([]string, len(query.IDs)) + for i, id := range query.IDs { + values[i] = strconv.FormatInt(id, 10) + } + + request.Options.Labels = append(request.Options.Labels, &resource.Requirement{ + Key: utils.LabelKeyDeprecatedInternalID, // nolint:staticcheck + Operator: string(selection.In), + Values: values, + }) + } + + if query.Title != "" { + // allow wildcard search + request.Query = "*" + strings.ToLower(query.Title) + "*" + } + + if query.Limit > 0 { + request.Limit = query.Limit + } + + client := s.k8sclient.getSearcher(ctx) + + res, err := client.Search(ctx, request) + if err != nil { + return nil, err + } + + parsedResults, err := dashboardsearch.ParseResults(res, 0) + if err != nil { + return nil, err + } + + hitList := make([]*model.Hit, len(parsedResults.Hits)) + foldersMap := map[string]*folder.Folder{} + for i, item := range parsedResults.Hits { + f, ok := foldersMap[item.Folder] + if !ok { + f, err = s.Get(ctx, &folder.GetFolderQuery{ + UID: &item.Folder, + OrgID: query.OrgID, + SignedInUser: query.SignedInUser, + }) + if err != nil { + return nil, err + } + foldersMap[item.Folder] = f + } + slug := slugify.Slugify(item.Title) + hitList[i] = &model.Hit{ + ID: item.Field.GetNestedInt64(search.DASHBOARD_LEGACY_ID), + UID: item.Name, + OrgID: query.OrgID, + Title: item.Title, + URI: "db/" + slug, + URL: dashboards.GetFolderURL(item.Name, slug), + Type: model.DashHitFolder, + FolderUID: item.Folder, + FolderTitle: f.Title, + FolderID: f.ID, // nolint:staticcheck + } + } + + return hitList, nil +} + func (s *Service) getFolderByIDFromApiServer(ctx context.Context, id int64, orgID int64) (*folder.Folder, error) { if id == 0 { return &folder.GeneralFolder, nil diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 7377c18bc2f..e2d50566662 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/grpc" + "k8s.io/client-go/dynamic" clientrest "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -33,6 +34,7 @@ import ( "github.com/grafana/grafana/pkg/services/guardian" ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/publicdashboards" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" @@ -605,6 +607,81 @@ func (r resourceClientMock) Search(ctx context.Context, in *resource.ResourceSea }, nil } + if in.Query == "*test*" { + return &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "_id", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid", + Resource: "folders", + }, + Cells: [][]byte{ + []byte("123"), + []byte("testing-123"), + []byte("parent-uid"), + }, + }, + }, + }, + TotalHits: 1, + }, nil + } + + if len(in.Options.Fields) > 0 && + in.Options.Fields[0].Key == resource.SEARCH_FIELD_NAME && + in.Options.Fields[0].Operator == "in" && + len(in.Options.Fields[0].Values) > 0 { + rows := []*resource.ResourceTableRow{} + for i, row := range in.Options.Fields[0].Values { + rows = append(rows, &resource.ResourceTableRow{ + Key: &resource.ResourceKey{ + Name: row, + Resource: "folders", + }, + Cells: [][]byte{ + []byte(fmt.Sprintf("%d", i)), // set legacy id as the row id + []byte(fmt.Sprintf("folder%d", i)), // set title as folder + row id + []byte(""), + }, + }) + } + return &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "_id", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: rows, + }, + TotalHits: int64(len(rows)), + }, nil + } + // not found return &resource.ResourceSearchResponse{ Results: &resource.ResourceTable{}, @@ -628,3 +705,132 @@ func (r resourceClientMock) GetBlob(ctx context.Context, in *resource.GetBlobReq func (r resourceClientMock) IsHealthy(ctx context.Context, in *resource.HealthCheckRequest, opts ...grpc.CallOption) (*resource.HealthCheckResponse, error) { return nil, nil } + +type mockFoldersK8sCli struct { + mock.Mock + searcher resourceClientMock +} + +func (m *mockFoldersK8sCli) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) { + args := m.Called(ctx, orgID) + return args.Get(0).(dynamic.ResourceInterface), args.Bool(1) +} + +func (m *mockFoldersK8sCli) getNamespace(orgID int64) string { + if orgID == 1 { + return "default" + } + return fmt.Sprintf("orgs-%d", orgID) +} + +func (m *mockFoldersK8sCli) getSearcher(ctx context.Context) resource.ResourceClient { + return m.searcher +} + +func TestSearchFoldersFromApiServer(t *testing.T) { + fakeK8sClient := new(mockFoldersK8sCli) + service := Service{ + k8sclient: fakeK8sClient, + features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2), + } + fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient) + user := &user.SignedInUser{OrgID: 1} + ctx := identity.WithRequester(context.Background(), user) + + t.Run("Should search by uids if provided", func(t *testing.T) { + query := folder.SearchFoldersQuery{ + UIDs: []string{"uid1", "uid2"}, + IDs: []int64{1, 2}, // will ignore these because uid is passed in + SignedInUser: user, + } + result, err := service.searchFoldersFromApiServer(ctx, query) + require.NoError(t, err) + + expectedResult := model.HitList{ + { + UID: "uid1", + // no parent folder is returned, so the general folder should be set + FolderID: 0, + FolderTitle: "General", + // orgID should be taken from signed in user + OrgID: 1, + // the rest should be automatically set when parsing the hit results from search + Type: model.DashHitFolder, + URI: "db/folder0", + Title: "folder0", + URL: "/dashboards/f/uid1/folder0", + }, + { + UID: "uid2", + FolderID: 0, + FolderTitle: "General", + OrgID: 1, + Type: model.DashHitFolder, + URI: "db/folder1", + Title: "folder1", + URL: "/dashboards/f/uid2/folder1", + }, + } + require.Equal(t, expectedResult, result) + }) + + t.Run("Search by ID if uids are not provided", func(t *testing.T) { + query := folder.SearchFoldersQuery{ + IDs: []int64{123}, + SignedInUser: user, + } + result, err := service.searchFoldersFromApiServer(ctx, query) + require.NoError(t, err) + + expectedResult := model.HitList{ + { + UID: "foo", + FolderID: 0, + FolderTitle: "General", + OrgID: 1, + Type: model.DashHitFolder, + URI: "db/folder1", + Title: "folder1", + URL: "/dashboards/f/foo/folder1", + }, + } + require.Equal(t, expectedResult, result) + }) + + t.Run("Search by title, wildcard should be added to search request (won't match in search mock if not)", func(t *testing.T) { + // the search here will return a parent, this will be the parent folder returned when we query for it to add to the hit info + fakeFolderStore := folder.NewFakeStore() + fakeFolderStore.ExpectedFolder = &folder.Folder{ + UID: "parent-uid", + ID: 2, + Title: "parent title", + } + service.unifiedStore = fakeFolderStore + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanSaveValue: true, + CanViewValue: true, + }) + + query := folder.SearchFoldersQuery{ + Title: "test", + SignedInUser: user, + } + result, err := service.searchFoldersFromApiServer(ctx, query) + require.NoError(t, err) + + expectedResult := model.HitList{ + { + UID: "uid", + FolderID: 2, + FolderTitle: "parent title", + FolderUID: "parent-uid", + OrgID: 1, + Type: model.DashHitFolder, + URI: "db/testing-123", + Title: "testing-123", + URL: "/dashboards/f/uid/testing-123", + }, + } + require.Equal(t, expectedResult, result) + }) +} diff --git a/pkg/services/folder/foldertest/foldertest.go b/pkg/services/folder/foldertest/foldertest.go index 205d984d72b..7a5edf096e6 100644 --- a/pkg/services/folder/foldertest/foldertest.go +++ b/pkg/services/folder/foldertest/foldertest.go @@ -4,11 +4,13 @@ import ( "context" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/search/model" ) type FakeService struct { ExpectedFolders []*folder.Folder ExpectedFolder *folder.Folder + ExpectedHitList model.HitList ExpectedError error ExpectedDescendantCounts map[string]int64 } @@ -82,6 +84,11 @@ func (s *FakeService) GetDescendantCountsLegacy(ctx context.Context, q *folder.G func (s *FakeService) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) { return s.ExpectedFolders, s.ExpectedError } + +func (s *FakeService) SearchFolders(ctx context.Context, q folder.SearchFoldersQuery) (model.HitList, error) { + return s.ExpectedHitList, s.ExpectedError +} + func (s *FakeService) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) { return s.ExpectedFolders, s.ExpectedError } diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index dae38765773..769ee259f3f 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -179,6 +179,15 @@ type GetFoldersQuery struct { SignedInUser identity.Requester `json:"-"` } +type SearchFoldersQuery struct { + OrgID int64 + UIDs []string + IDs []int64 + Title string + Limit int64 + SignedInUser identity.Requester `json:"-"` +} + // GetParentsQuery captures the information required by the folder service to // return a list of all parent folders of a given folder. type GetParentsQuery struct { diff --git a/pkg/services/folder/service.go b/pkg/services/folder/service.go index 656bdeeb866..89a123b9b45 100644 --- a/pkg/services/folder/service.go +++ b/pkg/services/folder/service.go @@ -2,6 +2,8 @@ package folder import ( "context" + + "github.com/grafana/grafana/pkg/services/search/model" ) type Service interface { @@ -40,6 +42,9 @@ type Service interface { GetFolders(ctx context.Context, q GetFoldersQuery) ([]*Folder, error) GetFoldersLegacy(ctx context.Context, q GetFoldersQuery) ([]*Folder, error) + // SearchFolders returns a list of folders that match the query. + SearchFolders(ctx context.Context, q SearchFoldersQuery) (model.HitList, error) + // GetChildren returns an array containing all child folders. GetChildren(ctx context.Context, q *GetChildrenQuery) ([]*Folder, error) GetChildrenLegacy(ctx context.Context, q *GetChildrenQuery) ([]*Folder, error) diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go index 2eea2f28864..1b48756ea07 100644 --- a/pkg/services/search/service.go +++ b/pkg/services/search/service.go @@ -8,7 +8,10 @@ import ( "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/search/model" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -17,7 +20,7 @@ import ( var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/search") -func ProvideService(cfg *setting.Cfg, sqlstore db.DB, starService star.Service, dashboardService dashboards.DashboardService) *SearchService { +func ProvideService(cfg *setting.Cfg, sqlstore db.DB, starService star.Service, dashboardService dashboards.DashboardService, folderService folder.Service, features featuremgmt.FeatureToggles) *SearchService { s := &SearchService{ Cfg: cfg, sortOptions: map[string]model.SortOption{ @@ -26,6 +29,8 @@ func ProvideService(cfg *setting.Cfg, sqlstore db.DB, starService star.Service, }, sqlstore: sqlstore, starService: starService, + folderService: folderService, + features: features, dashboardService: dashboardService, } return s @@ -61,6 +66,8 @@ type SearchService struct { sqlstore db.DB starService star.Service dashboardService dashboards.DashboardService + folderService folder.Service + features featuremgmt.FeatureToggles } func (s *SearchService) SearchHandler(ctx context.Context, query *Query) (model.HitList, error) { @@ -107,6 +114,20 @@ func (s *SearchService) SearchHandler(ctx context.Context, query *Query) (model. dashboardQuery.Sort = sortOpt } + // if folders are stored in unified storage, we need to use the folder service to query for folders + if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) && (query.Type == searchstore.TypeFolder || query.Type == searchstore.TypeAlertFolder) { + hits, err := s.folderService.SearchFolders(ctx, folder.SearchFoldersQuery{ + OrgID: query.OrgId, + UIDs: query.FolderUIDs, + IDs: query.FolderIds, + Title: query.Title, + Limit: query.Limit, + SignedInUser: query.SignedInUser, + }) + + return sortedHits(hits), err + } + hits, err := s.dashboardService.SearchDashboards(ctx, &dashboardQuery) if err != nil { return nil, err diff --git a/pkg/services/search/service_test.go b/pkg/services/search/service_test.go index 297884efbec..01bb5052598 100644 --- a/pkg/services/search/service_test.go +++ b/pkg/services/search/service_test.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/services/star/startest" @@ -35,6 +36,7 @@ func TestSearch_SortedResults(t *testing.T) { sqlstore: db, starService: ss, dashboardService: ds, + features: &featuremgmt.FeatureManager{}, } query := &Query{ @@ -76,6 +78,7 @@ func TestSearch_StarredResults(t *testing.T) { sqlstore: db, starService: ss, dashboardService: ds, + features: &featuremgmt.FeatureManager{}, } query := &Query{ From 4b0f8d8363090e109b2ed7d5f88444713158d870 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Wed, 29 Jan 2025 18:42:06 -0700 Subject: [PATCH 220/894] K8s: Fix search when query is set (#99787) --- .../dashboards/service/dashboard_service.go | 2 ++ pkg/services/dashboards/service/search/search.go | 15 +++++++++++++++ .../folder/folderimpl/folder_unifiedstorage.go | 2 ++ 3 files changed, 19 insertions(+) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index bc193b82b18..d85fb8ed692 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1702,6 +1702,8 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex if query.Title != "" { // allow wildcard search request.Query = "*" + strings.ToLower(query.Title) + "*" + // if using query, you need to specify the fields you want + request.Fields = dashboardsearch.IncludeFields } if len(query.Tags) > 0 { diff --git a/pkg/services/dashboards/service/search/search.go b/pkg/services/dashboards/service/search/search.go index a10f08954f2..6165dfe0237 100644 --- a/pkg/services/dashboards/service/search/search.go +++ b/pkg/services/dashboards/service/search/search.go @@ -19,6 +19,21 @@ var ( resource.SEARCH_FIELD_FOLDER: "", resource.SEARCH_FIELD_TAGS: "", } + + IncludeFields = []string{ + resource.SEARCH_FIELD_TITLE, + resource.SEARCH_FIELD_TAGS, + resource.SEARCH_FIELD_LABELS, + resource.SEARCH_FIELD_FOLDER, + resource.SEARCH_FIELD_CREATED, + resource.SEARCH_FIELD_CREATED_BY, + resource.SEARCH_FIELD_UPDATED, + resource.SEARCH_FIELD_UPDATED_BY, + resource.SEARCH_FIELD_REPOSITORY_NAME, + resource.SEARCH_FIELD_REPOSITORY_PATH, + resource.SEARCH_FIELD_REPOSITORY_HASH, + resource.SEARCH_FIELD_REPOSITORY_TIME, + } ) func ParseResults(result *resource.ResourceSearchResponse, offset int64) (*v0alpha1.SearchResults, error) { diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index fed599819a1..a1473e7558d 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -217,6 +217,8 @@ func (s *Service) searchFoldersFromApiServer(ctx context.Context, query folder.S if query.Title != "" { // allow wildcard search request.Query = "*" + strings.ToLower(query.Title) + "*" + // if using query, you need to specify the fields you want + request.Fields = dashboardsearch.IncludeFields } if query.Limit > 0 { From 3c0383f0d58e47a9f7f519681ba4faf47ccb314f Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 30 Jan 2025 09:59:00 +0300 Subject: [PATCH 221/894] K8s/Test: Allow setting license path in test helper (#99786) --- pkg/tests/apis/client.go | 95 ++++++++++++++++++++++++ pkg/tests/apis/helper.go | 120 +++++++++++-------------------- pkg/tests/apis/openapi_test.go | 56 +-------------- pkg/tests/testinfra/testinfra.go | 8 +++ 4 files changed, 147 insertions(+), 132 deletions(-) create mode 100644 pkg/tests/apis/client.go diff --git a/pkg/tests/apis/client.go b/pkg/tests/apis/client.go new file mode 100644 index 00000000000..f3acfa72a6d --- /dev/null +++ b/pkg/tests/apis/client.go @@ -0,0 +1,95 @@ +package apis + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" +) + +// TypedClient is the struct that implements a typed interface for resource operations +type TypedClient[T any, L any] struct { + Client dynamic.ResourceInterface +} + +func (c *TypedClient[T, L]) Create(ctx context.Context, resource *T, opts metav1.CreateOptions) (*T, error) { + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource) + if err != nil { + return nil, err + } + u := &unstructured.Unstructured{Object: unstructuredObj} + result, err := c.Client.Create(ctx, u, opts) + if err != nil { + return nil, err + } + createdObj := new(T) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, createdObj) + if err != nil { + return nil, err + } + return createdObj, nil +} + +func (c *TypedClient[T, L]) Update(ctx context.Context, resource *T, opts metav1.UpdateOptions) (*T, error) { + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource) + if err != nil { + return nil, err + } + u := &unstructured.Unstructured{Object: unstructuredObj} + result, err := c.Client.Update(ctx, u, opts) + if err != nil { + return nil, err + } + updatedObj := new(T) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, updatedObj) + if err != nil { + return nil, err + } + return updatedObj, nil +} + +func (c *TypedClient[T, L]) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return c.Client.Delete(ctx, name, opts) +} + +func (c *TypedClient[T, L]) Get(ctx context.Context, name string, opts metav1.GetOptions) (*T, error) { + result, err := c.Client.Get(ctx, name, opts) + if err != nil { + return nil, err + } + retrievedObj := new(T) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, retrievedObj) + if err != nil { + return nil, err + } + return retrievedObj, nil +} + +func (c *TypedClient[T, L]) List(ctx context.Context, opts metav1.ListOptions) (*L, error) { + result, err := c.Client.List(ctx, opts) + if err != nil { + return nil, err + } + listObj := new(L) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.UnstructuredContent(), listObj) + if err != nil { + return nil, err + } + return listObj, nil +} + +func (c *TypedClient[T, L]) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*T, error) { + result, err := c.Client.Patch(ctx, name, pt, data, opts, subresources...) + if err != nil { + return nil, err + } + patchedObj := new(T) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, patchedObj) + if err != nil { + return nil, err + } + return patchedObj, nil +} diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index efcd10c9a98..39cc4d8651e 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -8,10 +8,12 @@ import ( "io" "net/http" "os" + "path/filepath" "strconv" "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,7 +21,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer/yaml" - "k8s.io/apimachinery/pkg/types" yamlutil "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" @@ -661,86 +662,49 @@ func (c *K8sTestHelper) CreateTeam(name, email string, orgID int64) team.Team { return team } -// TypedClient is the struct that implements a typed interface for resource operations -type TypedClient[T any, L any] struct { - Client dynamic.ResourceInterface -} +// Compare the OpenAPI schema from one api against a cached snapshot +func VerifyOpenAPISnapshots(t *testing.T, dir string, gv schema.GroupVersion, h *K8sTestHelper) { + if gv.Group == "" { + return // skip invalid groups + } + path := fmt.Sprintf("/openapi/v3/apis/%s/%s", gv.Group, gv.Version) + t.Run(path, func(t *testing.T) { + rsp := DoRequest(h, RequestParams{ + Method: http.MethodGet, + Path: path, + User: h.Org1.Admin, + }, &AnyResource{}) -func (c *TypedClient[T, L]) Create(ctx context.Context, resource *T, opts metav1.CreateOptions) (*T, error) { - unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource) - if err != nil { - return nil, err - } - u := &unstructured.Unstructured{Object: unstructuredObj} - result, err := c.Client.Create(ctx, u, opts) - if err != nil { - return nil, err - } - createdObj := new(T) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, createdObj) - if err != nil { - return nil, err - } - return createdObj, nil -} + require.NotNil(t, rsp.Response) + require.Equal(t, 200, rsp.Response.StatusCode, path) -func (c *TypedClient[T, L]) Update(ctx context.Context, resource *T, opts metav1.UpdateOptions) (*T, error) { - unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource) - if err != nil { - return nil, err - } - u := &unstructured.Unstructured{Object: unstructuredObj} - result, err := c.Client.Update(ctx, u, opts) - if err != nil { - return nil, err - } - updatedObj := new(T) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, updatedObj) - if err != nil { - return nil, err - } - return updatedObj, nil -} + var prettyJSON bytes.Buffer + err := json.Indent(&prettyJSON, rsp.Body, "", " ") + require.NoError(t, err) + pretty := prettyJSON.String() -func (c *TypedClient[T, L]) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { - return c.Client.Delete(ctx, name, opts) -} + write := false + fpath := filepath.Join(dir, fmt.Sprintf("%s-%s.json", gv.Group, gv.Version)) -func (c *TypedClient[T, L]) Get(ctx context.Context, name string, opts metav1.GetOptions) (*T, error) { - result, err := c.Client.Get(ctx, name, opts) - if err != nil { - return nil, err - } - retrievedObj := new(T) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, retrievedObj) - if err != nil { - return nil, err - } - return retrievedObj, nil -} + // nolint:gosec + // We can ignore the gosec G304 warning since this is a test and the function is only called with explicit paths + body, err := os.ReadFile(fpath) + if err == nil { + if !assert.JSONEq(t, string(body), pretty) { + t.Logf("openapi spec has changed: %s", path) + t.Fail() + write = true + } + } else { + t.Errorf("missing openapi spec for: %s", path) + write = true + } -func (c *TypedClient[T, L]) List(ctx context.Context, opts metav1.ListOptions) (*L, error) { - result, err := c.Client.List(ctx, opts) - if err != nil { - return nil, err - } - listObj := new(L) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.UnstructuredContent(), listObj) - if err != nil { - return nil, err - } - return listObj, nil -} - -func (c *TypedClient[T, L]) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*T, error) { - result, err := c.Client.Patch(ctx, name, pt, data, opts, subresources...) - if err != nil { - return nil, err - } - patchedObj := new(T) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, patchedObj) - if err != nil { - return nil, err - } - return patchedObj, nil + if write { + e2 := os.WriteFile(fpath, []byte(pretty), 0644) + if e2 != nil { + t.Errorf("error writing file: %s", e2.Error()) + } + } + }) } diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index a4a7164fa0d..403a543d544 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -1,22 +1,16 @@ package apis import ( - "bytes" "context" "encoding/json" "fmt" - "net/http" - "os" - "path/filepath" "testing" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/version" apimachineryversion "k8s.io/apimachinery/pkg/version" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testsuite" @@ -36,6 +30,7 @@ func TestIntegrationOpenAPIs(t *testing.T) { EnableFeatureToggles: []string{ featuremgmt.FlagKubernetesFoldersServiceV2, // Will be default on by G12 featuremgmt.FlagQueryService, // Query Library + featuremgmt.FlagProvisioning, }, }) @@ -77,50 +72,3 @@ func TestIntegrationOpenAPIs(t *testing.T) { VerifyOpenAPISnapshots(t, dir, gv, h) } } - -// This function should be moved to oss (it is now a duplicate) -func VerifyOpenAPISnapshots(t *testing.T, dir string, gv schema.GroupVersion, h *K8sTestHelper) { - if gv.Group == "" { - return // skip invalid groups - } - path := fmt.Sprintf("/openapi/v3/apis/%s/%s", gv.Group, gv.Version) - t.Run(path, func(t *testing.T) { - rsp := DoRequest(h, RequestParams{ - Method: http.MethodGet, - Path: path, - User: h.Org1.Admin, - }, &AnyResource{}) - - require.NotNil(t, rsp.Response) - require.Equal(t, 200, rsp.Response.StatusCode, path) - - var prettyJSON bytes.Buffer - err := json.Indent(&prettyJSON, rsp.Body, "", " ") - require.NoError(t, err) - pretty := prettyJSON.String() - - write := false - fpath := filepath.Join(dir, fmt.Sprintf("%s-%s.json", gv.Group, gv.Version)) - - // nolint:gosec - // We can ignore the gosec G304 warning since this is a test and the function is only called with explicit paths - body, err := os.ReadFile(fpath) - if err == nil { - if !assert.JSONEq(t, string(body), pretty) { - t.Logf("openapi spec has changed: %s", path) - t.Fail() - write = true - } - } else { - t.Errorf("missing openapi spec for: %s", path) - write = true - } - - if write { - e2 := os.WriteFile(fpath, []byte(pretty), 0644) - if e2 != nil { - t.Errorf("error writing file: %s", e2.Error()) - } - } - }) -} diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index 6010c712356..eaeb4db521d 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -295,6 +295,13 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { _, err = alertingSect.NewKey("max_attempts", "3") require.NoError(t, err) + if opts.LicensePath != "" { + section, err := cfg.NewSection("enterprise") + require.NoError(t, err) + _, err = section.NewKey("license_path", opts.LicensePath) + require.NoError(t, err) + } + rbacSect, err := cfg.NewSection("rbac") require.NoError(t, err) _, err = rbacSect.NewKey("permission_cache", "false") @@ -529,6 +536,7 @@ type GrafanaOpts struct { GrafanaComAPIURL string UnifiedStorageConfig map[string]setting.UnifiedStorageConfig GrafanaComSSOAPIToken string + LicensePath string // When "unified-grpc" is selected it will also start the grpc server APIServerStorageType options.StorageType From 0c120db84d6a6019e2eb9ce3456225afd7830c6e Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Thu, 30 Jan 2025 08:11:50 +0100 Subject: [PATCH 222/894] Provisioning: Setup server and storage (#99757) feat: setup server and storage This simply sets up the API server and its storage for `provisioning.grafana.app`. Features will be added eventually. --- pkg/apiserver/registry/generic/storage.go | 6 + pkg/apiserver/registry/generic/store.go | 5 +- pkg/registry/apis/provisioning/register.go | 160 +++++++++++++++++++++ pkg/registry/apis/wireset.go | 2 + 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 pkg/registry/apis/provisioning/register.go diff --git a/pkg/apiserver/registry/generic/storage.go b/pkg/apiserver/registry/generic/storage.go index 945d51fbd58..67b0efecb8d 100644 --- a/pkg/apiserver/registry/generic/storage.go +++ b/pkg/apiserver/registry/generic/storage.go @@ -29,3 +29,9 @@ func NewRegistryStore(scheme *runtime.Scheme, resourceInfo utils.ResourceInfo, o } return store, nil } + +func NewRegistryStatusStore(scheme *runtime.Scheme, specStore *registry.Store) *StatusREST { + gv := specStore.New().GetObjectKind().GroupVersionKind().GroupVersion() + strategy := NewStatusStrategy(scheme, gv) + return NewStatusREST(specStore, strategy) +} diff --git a/pkg/apiserver/registry/generic/store.go b/pkg/apiserver/registry/generic/store.go index 3cc50bd5445..d8eb09d8cf7 100644 --- a/pkg/apiserver/registry/generic/store.go +++ b/pkg/apiserver/registry/generic/store.go @@ -26,7 +26,10 @@ type StatusREST struct { store *genericregistry.Store } -var _ = rest.Patcher(&StatusREST{}) +var ( + _ rest.Patcher = (*StatusREST)(nil) + _ rest.Storage = (*StatusREST)(nil) +) // New creates a new DataPlaneService object. func (r *StatusREST) New() runtime.Object { diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go new file mode 100644 index 00000000000..3171f55c66a --- /dev/null +++ b/pkg/registry/apis/provisioning/register.go @@ -0,0 +1,160 @@ +package provisioning + +import ( + "context" + "fmt" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/featuremgmt" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/rest" + genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" +) + +var ( + _ builder.APIGroupBuilder = (*APIBuilder)(nil) +) + +type APIBuilder struct{} + +// NewAPIBuilder creates an API builder. +// It avoids anything that is core to Grafana, such that it can be used in a multi-tenant service down the line. +// This means there are no hidden dependencies, and no use of e.g. *settings.Cfg. +func NewAPIBuilder() *APIBuilder { + return &APIBuilder{} +} + +// RegisterAPIService returns an API builder, from [NewAPIBuilder]. It is called by Wire. +// This function happily uses services core to Grafana, and does not need to be multi-tenancy-compatible. +func RegisterAPIService( + features featuremgmt.FeatureToggles, + apiregistration builder.APIRegistrar, +) (*APIBuilder, error) { + if !features.IsEnabledGlobally(featuremgmt.FlagProvisioning) && + !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + return nil, nil // skip registration unless opting into experimental apis OR the feature specifically + } + + builder := NewAPIBuilder() + apiregistration.RegisterAPI(builder) + return builder, nil +} + +func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc( + func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + // TODO: Implement a webhook authoriser somehow. + + // fallback to the standard authorizer + return authorizer.DecisionNoOpinion, "", nil + }) +} + +func (b *APIBuilder) GetGroupVersion() schema.GroupVersion { + return provisioning.SchemeGroupVersion +} + +func (b *APIBuilder) InstallSchema(scheme *runtime.Scheme) error { + err := provisioning.AddToScheme(scheme) + if err != nil { + return err + } + + // This is required for --server-side apply + err = provisioning.AddKnownTypes(provisioning.InternalGroupVersion, scheme) + if err != nil { + return err + } + + metav1.AddToGroupVersion(scheme, provisioning.SchemeGroupVersion) + // Only 1 version (for now?) + return scheme.SetVersionPriority(provisioning.SchemeGroupVersion) +} + +func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { + repositoryStorage, err := grafanaregistry.NewRegistryStore(opts.Scheme, provisioning.RepositoryResourceInfo, opts.OptsGetter) + if err != nil { + return fmt.Errorf("failed to create repository storage: %w", err) + } + + repositoryStatusStorage := grafanaregistry.NewRegistryStatusStore(opts.Scheme, repositoryStorage) + + storage := map[string]rest.Storage{} + storage[provisioning.RepositoryResourceInfo.StoragePath()] = repositoryStorage + storage[provisioning.RepositoryResourceInfo.StoragePath("status")] = repositoryStatusStorage + apiGroupInfo.VersionedResourcesStorageMap[provisioning.VERSION] = storage + return nil +} + +func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + obj := a.GetObject() + + if obj == nil || a.GetOperation() == admission.Connect { + return nil // This is normal for sub-resource + } + + r, ok := obj.(*provisioning.Repository) + if !ok { + return fmt.Errorf("expected repository configuration") + } + + // TODO: Do something based on the resource we got. + _ = r + + return nil +} + +func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { + obj := a.GetObject() + if obj == nil || a.GetOperation() == admission.Connect { + return nil // This is normal for sub-resource + } + + var list field.ErrorList + // TODO: Fill the list with validation errors. + + if len(list) > 0 { + return apierrors.NewInvalid( + provisioning.RepositoryResourceInfo.GroupVersionKind().GroupKind(), + a.GetName(), list) + } + return nil +} + +func (b *APIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { + return provisioning.GetOpenAPIDefinitions +} + +func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartHookFunc, error) { + postStartHooks := map[string]genericapiserver.PostStartHookFunc{ + "grafana-provisioning": func(postStartHookCtx genericapiserver.PostStartHookContext) error { + // TODO: Set up a shared informer for a controller and a watcher with workers. + return nil + }, + } + return postStartHooks, nil +} + +func (b *APIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { + oas.Info.Description = "Provisioning" + + root := "/apis/" + b.GetGroupVersion().String() + "/" + + // The root API discovery list + sub := oas.Paths.Paths[root] + if sub != nil && sub.Get != nil { + sub.Get.Tags = []string{"API Discovery"} // sorts first in the list + } + + return oas, nil +} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 7cedee7d208..8c4ec829b3b 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/iam" "github.com/grafana/grafana/pkg/registry/apis/peakq" + "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/scope" "github.com/grafana/grafana/pkg/registry/apis/service" @@ -40,6 +41,7 @@ var WireSet = wire.NewSet( folders.RegisterAPIService, iam.RegisterAPIService, peakq.RegisterAPIService, + provisioning.RegisterAPIService, service.RegisterAPIService, query.RegisterAPIService, scope.RegisterAPIService, From e61036271a3a6290989a70ca3dfd6be1718816d2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 09:47:58 +0000 Subject: [PATCH 223/894] Update dependency react-select to v5.10.0 (#99775) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 62 +++++++++---------- 12 files changed, 42 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 1e7dde232a2..e7c2c0e8ad2 100644 --- a/package.json +++ b/package.json @@ -381,7 +381,7 @@ "react-router": "5.3.3", "react-router-dom": "5.3.3", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.9.0", + "react-select": "5.10.0", "react-split-pane": "0.1.92", "react-table": "7.8.0", "react-transition-group": "4.4.5", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index a668fa0280b..30d1371c9c4 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -24,7 +24,7 @@ "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", - "react-select": "5.9.0", + "react-select": "5.10.0", "react-use": "17.6.0", "rxjs": "7.8.1", "tslib": "2.8.1" diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index aee6ccc8bbe..6af91236b93 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -66,7 +66,7 @@ "pluralize": "8.0.0", "prismjs": "1.29.0", "react-highlight-words": "0.21.0", - "react-select": "5.9.0", + "react-select": "5.10.0", "react-use": "17.6.0", "react-window": "1.8.11", "rxjs": "7.8.1", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 6bafb8a31ed..87c21909f24 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -25,7 +25,7 @@ "lodash": "4.17.21", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.9.0", + "react-select": "5.10.0", "react-use": "17.6.0", "react-virtualized-auto-sizer": "1.0.25", "rxjs": "7.8.1", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index aa162a98040..a48a1deb146 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -99,7 +99,7 @@ "react-loading-skeleton": "3.5.0", "react-router-dom": "5.3.3", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.9.0", + "react-select": "5.10.0", "react-table": "7.8.0", "react-transition-group": "4.4.5", "react-use": "17.6.0", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 0f8b460420e..dab73a2bc56 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -19,7 +19,7 @@ "prismjs": "1.29.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.9.0", + "react-select": "5.10.0", "react-use": "17.6.0", "rxjs": "7.8.1", "tslib": "2.8.1" diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index b281d43e7a3..8d6bbae331a 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -20,7 +20,7 @@ "prismjs": "1.29.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.9.0", + "react-select": "5.10.0", "react-use": "17.6.0", "rxjs": "7.8.1", "tslib": "2.8.1" diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 47acd088aeb..10ee41620e5 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -15,7 +15,7 @@ "micro-memoize": "^4.1.2", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.9.0", + "react-select": "5.10.0", "react-use": "17.6.0", "rxjs": "7.8.1", "tslib": "2.8.1", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index a882e2093de..b5dd82b92a7 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -15,7 +15,7 @@ "logfmt": "^1.3.2", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.9.0", + "react-select": "5.10.0", "react-window": "1.8.11", "rxjs": "7.8.1", "stream-browserify": "3.0.0", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 55a87c3c5f3..32e5bd74287 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -29,7 +29,7 @@ "prismjs": "1.29.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.9.0", + "react-select": "5.10.0", "react-use": "17.6.0", "rxjs": "7.8.1", "semver": "7.6.3", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index efddda4e217..798087d01f5 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -14,7 +14,7 @@ "lodash": "4.17.21", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.9.0", + "react-select": "5.10.0", "react-use": "17.6.0", "rxjs": "7.8.1", "tslib": "2.8.1" diff --git a/yarn.lock b/yarn.lock index 02276433807..2708ec8a2dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2730,7 +2730,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.9.0" + react-select: "npm:5.10.0" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.1" @@ -2841,7 +2841,7 @@ __metadata: micro-memoize: "npm:^4.1.2" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.9.0" + react-select: "npm:5.10.0" react-use: "npm:17.6.0" rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" @@ -2882,7 +2882,7 @@ __metadata: logfmt: "npm:^1.3.2" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.9.0" + react-select: "npm:5.10.0" react-window: "npm:1.8.11" rxjs: "npm:7.8.1" stream-browserify: "npm:3.0.0" @@ -3023,7 +3023,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.9.0" + react-select: "npm:5.10.0" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.1" @@ -3079,7 +3079,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.9.0" + react-select: "npm:5.10.0" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.1" @@ -3119,7 +3119,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.9.0" + react-select: "npm:5.10.0" react-use: "npm:17.6.0" rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" @@ -3512,7 +3512,7 @@ __metadata: "@types/systemjs": "npm:6.15.1" jest: "npm:^29.6.4" react: "npm:18.3.1" - react-select: "npm:5.9.0" + react-select: "npm:5.10.0" react-use: "npm:17.6.0" rxjs: "npm:7.8.1" ts-jest: "npm:29.2.5" @@ -3674,7 +3674,7 @@ __metadata: react: "npm:18.3.1" react-dom: "npm:18.3.1" react-highlight-words: "npm:0.21.0" - react-select: "npm:5.9.0" + react-select: "npm:5.10.0" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" react-window: "npm:1.8.11" @@ -3897,7 +3897,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.9.0" + react-select: "npm:5.10.0" react-use: "npm:17.6.0" react-virtualized-auto-sizer: "npm:1.0.25" rxjs: "npm:7.8.1" @@ -4109,7 +4109,7 @@ __metadata: react-loading-skeleton: "npm:3.5.0" react-router-dom: "npm:5.3.3" react-router-dom-v5-compat: "npm:^6.26.1" - react-select: "npm:5.9.0" + react-select: "npm:5.10.0" react-select-event: "npm:^5.1.0" react-table: "npm:7.8.0" react-transition-group: "npm:4.4.5" @@ -18044,7 +18044,7 @@ __metadata: react-router: "npm:5.3.3" react-router-dom: "npm:5.3.3" react-router-dom-v5-compat: "npm:^6.26.1" - react-select: "npm:5.9.0" + react-select: "npm:5.10.0" react-select-event: "npm:5.5.1" react-split-pane: "npm:0.1.92" react-table: "npm:7.8.0" @@ -26369,6 +26369,26 @@ __metadata: languageName: node linkType: hard +"react-select@npm:5.10.0": + version: 5.10.0 + resolution: "react-select@npm:5.10.0" + dependencies: + "@babel/runtime": "npm:^7.12.0" + "@emotion/cache": "npm:^11.4.0" + "@emotion/react": "npm:^11.8.1" + "@floating-ui/dom": "npm:^1.0.1" + "@types/react-transition-group": "npm:^4.4.0" + memoize-one: "npm:^6.0.0" + prop-types: "npm:^15.6.0" + react-transition-group: "npm:^4.3.0" + use-isomorphic-layout-effect: "npm:^1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/70ccb74ef31a1fe24c3f7cb94459dd33289999ceda104b1b75256758b7f25ead5dcb4a1877d513f9e22d9ac62d2656414c51dd57b9436d49bd6834a7c8b1c727 + languageName: node + linkType: hard + "react-select@npm:5.8.1": version: 5.8.1 resolution: "react-select@npm:5.8.1" @@ -26389,26 +26409,6 @@ __metadata: languageName: node linkType: hard -"react-select@npm:5.9.0": - version: 5.9.0 - resolution: "react-select@npm:5.9.0" - dependencies: - "@babel/runtime": "npm:^7.12.0" - "@emotion/cache": "npm:^11.4.0" - "@emotion/react": "npm:^11.8.1" - "@floating-ui/dom": "npm:^1.0.1" - "@types/react-transition-group": "npm:^4.4.0" - memoize-one: "npm:^6.0.0" - prop-types: "npm:^15.6.0" - react-transition-group: "npm:^4.3.0" - use-isomorphic-layout-effect: "npm:^1.2.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/2206b6687d6584ff1426056a779d8014ef4d8c4d1d5253dea4f03b01569fdedab8ae6b683ef475ead7da5a824934008b682838d734841d9734e2a8a63b9959fd - languageName: node - linkType: hard - "react-selecto@npm:^1.25.0": version: 1.26.3 resolution: "react-selecto@npm:1.26.3" From 9d5af95565631b0758141242781299516d039ae6 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 30 Jan 2025 12:51:55 +0300 Subject: [PATCH 224/894] K8s/OpenAPI: Remove /watch/ from the openapi spec (#99793) --- .../apis/alerting/notifications/register.go | 5 - .../apis/dashboard/v0alpha1/register.go | 5 - .../apis/dashboard/v1alpha1/register.go | 5 - .../apis/dashboard/v2alpha1/register.go | 6 - .../apis/dashboardsnapshot/register.go | 5 - pkg/registry/apis/datasource/register.go | 5 - pkg/registry/apis/folders/register.go | 6 - pkg/registry/apis/query/register.go | 5 - pkg/registry/apis/scope/register.go | 5 - pkg/services/apiserver/builder/openapi.go | 14 + .../dashboard.grafana.app-v0alpha1.json | 482 +++--------- .../folder.grafana.app-v0alpha1.json | 102 ++- .../peakq.grafana.app-v0alpha1.json | 703 ++++-------------- 13 files changed, 375 insertions(+), 973 deletions(-) diff --git a/pkg/registry/apis/alerting/notifications/register.go b/pkg/registry/apis/alerting/notifications/register.go index d5a7780e978..1adbcac2440 100644 --- a/pkg/registry/apis/alerting/notifications/register.go +++ b/pkg/registry/apis/alerting/notifications/register.go @@ -143,11 +143,6 @@ func (t *NotificationsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3 delete(oas.Paths.Paths, root+templategroup.ResourceInfo.GroupResource().Resource) delete(oas.Paths.Paths, root+routingtree.ResourceInfo.GroupResource().Resource) - // The root API discovery list - sub := oas.Paths.Paths[root] - if sub != nil && sub.Get != nil { - sub.Get.Tags = []string{"API Discovery"} // sorts first in the list - } return oas, nil } diff --git a/pkg/registry/apis/dashboard/v0alpha1/register.go b/pkg/registry/apis/dashboard/v0alpha1/register.go index 535cb0cb08b..c75b159961f 100644 --- a/pkg/registry/apis/dashboard/v0alpha1/register.go +++ b/pkg/registry/apis/dashboard/v0alpha1/register.go @@ -219,11 +219,6 @@ func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op oas.Paths.Paths[root+"search"] = sub delete(oas.Paths.Paths, root+"search/{name}") - // The root API discovery list - sub = oas.Paths.Paths[root] - if sub != nil && sub.Get != nil { - sub.Get.Tags = []string{"API Discovery"} // sorts first in the list - } return oas, nil } diff --git a/pkg/registry/apis/dashboard/v1alpha1/register.go b/pkg/registry/apis/dashboard/v1alpha1/register.go index ea8d6d27df2..5c73464fa9d 100644 --- a/pkg/registry/apis/dashboard/v1alpha1/register.go +++ b/pkg/registry/apis/dashboard/v1alpha1/register.go @@ -209,11 +209,6 @@ func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op delete(oas.Paths.Paths, root+dashboardv1alpha1.DashboardResourceInfo.GroupResource().Resource) delete(oas.Paths.Paths, root+"watch/"+dashboardv1alpha1.DashboardResourceInfo.GroupResource().Resource) - // The root API discovery list - sub := oas.Paths.Paths[root] - if sub != nil && sub.Get != nil { - sub.Get.Tags = []string{"API Discovery"} // sorts first in the list - } return oas, nil } diff --git a/pkg/registry/apis/dashboard/v2alpha1/register.go b/pkg/registry/apis/dashboard/v2alpha1/register.go index bc9e900a8de..f7bacc0afc6 100644 --- a/pkg/registry/apis/dashboard/v2alpha1/register.go +++ b/pkg/registry/apis/dashboard/v2alpha1/register.go @@ -208,13 +208,7 @@ func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op // Hide the ability to list or watch across all tenants delete(oas.Paths.Paths, root+dashboardv2alpha1.DashboardResourceInfo.GroupResource().Resource) - delete(oas.Paths.Paths, root+"watch/"+dashboardv2alpha1.DashboardResourceInfo.GroupResource().Resource) - // The root API discovery list - sub := oas.Paths.Paths[root] - if sub != nil && sub.Get != nil { - sub.Get.Tags = []string{"API Discovery"} // sorts first in the list - } return oas, nil } diff --git a/pkg/registry/apis/dashboardsnapshot/register.go b/pkg/registry/apis/dashboardsnapshot/register.go index c7ab59d5ab5..994ec4d4ce2 100644 --- a/pkg/registry/apis/dashboardsnapshot/register.go +++ b/pkg/registry/apis/dashboardsnapshot/register.go @@ -350,10 +350,5 @@ func (b *SnapshotsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Ope // Hide the invalid endpoint to list all snapshots for all orgs delete(oas.Paths.Paths, "/apis/dashboardsnapshot.grafana.app/v0alpha1/dashboardsnapshots") - // The root API discovery list - sub = oas.Paths.Paths["/apis/dashboardsnapshot.grafana.app/v0alpha1/"] - if sub != nil && sub.Get != nil { - sub.Get.Tags = []string{"API Discovery"} // sorts first in the list - } return oas, nil } diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index 67aafa8cb14..1285faf0b99 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -274,10 +274,5 @@ func (b *DataSourceAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op QueryDescription: fmt.Sprintf("Query the %s datasources", b.pluginJSON.Name), }) - // The root API discovery list - sub := oas.Paths.Paths[root] - if sub != nil && sub.Get != nil { - sub.Get.Tags = []string{"API Discovery"} // sorts first in the list - } return oas, err } diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index c5c352facc8..41beb600817 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -191,13 +191,7 @@ func (b *FolderAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAP // Hide the ability to list or watch across all tenants delete(oas.Paths.Paths, root+v0alpha1.FolderResourceInfo.GroupResource().Resource) - delete(oas.Paths.Paths, root+"watch/"+v0alpha1.FolderResourceInfo.GroupResource().Resource) - // The root API discovery list - sub := oas.Paths.Paths[root] - if sub != nil && sub.Get != nil { - sub.Get.Tags = []string{"API Discovery"} // sorts first in the list - } return oas, nil } diff --git a/pkg/registry/apis/query/register.go b/pkg/registry/apis/query/register.go index 61499f78f03..ddf56be78fd 100644 --- a/pkg/registry/apis/query/register.go +++ b/pkg/registry/apis/query/register.go @@ -251,10 +251,5 @@ func (b *QueryAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI return oas, nil } - // The root API discovery list - sub := oas.Paths.Paths[root] - if sub != nil && sub.Get != nil { - sub.Get.Tags = []string{"API Discovery"} // sorts first in the list - } return oas, nil } diff --git a/pkg/registry/apis/scope/register.go b/pkg/registry/apis/scope/register.go index e413a6c7d47..c1144f519a8 100644 --- a/pkg/registry/apis/scope/register.go +++ b/pkg/registry/apis/scope/register.go @@ -220,10 +220,5 @@ func (b *ScopeAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI oas.Paths.Paths[root+"namespaces/{namespace}/find/scope_dashboard_bindings"] = findDashboardPath } - // The root API discovery list - sub = oas.Paths.Paths[root] - if sub != nil && sub.Get != nil { - sub.Get.Tags = []string{"API Discovery"} // sorts first in the list - } return oas, nil } diff --git a/pkg/services/apiserver/builder/openapi.go b/pkg/services/apiserver/builder/openapi.go index 78089f44628..5ca84226306 100644 --- a/pkg/services/apiserver/builder/openapi.go +++ b/pkg/services/apiserver/builder/openapi.go @@ -66,6 +66,20 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder) func(*s Paths: s.Paths, } + for k := range copy.Paths.Paths { + // Remove the deprecated watch URL -- can use list with ?watch=true + if strings.HasPrefix(k, prefix+"watch/") { + delete(copy.Paths.Paths, k) + continue + } + } + + sub := copy.Paths.Paths[prefix] + if sub != nil && sub.Get != nil { + sub.Get.Tags = []string{"API Discovery"} + sub.Get.Description = "Describe the available kubernetes resources" + } + // Remove the growing list of kinds for k, v := range copy.Components.Schemas { if strings.HasPrefix(k, "io.k8s.apimachinery.pkg.apis.meta.v1") && v.Extensions != nil { diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json index 2cf10939f94..1b0a39351de 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json @@ -7,8 +7,10 @@ "paths": { "/apis/dashboard.grafana.app/v0alpha1/": { "get": { - "tags": ["API Discovery"], - "description": "get available resources", + "tags": [ + "API Discovery" + ], + "description": "Describe the available kubernetes resources", "operationId": "getAPIResources", "responses": { "200": { @@ -36,7 +38,9 @@ }, "/apis/dashboard.grafana.app/v0alpha1/librarypanels": { "get": { - "tags": ["LibraryPanel"], + "tags": [ + "LibraryPanel" + ], "description": "list objects of kind LibraryPanel", "operationId": "listLibraryPanelForAllNamespaces", "responses": { @@ -182,7 +186,9 @@ }, "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/dashboards": { "get": { - "tags": ["Dashboard"], + "tags": [ + "Dashboard" + ], "description": "list or watch objects of kind Dashboard", "operationId": "listDashboard", "parameters": [ @@ -317,7 +323,9 @@ } }, "post": { - "tags": ["Dashboard"], + "tags": [ + "Dashboard" + ], "description": "create a Dashboard", "operationId": "createDashboard", "parameters": [ @@ -429,7 +437,9 @@ } }, "delete": { - "tags": ["Dashboard"], + "tags": [ + "Dashboard" + ], "description": "delete collection of Dashboard", "operationId": "deletecollectionDashboard", "parameters": [ @@ -613,7 +623,9 @@ }, "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/dashboards/{name}": { "get": { - "tags": ["Dashboard"], + "tags": [ + "Dashboard" + ], "description": "read the specified Dashboard", "operationId": "getDashboard", "responses": { @@ -646,7 +658,9 @@ } }, "put": { - "tags": ["Dashboard"], + "tags": [ + "Dashboard" + ], "description": "replace the specified Dashboard", "operationId": "replaceDashboard", "parameters": [ @@ -738,7 +752,9 @@ } }, "delete": { - "tags": ["Dashboard"], + "tags": [ + "Dashboard" + ], "description": "delete a Dashboard", "operationId": "deleteDashboard", "parameters": [ @@ -847,7 +863,9 @@ } }, "patch": { - "tags": ["Dashboard"], + "tags": [ + "Dashboard" + ], "description": "partially update the specified Dashboard", "operationId": "updateDashboard", "parameters": [ @@ -996,7 +1014,9 @@ }, "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/dashboards/{name}/dto": { "get": { - "tags": ["Dashboard"], + "tags": [ + "Dashboard" + ], "description": "connect GET requests to dto of Dashboard", "operationId": "getDashboardDto", "responses": { @@ -1059,7 +1079,9 @@ }, "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/librarypanels": { "get": { - "tags": ["LibraryPanel"], + "tags": [ + "LibraryPanel" + ], "description": "list objects of kind LibraryPanel", "operationId": "listLibraryPanel", "responses": { @@ -1215,7 +1237,9 @@ }, "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/librarypanels/{name}": { "get": { - "tags": ["LibraryPanel"], + "tags": [ + "LibraryPanel" + ], "description": "read the specified LibraryPanel", "operationId": "getLibraryPanel", "responses": { @@ -1281,7 +1305,9 @@ }, "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search": { "get": { - "tags": ["Search"], + "tags": [ + "Search" + ], "description": "Dashboard search", "parameters": [ { @@ -1338,7 +1364,10 @@ "application/json": { "schema": { "type": "object", - "required": ["totalHits", "hits"], + "required": [ + "totalHits", + "hits" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -1396,7 +1425,9 @@ }, "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search/sortable": { "get": { - "tags": ["Search"], + "tags": [ + "Search" + ], "description": "Get sortable fields", "parameters": [ { @@ -1416,7 +1447,9 @@ "application/json": { "schema": { "type": "object", - "required": ["fields"], + "required": [ + "fields" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -1441,329 +1474,7 @@ } } }, - "/apis/dashboard.grafana.app/v0alpha1/search": null, - "/apis/dashboard.grafana.app/v0alpha1/watch/namespaces/{namespace}/dashboards": { - "get": { - "tags": ["Dashboard"], - "description": "watch individual changes to a list of Dashboard. deprecated: use the 'watch' parameter with a list operation instead.", - "operationId": "watchDashboardList", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/json;stream=watch": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/vnd.kubernetes.protobuf;stream=watch": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - } - } - } - }, - "x-kubernetes-action": "watchlist", - "x-kubernetes-group-version-kind": { - "group": "dashboard.grafana.app", - "version": "v0alpha1", - "kind": "Dashboard" - } - }, - "parameters": [ - { - "name": "allowWatchBookmarks", - "in": "query", - "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "continue", - "in": "query", - "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "labelSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "limit", - "in": "query", - "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersion", - "in": "query", - "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersionMatch", - "in": "query", - "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "sendInitialEvents", - "in": "query", - "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "timeoutSeconds", - "in": "query", - "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "watch", - "in": "query", - "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ] - }, - "/apis/dashboard.grafana.app/v0alpha1/watch/namespaces/{namespace}/dashboards/{name}": { - "get": { - "tags": ["Dashboard"], - "description": "watch changes to an object of kind Dashboard. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", - "operationId": "watchDashboard", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/json;stream=watch": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/vnd.kubernetes.protobuf;stream=watch": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - } - } - } - }, - "x-kubernetes-action": "watch", - "x-kubernetes-group-version-kind": { - "group": "dashboard.grafana.app", - "version": "v0alpha1", - "kind": "Dashboard" - } - }, - "parameters": [ - { - "name": "allowWatchBookmarks", - "in": "query", - "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "continue", - "in": "query", - "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "labelSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "limit", - "in": "query", - "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "name", - "in": "path", - "description": "name of the Dashboard", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersion", - "in": "query", - "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersionMatch", - "in": "query", - "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "sendInitialEvents", - "in": "query", - "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "timeoutSeconds", - "in": "query", - "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "watch", - "in": "query", - "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ] - } + "/apis/dashboard.grafana.app/v0alpha1/search": null }, "components": { "schemas": { @@ -1774,7 +1485,9 @@ "datasource": { "description": "The datasource", "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "apiVersion": { "description": "The apiserver version", @@ -1814,7 +1527,9 @@ "resultAssertions": { "description": "Optionally define expected query result behavior", "type": "object", - "required": ["typeVersion"], + "required": [ + "typeVersion" + ], "properties": { "maxFrames": { "description": "Maximum frame count", @@ -1853,19 +1568,26 @@ "timeRange": { "description": "TimeRange represents the query range\nNOTE: unlike generic /ds/query, we can now send explicit time values in each query\nNOTE: the values for timeRange are not saved in a dashboard, they are constructed on the fly", "type": "object", - "required": ["from", "to"], + "required": [ + "from", + "to" + ], "properties": { "from": { "description": "From is the start time of the query.", "type": "string", "default": "now-6h", - "examples": ["now-1h"] + "examples": [ + "now-1h" + ] }, "to": { "description": "To is the end time of the query.", "type": "string", "default": "now", - "examples": ["now"] + "examples": [ + "now" + ] } }, "additionalProperties": false @@ -1885,7 +1607,11 @@ }, "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationActions": { "type": "object", - "required": ["canAdd", "canEdit", "canDelete"], + "required": [ + "canAdd", + "canEdit", + "canDelete" + ], "properties": { "canAdd": { "type": "boolean", @@ -1903,7 +1629,10 @@ }, "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationPermission": { "type": "object", - "required": ["dashboard", "organization"], + "required": [ + "dashboard", + "organization" + ], "properties": { "dashboard": { "default": {}, @@ -1925,7 +1654,9 @@ }, "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard": { "type": "object", - "required": ["spec"], + "required": [ + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -1964,7 +1695,14 @@ "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardAccess": { "description": "Information about how the requesting user can use a given dashboard", "type": "object", - "required": ["canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"], + "required": [ + "canSave", + "canEdit", + "canAdmin", + "canStar", + "canDelete", + "annotationsPermissions" + ], "properties": { "annotationsPermissions": { "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationPermission" @@ -2041,7 +1779,10 @@ "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardWithAccessInfo": { "description": "This is like the legacy DTO where access and metadata are all returned in a single call", "type": "object", - "required": ["spec", "access"], + "required": [ + "spec", + "access" + ], "properties": { "access": { "default": {}, @@ -2087,7 +1828,9 @@ }, "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanel": { "type": "object", - "required": ["spec"], + "required": [ + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -2173,7 +1916,11 @@ }, "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelSpec": { "type": "object", - "required": ["type", "options", "fieldConfig"], + "required": [ + "type", + "options", + "fieldConfig" + ], "properties": { "datasource": { "description": "The default datasource type", @@ -2250,7 +1997,13 @@ "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { "description": "APIResource specifies the name of a resource and whether it is namespaced.", "type": "object", - "required": ["name", "singularName", "namespaced", "kind", "verbs"], + "required": [ + "name", + "singularName", + "namespaced", + "kind", + "verbs" + ], "properties": { "categories": { "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", @@ -2315,7 +2068,10 @@ "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", "type": "object", - "required": ["groupVersion", "resources"], + "required": [ + "groupVersion", + "resources" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -2554,7 +2310,9 @@ } ] }, - "x-kubernetes-list-map-keys": ["uid"], + "x-kubernetes-list-map-keys": [ + "uid" + ], "x-kubernetes-list-type": "map", "x-kubernetes-patch-merge-key": "uid", "x-kubernetes-patch-strategy": "merge" @@ -2576,7 +2334,12 @@ "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", "type": "object", - "required": ["apiVersion", "kind", "name", "uid"], + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], "properties": { "apiVersion": { "description": "API version of the referent.", @@ -2742,7 +2505,10 @@ "io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": { "description": "Event represents a single event to a watched resource.", "type": "object", - "required": ["type", "object"], + "required": [ + "type", + "object" + ], "properties": { "object": { "description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", @@ -2764,4 +2530,4 @@ } } } -} +} \ No newline at end of file diff --git a/pkg/tests/apis/openapi_snapshots/folder.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/folder.grafana.app-v0alpha1.json index 426d681c6bd..709a62018c5 100644 --- a/pkg/tests/apis/openapi_snapshots/folder.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/folder.grafana.app-v0alpha1.json @@ -7,8 +7,10 @@ "paths": { "/apis/folder.grafana.app/v0alpha1/": { "get": { - "tags": ["API Discovery"], - "description": "get available resources", + "tags": [ + "API Discovery" + ], + "description": "Describe the available kubernetes resources", "operationId": "getAPIResources", "responses": { "200": { @@ -36,7 +38,9 @@ }, "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders": { "get": { - "tags": ["Folder"], + "tags": [ + "Folder" + ], "description": "list objects of kind Folder", "operationId": "listFolder", "parameters": [ @@ -171,7 +175,9 @@ } }, "post": { - "tags": ["Folder"], + "tags": [ + "Folder" + ], "description": "create a Folder", "operationId": "createFolder", "parameters": [ @@ -283,7 +289,9 @@ } }, "delete": { - "tags": ["Folder"], + "tags": [ + "Folder" + ], "description": "delete collection of Folder", "operationId": "deletecollectionFolder", "parameters": [ @@ -467,7 +475,9 @@ }, "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}": { "get": { - "tags": ["Folder"], + "tags": [ + "Folder" + ], "description": "read the specified Folder", "operationId": "getFolder", "responses": { @@ -500,7 +510,9 @@ } }, "put": { - "tags": ["Folder"], + "tags": [ + "Folder" + ], "description": "replace the specified Folder", "operationId": "replaceFolder", "parameters": [ @@ -592,7 +604,9 @@ } }, "delete": { - "tags": ["Folder"], + "tags": [ + "Folder" + ], "description": "delete a Folder", "operationId": "deleteFolder", "parameters": [ @@ -701,7 +715,9 @@ } }, "patch": { - "tags": ["Folder"], + "tags": [ + "Folder" + ], "description": "partially update the specified Folder", "operationId": "updateFolder", "parameters": [ @@ -850,7 +866,9 @@ }, "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}/access": { "get": { - "tags": ["Folder"], + "tags": [ + "Folder" + ], "description": "connect GET requests to access of Folder", "operationId": "getFolderAccess", "responses": { @@ -897,7 +915,9 @@ }, "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}/counts": { "get": { - "tags": ["Folder"], + "tags": [ + "Folder" + ], "description": "connect GET requests to counts of Folder", "operationId": "getFolderCounts", "responses": { @@ -944,7 +964,9 @@ }, "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}/parents": { "get": { - "tags": ["Folder"], + "tags": [ + "Folder" + ], "description": "connect GET requests to parents of Folder", "operationId": "getFolderParents", "responses": { @@ -994,7 +1016,9 @@ "schemas": { "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.DescendantCounts": { "type": "object", - "required": ["counts"], + "required": [ + "counts" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -1073,7 +1097,12 @@ "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderAccessInfo": { "description": "Access control information for the current user", "type": "object", - "required": ["canSave", "canEdit", "canAdmin", "canDelete"], + "required": [ + "canSave", + "canEdit", + "canAdmin", + "canDelete" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -1116,7 +1145,10 @@ "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderInfo": { "description": "FolderInfo briefly describes a folder -- unlike a folder resource, this is a partial record of the folder metadata used for navigating parents and children", "type": "object", - "required": ["name", "title"], + "required": [ + "name", + "title" + ], "properties": { "description": { "description": "The folder description", @@ -1160,7 +1192,9 @@ } ] }, - "x-kubernetes-list-map-keys": ["uid"], + "x-kubernetes-list-map-keys": [ + "uid" + ], "x-kubernetes-list-type": "map" }, "kind": { @@ -1235,7 +1269,11 @@ }, "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.ResourceStats": { "type": "object", - "required": ["group", "resource", "count"], + "required": [ + "group", + "resource", + "count" + ], "properties": { "count": { "type": "integer", @@ -1254,7 +1292,9 @@ }, "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Spec": { "type": "object", - "required": ["title"], + "required": [ + "title" + ], "properties": { "description": { "description": "Describe the feature toggle", @@ -1270,7 +1310,13 @@ "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { "description": "APIResource specifies the name of a resource and whether it is namespaced.", "type": "object", - "required": ["name", "singularName", "namespaced", "kind", "verbs"], + "required": [ + "name", + "singularName", + "namespaced", + "kind", + "verbs" + ], "properties": { "categories": { "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", @@ -1335,7 +1381,10 @@ "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", "type": "object", - "required": ["groupVersion", "resources"], + "required": [ + "groupVersion", + "resources" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -1574,7 +1623,9 @@ } ] }, - "x-kubernetes-list-map-keys": ["uid"], + "x-kubernetes-list-map-keys": [ + "uid" + ], "x-kubernetes-list-type": "map", "x-kubernetes-patch-merge-key": "uid", "x-kubernetes-patch-strategy": "merge" @@ -1596,7 +1647,12 @@ "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", "type": "object", - "required": ["apiVersion", "kind", "name", "uid"], + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], "properties": { "apiVersion": { "description": "API version of the referent.", @@ -1761,4 +1817,4 @@ } } } -} +} \ No newline at end of file diff --git a/pkg/tests/apis/openapi_snapshots/peakq.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/peakq.grafana.app-v0alpha1.json index b931b96dde8..83f3b28ee14 100644 --- a/pkg/tests/apis/openapi_snapshots/peakq.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/peakq.grafana.app-v0alpha1.json @@ -6,7 +6,10 @@ "paths": { "/apis/peakq.grafana.app/v0alpha1/": { "get": { - "description": "get available resources", + "tags": [ + "API Discovery" + ], + "description": "Describe the available kubernetes resources", "operationId": "getAPIResources", "responses": { "200": { @@ -34,7 +37,9 @@ }, "/apis/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates": { "get": { - "tags": ["QueryTemplate"], + "tags": [ + "QueryTemplate" + ], "description": "list or watch objects of kind QueryTemplate", "operationId": "listQueryTemplate", "parameters": [ @@ -169,7 +174,9 @@ } }, "post": { - "tags": ["QueryTemplate"], + "tags": [ + "QueryTemplate" + ], "description": "create a QueryTemplate", "operationId": "createQueryTemplate", "parameters": [ @@ -281,7 +288,9 @@ } }, "delete": { - "tags": ["QueryTemplate"], + "tags": [ + "QueryTemplate" + ], "description": "delete collection of QueryTemplate", "operationId": "deletecollectionQueryTemplate", "parameters": [ @@ -465,7 +474,9 @@ }, "/apis/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates/{name}": { "get": { - "tags": ["QueryTemplate"], + "tags": [ + "QueryTemplate" + ], "description": "read the specified QueryTemplate", "operationId": "getQueryTemplate", "responses": { @@ -498,7 +509,9 @@ } }, "put": { - "tags": ["QueryTemplate"], + "tags": [ + "QueryTemplate" + ], "description": "replace the specified QueryTemplate", "operationId": "replaceQueryTemplate", "parameters": [ @@ -590,7 +603,9 @@ } }, "delete": { - "tags": ["QueryTemplate"], + "tags": [ + "QueryTemplate" + ], "description": "delete a QueryTemplate", "operationId": "deleteQueryTemplate", "parameters": [ @@ -699,7 +714,9 @@ } }, "patch": { - "tags": ["QueryTemplate"], + "tags": [ + "QueryTemplate" + ], "description": "partially update the specified QueryTemplate", "operationId": "updateQueryTemplate", "parameters": [ @@ -848,7 +865,9 @@ }, "/apis/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates/{name}/render": { "get": { - "tags": ["QueryTemplate"], + "tags": [ + "QueryTemplate" + ], "description": "connect GET requests to render of QueryTemplate", "operationId": "getQueryTemplateRender", "responses": { @@ -895,7 +914,9 @@ }, "/apis/peakq.grafana.app/v0alpha1/querytemplates": { "get": { - "tags": ["QueryTemplate"], + "tags": [ + "QueryTemplate" + ], "description": "list or watch objects of kind QueryTemplate", "operationId": "listQueryTemplateForAllNamespaces", "responses": { @@ -1060,8 +1081,13 @@ } }, "example": { - "var-another": ["first", "second"], - "var-metricName": ["up"] + "var-another": [ + "first", + "second" + ], + "var-metricName": [ + "up" + ] } } ], @@ -1077,53 +1103,9 @@ "vars": [ { "key": "metricName", - "defaultValues": ["down"] - } - ], - "targets": [ - { - "variables": { - "metricName": [ - { - "path": "$.expr", - "position": { - "start": 0, - "end": 10 - } - }, - { - "path": "$.expr", - "position": { - "start": 13, - "end": 23 - } - } - ] - }, - "properties": { - "refId": "A", - "datasource": { - "type": "prometheus", - "uid": "foo" - }, - "editorMode": "builder", - "expr": "metricName + metricName + 42", - "instant": true, - "range": false, - "exemplar": false - } - } - ] - } - }, - "test2": { - "summary": "hello2", - "value": { - "title": "Test", - "vars": [ - { - "key": "metricName", - "defaultValues": ["down"] + "defaultValues": [ + "down" + ] } ], "targets": [ @@ -1161,6 +1143,54 @@ } ] } + }, + "test2": { + "summary": "hello2", + "value": { + "title": "Test", + "vars": [ + { + "key": "metricName", + "defaultValues": [ + "down" + ] + } + ], + "targets": [ + { + "variables": { + "metricName": [ + { + "path": "$.expr", + "position": { + "start": 0, + "end": 10 + } + }, + { + "path": "$.expr", + "position": { + "start": 13, + "end": 23 + } + } + ] + }, + "properties": { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "foo" + }, + "editorMode": "builder", + "expr": "metricName + metricName + 42", + "instant": true, + "range": false, + "exemplar": false + } + } + ] + } } } } @@ -1197,474 +1227,6 @@ } } } - }, - "/apis/peakq.grafana.app/v0alpha1/watch/namespaces/{namespace}/querytemplates": { - "get": { - "tags": ["QueryTemplate"], - "description": "watch individual changes to a list of QueryTemplate. deprecated: use the 'watch' parameter with a list operation instead.", - "operationId": "watchQueryTemplateList", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/json;stream=watch": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/vnd.kubernetes.protobuf;stream=watch": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - } - } - } - }, - "x-kubernetes-action": "watchlist", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "QueryTemplate" - } - }, - "parameters": [ - { - "name": "allowWatchBookmarks", - "in": "query", - "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "continue", - "in": "query", - "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "labelSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "limit", - "in": "query", - "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersion", - "in": "query", - "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersionMatch", - "in": "query", - "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "sendInitialEvents", - "in": "query", - "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "timeoutSeconds", - "in": "query", - "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "watch", - "in": "query", - "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ] - }, - "/apis/peakq.grafana.app/v0alpha1/watch/namespaces/{namespace}/querytemplates/{name}": { - "get": { - "tags": ["QueryTemplate"], - "description": "watch changes to an object of kind QueryTemplate. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", - "operationId": "watchQueryTemplate", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/json;stream=watch": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/vnd.kubernetes.protobuf;stream=watch": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - } - } - } - }, - "x-kubernetes-action": "watch", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "QueryTemplate" - } - }, - "parameters": [ - { - "name": "allowWatchBookmarks", - "in": "query", - "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "continue", - "in": "query", - "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "labelSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "limit", - "in": "query", - "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "name", - "in": "path", - "description": "name of the QueryTemplate", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersion", - "in": "query", - "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersionMatch", - "in": "query", - "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "sendInitialEvents", - "in": "query", - "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "timeoutSeconds", - "in": "query", - "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "watch", - "in": "query", - "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ] - }, - "/apis/peakq.grafana.app/v0alpha1/watch/querytemplates": { - "get": { - "tags": ["QueryTemplate"], - "description": "watch individual changes to a list of QueryTemplate. deprecated: use the 'watch' parameter with a list operation instead.", - "operationId": "watchQueryTemplateListForAllNamespaces", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/json;stream=watch": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/vnd.kubernetes.protobuf;stream=watch": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" - } - } - } - } - }, - "x-kubernetes-action": "watchlist", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "QueryTemplate" - } - }, - "parameters": [ - { - "name": "allowWatchBookmarks", - "in": "query", - "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "continue", - "in": "query", - "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "labelSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "limit", - "in": "query", - "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersion", - "in": "query", - "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersionMatch", - "in": "query", - "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "sendInitialEvents", - "in": "query", - "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "timeoutSeconds", - "in": "query", - "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "watch", - "in": "query", - "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ] } }, "components": { @@ -1676,7 +1238,9 @@ "datasource": { "description": "The datasource", "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "apiVersion": { "description": "The apiserver version", @@ -1716,7 +1280,9 @@ "resultAssertions": { "description": "Optionally define expected query result behavior", "type": "object", - "required": ["typeVersion"], + "required": [ + "typeVersion" + ], "properties": { "maxFrames": { "description": "Maximum frame count", @@ -1755,19 +1321,26 @@ "timeRange": { "description": "TimeRange represents the query range\nNOTE: unlike generic /ds/query, we can now send explicit time values in each query\nNOTE: the values for timeRange are not saved in a dashboard, they are constructed on the fly", "type": "object", - "required": ["from", "to"], + "required": [ + "from", + "to" + ], "properties": { "from": { "description": "From is the start time of the query.", "type": "string", "default": "now-6h", - "examples": ["now-1h"] + "examples": [ + "now-1h" + ] }, "to": { "description": "To is the end time of the query.", "type": "string", "default": "now", - "examples": ["now"] + "examples": [ + "now" + ] } }, "additionalProperties": false @@ -1859,7 +1432,10 @@ "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Position": { "description": "Position is where to do replacement in the targets during render.", "type": "object", - "required": ["start", "end"], + "required": [ + "start", + "end" + ], "properties": { "end": { "description": "End is the byte offset of the end of the variable.", @@ -1877,7 +1453,9 @@ }, "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.QueryTemplate": { "type": "object", - "required": ["targets"], + "required": [ + "targets" + ], "properties": { "description": { "description": "Longer description for why it is interesting", @@ -1911,14 +1489,19 @@ } ] }, - "x-kubernetes-list-map-keys": ["key"], + "x-kubernetes-list-map-keys": [ + "key" + ], "x-kubernetes-list-type": "map" } } }, "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Target": { "type": "object", - "required": ["variables", "properties"], + "required": [ + "variables", + "properties" + ], "properties": { "dataType": { "description": "DataType is the returned Dataplane type from the query.", @@ -1952,7 +1535,9 @@ "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.TemplateVariable": { "description": "TemplateVariable is the definition of a variable that will be interpolated in targets.", "type": "object", - "required": ["key"], + "required": [ + "key" + ], "properties": { "defaultValues": { "description": "DefaultValue is the value to be used when there is no selected value during render.", @@ -1981,12 +1566,21 @@ "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.VariableReplacement": { "description": "QueryVariable is the definition of a variable that will be interpolated in targets.", "type": "object", - "required": ["path"], + "required": [ + "path" + ], "properties": { "format": { "description": "How values should be interpolated\n\nPossible enum values:\n - `\"csv\"` Formats variables with multiple values as a comma-separated string.\n - `\"doublequote\"` Formats single- and multi-valued variables into a comma-separated string\n - `\"json\"` Formats variables with multiple values as a comma-separated string.\n - `\"pipe\"` Formats variables with multiple values into a pipe-separated string.\n - `\"raw\"` Formats variables with multiple values into comma-separated string. This is the default behavior when no format is specified\n - `\"singlequote\"` Formats single- and multi-valued variables into a comma-separated string", "type": "string", - "enum": ["csv", "doublequote", "json", "pipe", "raw", "singlequote"] + "enum": [ + "csv", + "doublequote", + "json", + "pipe", + "raw", + "singlequote" + ] }, "path": { "description": "Path is the location of the property within a target. The format for this is not figured out yet (Maybe JSONPath?). Idea: [\"string\", int, \"string\"] where int indicates array offset", @@ -2006,7 +1600,13 @@ "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { "description": "APIResource specifies the name of a resource and whether it is namespaced.", "type": "object", - "required": ["name", "singularName", "namespaced", "kind", "verbs"], + "required": [ + "name", + "singularName", + "namespaced", + "kind", + "verbs" + ], "properties": { "categories": { "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", @@ -2071,7 +1671,10 @@ "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", "type": "object", - "required": ["groupVersion", "resources"], + "required": [ + "groupVersion", + "resources" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -2310,7 +1913,9 @@ } ] }, - "x-kubernetes-list-map-keys": ["uid"], + "x-kubernetes-list-map-keys": [ + "uid" + ], "x-kubernetes-list-type": "map", "x-kubernetes-patch-merge-key": "uid", "x-kubernetes-patch-strategy": "merge" @@ -2332,7 +1937,12 @@ "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", "type": "object", - "required": ["apiVersion", "kind", "name", "uid"], + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], "properties": { "apiVersion": { "description": "API version of the referent.", @@ -2498,7 +2108,10 @@ "io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": { "description": "Event represents a single event to a watched resource.", "type": "object", - "required": ["type", "object"], + "required": [ + "type", + "object" + ], "properties": { "object": { "description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", @@ -2520,4 +2133,4 @@ } } } -} +} \ No newline at end of file From 408e3e91a8355fff3ef62315c5f34e6cebdcf8cd Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Thu, 30 Jan 2025 10:24:29 +0000 Subject: [PATCH 225/894] Live: make maximum WebSocket message size configurable (#99770) Co-authored-by: Chris Marchbanks --- conf/defaults.ini | 4 ++++ pkg/services/live/live.go | 12 +++++++----- pkg/setting/setting.go | 7 +++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 38ec12ab7e3..859e0c65d84 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1803,6 +1803,10 @@ preinstall_disabled = false # tuning. 0 disables Live, -1 means unlimited connections. max_connections = 100 +# message_size_limit is the maximum size in bytes of Websocket messages from clients. Defaults to 64KB. +# The limit can be disabled by setting it to -1. +message_size_limit = 65536 + # allowed_origins is a comma-separated list of origins that can establish connection with Grafana Live. # If not set then origin will be matched over root_url. Supports wildcard symbol "*". allowed_origins = diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index feabf87ad83..777c398f083 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -269,12 +269,14 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r originGlobs, _ := setting.GetAllowedOriginGlobs(originPatterns) // error already checked on config load. checkOrigin := getCheckOriginFunc(appURL, originPatterns, originGlobs) + wsCfg := centrifuge.WebsocketConfig{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: checkOrigin, + MessageSizeLimit: cfg.LiveMessageSizeLimit, + } // Use a pure websocket transport. - wsHandler := centrifuge.NewWebsocketHandler(node, centrifuge.WebsocketConfig{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: checkOrigin, - }) + wsHandler := centrifuge.NewWebsocketHandler(node, wsCfg) pushWSHandler := pushws.NewHandler(g.ManagedStreamRunner, pushws.Config{ ReadBufferSize: 1024, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index d70029b4b20..d1f47153138 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -435,6 +435,9 @@ type Cfg struct { // LiveAllowedOrigins is a set of origins accepted by Live. If not provided // then Live uses AppURL as the only allowed origin. LiveAllowedOrigins []string + // LiveMessageSizeLimit is the maximum size in bytes of Websocket messages + // from clients. Defaults to 64KB. + LiveMessageSizeLimit int // Grafana.com URL, used for OAuth redirect. GrafanaComURL string @@ -1974,6 +1977,10 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error { if cfg.LiveMaxConnections < -1 { return fmt.Errorf("unexpected value %d for [live] max_connections", cfg.LiveMaxConnections) } + cfg.LiveMessageSizeLimit = section.Key("message_size_limit").MustInt(65536) + if cfg.LiveMessageSizeLimit < -1 { + return fmt.Errorf("unexpected value %d for [live] message_size_limit", cfg.LiveMaxConnections) + } cfg.LiveHAEngine = section.Key("ha_engine").MustString("") switch cfg.LiveHAEngine { case "", "redis": From c2b44a5da1348a20fa4980bf02bb6f1a6706713f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:26:18 +0000 Subject: [PATCH 226/894] Update dependency @swc/core to v1.10.12 (#99796) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-plugin-configs/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 94 ++++++++++---------- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index e7c2c0e8ad2..47b05197739 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "@rtk-query/codegen-openapi": "^2.0.0", "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", "@stylistic/eslint-plugin-ts": "^2.9.0", - "@swc/core": "1.10.11", + "@swc/core": "1.10.12", "@swc/helpers": "0.5.15", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index e7d6fc1bb66..af69b3119a2 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -8,7 +8,7 @@ }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", - "@swc/core": "1.10.11", + "@swc/core": "1.10.12", "@types/eslint": "9.6.1", "@types/webpack-bundle-analyzer": "^4.7.0", "copy-webpack-plugin": "12.0.2", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 6af91236b93..9df8ba063ed 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -81,7 +81,7 @@ "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-image": "3.0.3", "@rollup/plugin-node-resolve": "16.0.0", - "@swc/core": "1.10.11", + "@swc/core": "1.10.12", "@swc/helpers": "0.5.15", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", diff --git a/yarn.lock b/yarn.lock index 2708ec8a2dc..e50855e34a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3530,7 +3530,7 @@ __metadata: resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs" dependencies: "@grafana/tsconfig": "npm:^2.0.0" - "@swc/core": "npm:1.10.11" + "@swc/core": "npm:1.10.12" "@types/eslint": "npm:9.6.1" "@types/webpack-bundle-analyzer": "npm:^4.7.0" copy-webpack-plugin: "npm:12.0.2" @@ -3616,7 +3616,7 @@ __metadata: "@reduxjs/toolkit": "npm:2.5.1" "@rollup/plugin-image": "npm:3.0.3" "@rollup/plugin-node-resolve": "npm:16.0.0" - "@swc/core": "npm:1.10.11" + "@swc/core": "npm:1.10.12" "@swc/helpers": "npm:0.5.15" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" @@ -8513,90 +8513,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.10.11": - version: 1.10.11 - resolution: "@swc/core-darwin-arm64@npm:1.10.11" +"@swc/core-darwin-arm64@npm:1.10.12": + version: 1.10.12 + resolution: "@swc/core-darwin-arm64@npm:1.10.12" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.10.11": - version: 1.10.11 - resolution: "@swc/core-darwin-x64@npm:1.10.11" +"@swc/core-darwin-x64@npm:1.10.12": + version: 1.10.12 + resolution: "@swc/core-darwin-x64@npm:1.10.12" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.10.11": - version: 1.10.11 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.10.11" +"@swc/core-linux-arm-gnueabihf@npm:1.10.12": + version: 1.10.12 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.10.12" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.10.11": - version: 1.10.11 - resolution: "@swc/core-linux-arm64-gnu@npm:1.10.11" +"@swc/core-linux-arm64-gnu@npm:1.10.12": + version: 1.10.12 + resolution: "@swc/core-linux-arm64-gnu@npm:1.10.12" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.10.11": - version: 1.10.11 - resolution: "@swc/core-linux-arm64-musl@npm:1.10.11" +"@swc/core-linux-arm64-musl@npm:1.10.12": + version: 1.10.12 + resolution: "@swc/core-linux-arm64-musl@npm:1.10.12" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.10.11": - version: 1.10.11 - resolution: "@swc/core-linux-x64-gnu@npm:1.10.11" +"@swc/core-linux-x64-gnu@npm:1.10.12": + version: 1.10.12 + resolution: "@swc/core-linux-x64-gnu@npm:1.10.12" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.10.11": - version: 1.10.11 - resolution: "@swc/core-linux-x64-musl@npm:1.10.11" +"@swc/core-linux-x64-musl@npm:1.10.12": + version: 1.10.12 + resolution: "@swc/core-linux-x64-musl@npm:1.10.12" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.10.11": - version: 1.10.11 - resolution: "@swc/core-win32-arm64-msvc@npm:1.10.11" +"@swc/core-win32-arm64-msvc@npm:1.10.12": + version: 1.10.12 + resolution: "@swc/core-win32-arm64-msvc@npm:1.10.12" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.10.11": - version: 1.10.11 - resolution: "@swc/core-win32-ia32-msvc@npm:1.10.11" +"@swc/core-win32-ia32-msvc@npm:1.10.12": + version: 1.10.12 + resolution: "@swc/core-win32-ia32-msvc@npm:1.10.12" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.10.11": - version: 1.10.11 - resolution: "@swc/core-win32-x64-msvc@npm:1.10.11" +"@swc/core-win32-x64-msvc@npm:1.10.12": + version: 1.10.12 + resolution: "@swc/core-win32-x64-msvc@npm:1.10.12" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core@npm:1.10.11, @swc/core@npm:^1.7.3": - version: 1.10.11 - resolution: "@swc/core@npm:1.10.11" +"@swc/core@npm:1.10.12, @swc/core@npm:^1.7.3": + version: 1.10.12 + resolution: "@swc/core@npm:1.10.12" dependencies: - "@swc/core-darwin-arm64": "npm:1.10.11" - "@swc/core-darwin-x64": "npm:1.10.11" - "@swc/core-linux-arm-gnueabihf": "npm:1.10.11" - "@swc/core-linux-arm64-gnu": "npm:1.10.11" - "@swc/core-linux-arm64-musl": "npm:1.10.11" - "@swc/core-linux-x64-gnu": "npm:1.10.11" - "@swc/core-linux-x64-musl": "npm:1.10.11" - "@swc/core-win32-arm64-msvc": "npm:1.10.11" - "@swc/core-win32-ia32-msvc": "npm:1.10.11" - "@swc/core-win32-x64-msvc": "npm:1.10.11" + "@swc/core-darwin-arm64": "npm:1.10.12" + "@swc/core-darwin-x64": "npm:1.10.12" + "@swc/core-linux-arm-gnueabihf": "npm:1.10.12" + "@swc/core-linux-arm64-gnu": "npm:1.10.12" + "@swc/core-linux-arm64-musl": "npm:1.10.12" + "@swc/core-linux-x64-gnu": "npm:1.10.12" + "@swc/core-linux-x64-musl": "npm:1.10.12" + "@swc/core-win32-arm64-msvc": "npm:1.10.12" + "@swc/core-win32-ia32-msvc": "npm:1.10.12" + "@swc/core-win32-x64-msvc": "npm:1.10.12" "@swc/counter": "npm:^0.1.3" "@swc/types": "npm:^0.1.17" peerDependencies: @@ -8625,7 +8625,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10/249eaa3179ff2ae58d8b7214ea58f4e54b09fa9c7bc2bbfbf1e4d8094955a676c0cc4debab786487af43610771d13f9cefad802e4ce367a0d5f50f18c82fab50 + checksum: 10/62b77009f267f28a486ded1de9ac250b65dc618a4c551a86ad8e438ee54d96b0d44f52332fb242eaa9a6e00db499436753ef99747e6c1a14bd6894554c10e799 languageName: node linkType: hard @@ -17832,7 +17832,7 @@ __metadata: "@rtk-query/codegen-openapi": "npm:^2.0.0" "@rtsao/plugin-proposal-class-properties": "npm:7.0.1-patch.1" "@stylistic/eslint-plugin-ts": "npm:^2.9.0" - "@swc/core": "npm:1.10.11" + "@swc/core": "npm:1.10.12" "@swc/helpers": "npm:0.5.15" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" From 2df05505dbce4ab43335b3fc7a9f80014d6f29cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Thu, 30 Jan 2025 11:44:15 +0100 Subject: [PATCH 227/894] MultiCombobox: Add `Clear all` button (#99668) --- .../Combobox/MultiCombobox.test.tsx | 26 ++++++++++++++ .../src/components/Combobox/MultiCombobox.tsx | 36 +++++++++++++++++-- .../Combobox/getMultiComboboxStyles.ts | 5 +-- public/locales/en-US/grafana.json | 3 ++ public/locales/pseudo-LOCALE/grafana.json | 3 ++ 5 files changed, 68 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx index 8ed509b26db..fa1274b5666 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx @@ -128,6 +128,32 @@ describe('MultiCombobox', () => { expect(await screen.findByText('d')).toBeInTheDocument(); }); + it('should remove value when clicking on the close icon of the pill', async () => { + const options = [ + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + ]; + const onChange = jest.fn(); + render(); + const fistPillRemoveButton = await screen.findByRole('button', { name: 'Remove A' }); + await user.click(fistPillRemoveButton); + expect(onChange).toHaveBeenCalledWith(options.filter((o) => o.value !== 'a')); + }); + + it('should remove all selected items when clicking on clear all button', async () => { + const options = [ + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + ]; + const onChange = jest.fn(); + render(); + const clearAllButton = await screen.findByTitle('Clear all'); + await user.click(clearAllButton); + expect(onChange).toHaveBeenCalledWith([]); + }); + describe('all option', () => { it('should render all option', async () => { const options = [ diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 1d5ab5bd6d8..a2d5063b096 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -6,6 +6,7 @@ import { useCallback, useMemo, useState } from 'react'; import { useStyles2 } from '../../themes'; import { t } from '../../utils/i18n'; import { Checkbox } from '../Forms/Checkbox'; +import { Icon } from '../Icon/Icon'; import { Box } from '../Layout/Box/Box'; import { Stack } from '../Layout/Stack/Stack'; import { Portal } from '../Portal/Portal'; @@ -36,7 +37,8 @@ interface MultiComboboxBaseProps extends Omit = MultiComboboxBaseProps & AutoSizeConditionals; export const MultiCombobox = (props: MultiComboboxProps) => { - const { placeholder, onChange, value, width, enableAllOption, invalid, disabled, minWidth, maxWidth } = props; + const { placeholder, onChange, value, width, enableAllOption, invalid, disabled, minWidth, maxWidth, isClearable } = + props; const styles = useStyles2(getComboboxStyles); const [inputValue, setInputValue] = useState(''); @@ -80,7 +82,7 @@ export const MultiCombobox = (props: MultiComboboxPro [selectedItems] ); - const { getSelectedItemProps, getDropdownProps, setSelectedItems, addSelectedItem, removeSelectedItem } = + const { getSelectedItemProps, getDropdownProps, setSelectedItems, addSelectedItem, removeSelectedItem, reset } = useMultipleSelection({ selectedItems, // initally selected items, onStateChange: ({ type, selectedItems: newSelectedItems }) => { @@ -91,6 +93,7 @@ export const MultiCombobox = (props: MultiComboboxPro case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem: case useMultipleSelection.stateChangeTypes.FunctionAddSelectedItem: case useMultipleSelection.stateChangeTypes.FunctionSetSelectedItems: + case useMultipleSelection.stateChangeTypes.FunctionReset: // Unclear why newSelectedItems would be undefined, but this seems logical onChange(newSelectedItems ?? []); break; @@ -220,7 +223,16 @@ export const MultiCombobox = (props: MultiComboboxPro }); const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(options, isOpen); - const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled, width, minWidth, maxWidth); + const multiStyles = useStyles2( + getMultiComboboxStyles, + isOpen, + invalid, + disabled, + width, + minWidth, + maxWidth, + isClearable + ); const virtualizerOptions = { count: options.length, @@ -284,6 +296,24 @@ export const MultiCombobox = (props: MultiComboboxPro />
+ {isClearable && selectedItems.length > 0 && ( + { + e.stopPropagation(); + reset(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + reset(); + } + }} + /> + )}
diff --git a/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts b/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts index 06bd2dd2a22..3b330240995 100644 --- a/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts +++ b/packages/grafana-ui/src/components/Combobox/getMultiComboboxStyles.ts @@ -12,7 +12,8 @@ export const getMultiComboboxStyles = ( disabled?: boolean, width?: number | 'auto', minWidth?: number, - maxWidth?: number + maxWidth?: number, + isClearable?: boolean ) => { const inputStyles = getInputStyles({ theme, invalid }); const focusStyles = getFocusStyles(theme); @@ -35,7 +36,7 @@ export const getMultiComboboxStyles = ( width: '100%', gap: theme.spacing(0.5), padding: theme.spacing(0.5), - paddingRight: 28, // Account for suffix + paddingRight: isClearable ? theme.spacing(5) : 28, // Account for suffix '&:focus-within': { ...focusStyles, }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 3b6111cb266..6f9cda10abe 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2072,6 +2072,9 @@ "all": { "title": "All", "title-filtered": "All (filtered)" + }, + "clear": { + "title": "Clear all" } }, "nav": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index f04a543f927..6b3f2d081a7 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2072,6 +2072,9 @@ "all": { "title": "Åľľ", "title-filtered": "Åľľ (ƒįľŧęřęđ)" + }, + "clear": { + "title": "Cľęäř äľľ" } }, "nav": { From 4aa495fd025e8906fd2f65976346a989ef16147c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:49:34 +0000 Subject: [PATCH 228/894] Update dependency centrifuge to v5.3.2 (#99797) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 47b05197739..8cc4da3dbc7 100644 --- a/package.json +++ b/package.json @@ -312,7 +312,7 @@ "ansicolor": "2.0.3", "baron": "3.0.3", "brace": "0.11.1", - "centrifuge": "5.3.1", + "centrifuge": "5.3.2", "classnames": "2.5.1", "combokeys": "^3.0.0", "comlink": "4.4.2", diff --git a/yarn.lock b/yarn.lock index e50855e34a9..cbc2f604fa8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12515,13 +12515,13 @@ __metadata: languageName: node linkType: hard -"centrifuge@npm:5.3.1": - version: 5.3.1 - resolution: "centrifuge@npm:5.3.1" +"centrifuge@npm:5.3.2": + version: 5.3.2 + resolution: "centrifuge@npm:5.3.2" dependencies: events: "npm:^3.3.0" protobufjs: "npm:^7.2.5" - checksum: 10/939ed7c3ba1c4964cfe2c0c5d646d66ab016e56e6835f2cdc997ca14bb11c56851935cf2709af12be1c2e542447c077e8bdc07b11ab44d3a80cd14e9ae83508f + checksum: 10/110c88c01206761dffb148f28aa89eb14ec345b564b2613d2aea9cf686db35b77330098df983fd6feed70a97ea4926791ac095c9bc2401894695840173e18eb4 languageName: node linkType: hard @@ -17912,7 +17912,7 @@ __metadata: blob-polyfill: "npm:9.0.20240710" brace: "npm:0.11.1" browserslist: "npm:^4.21.4" - centrifuge: "npm:5.3.1" + centrifuge: "npm:5.3.2" chance: "npm:^1.0.10" chrome-remote-interface: "npm:0.33.2" classnames: "npm:2.5.1" From f637ea225a8f0dd5659de730f6c387c375ff8bb4 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:13:29 +0100 Subject: [PATCH 229/894] Loki: Replace deprecated `@grafana/experimental` with `@grafana/plugin-ui` (#99642) * Loki: Replace @grafana/experimental with @grafana/plugin-ui * Fix imports * Fix incorrect import --- .../datasource/loki/components/AnnotationsQueryEditor.tsx | 2 +- public/app/plugins/datasource/loki/components/LokiContextUi.tsx | 2 +- .../plugins/datasource/loki/components/LokiQueryEditor.test.tsx | 2 +- .../app/plugins/datasource/loki/components/LokiQueryEditor.tsx | 2 +- .../plugins/datasource/loki/configuration/AlertingSettings.tsx | 2 +- .../app/plugins/datasource/loki/configuration/ConfigEditor.tsx | 2 +- .../app/plugins/datasource/loki/configuration/DerivedFields.tsx | 2 +- .../app/plugins/datasource/loki/configuration/QuerySettings.tsx | 2 +- public/app/plugins/datasource/loki/modifyQuery.ts | 2 +- .../plugins/datasource/loki/querybuilder/LokiQueryModeller.ts | 2 +- .../datasource/loki/querybuilder/binaryScalarOperations.ts | 2 +- .../loki/querybuilder/components/LabelParamEditor.test.tsx | 2 +- .../loki/querybuilder/components/LabelParamEditor.tsx | 2 +- .../loki/querybuilder/components/LokiQueryBuilder.tsx | 2 +- .../loki/querybuilder/components/LokiQueryBuilderExplained.tsx | 2 +- .../loki/querybuilder/components/LokiQueryBuilderOptions.tsx | 2 +- .../datasource/loki/querybuilder/components/NestedQuery.tsx | 2 +- .../datasource/loki/querybuilder/components/QueryPattern.tsx | 2 +- .../datasource/loki/querybuilder/components/QueryPreview.tsx | 2 +- .../loki/querybuilder/components/UnwrapParamEditor.test.tsx | 2 +- .../loki/querybuilder/components/UnwrapParamEditor.tsx | 2 +- .../plugins/datasource/loki/querybuilder/operationUtils.test.ts | 2 +- .../app/plugins/datasource/loki/querybuilder/operationUtils.ts | 2 +- public/app/plugins/datasource/loki/querybuilder/operations.ts | 2 +- public/app/plugins/datasource/loki/querybuilder/parsing.ts | 2 +- public/app/plugins/datasource/loki/querybuilder/parsingUtils.ts | 2 +- public/app/plugins/datasource/loki/querybuilder/state.test.ts | 2 +- public/app/plugins/datasource/loki/querybuilder/state.ts | 2 +- public/app/plugins/datasource/loki/querybuilder/types.ts | 2 +- public/app/plugins/datasource/loki/tracking.test.ts | 2 +- public/app/plugins/datasource/loki/tracking.ts | 2 +- 31 files changed, 31 insertions(+), 31 deletions(-) diff --git a/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx b/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx index 907c1f48177..03c30178b6e 100644 --- a/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import { AnnotationQuery } from '@grafana/data'; -import { EditorField, EditorRow } from '@grafana/experimental'; +import { EditorField, EditorRow } from '@grafana/plugin-ui'; import { Input, Stack } from '@grafana/ui'; // Types diff --git a/public/app/plugins/datasource/loki/components/LokiContextUi.tsx b/public/app/plugins/datasource/loki/components/LokiContextUi.tsx index 43c6d63bd26..c4a46e09690 100644 --- a/public/app/plugins/datasource/loki/components/LokiContextUi.tsx +++ b/public/app/plugins/datasource/loki/components/LokiContextUi.tsx @@ -3,7 +3,7 @@ import { useRef, useCallback, useEffect, useMemo, useState } from 'react'; import { useAsync } from 'react-use'; import { dateTime, GrafanaTheme2, LogRowModel, renderMarkdown, SelectableValue } from '@grafana/data'; -import { RawQuery } from '@grafana/experimental'; +import { RawQuery } from '@grafana/plugin-ui'; import { reportInteraction } from '@grafana/runtime'; import { Alert, diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx index 47626e222f7..a821cdf8c38 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { cloneDeep, defaultsDeep } from 'lodash'; import { CoreApp } from '@grafana/data'; -import { QueryEditorMode } from '@grafana/experimental'; +import { QueryEditorMode } from '@grafana/plugin-ui'; import { createLokiDatasource } from '../__mocks__/datasource'; import { EXPLAIN_LABEL_FILTER_CONTENT } from '../querybuilder/components/LokiQueryBuilderExplained'; diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx index eba24c05762..a6da1b06ff8 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx @@ -11,7 +11,7 @@ import { QueryEditorModeToggle, QueryHeaderSwitch, QueryEditorMode, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; import { config, reportInteraction } from '@grafana/runtime'; import { Button, ConfirmModal, Space, Stack } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/loki/configuration/AlertingSettings.tsx b/public/app/plugins/datasource/loki/configuration/AlertingSettings.tsx index 7e3bcc24d54..950b5ca8e7a 100644 --- a/public/app/plugins/datasource/loki/configuration/AlertingSettings.tsx +++ b/public/app/plugins/datasource/loki/configuration/AlertingSettings.tsx @@ -1,5 +1,5 @@ import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/plugin-ui'; import { InlineField, InlineSwitch } from '@grafana/ui'; export function AlertingSettings({ diff --git a/public/app/plugins/datasource/loki/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/loki/configuration/ConfigEditor.tsx index 2908238f7b6..34e5420c53d 100644 --- a/public/app/plugins/datasource/loki/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/loki/configuration/ConfigEditor.tsx @@ -8,7 +8,7 @@ import { Auth, convertLegacyAuthProps, AdvancedHttpSettings, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; import { config, reportInteraction } from '@grafana/runtime'; import { Divider, SecureSocksProxySettings, Stack } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/loki/configuration/DerivedFields.tsx b/public/app/plugins/datasource/loki/configuration/DerivedFields.tsx index 8a72856117e..652824719cb 100644 --- a/public/app/plugins/datasource/loki/configuration/DerivedFields.tsx +++ b/public/app/plugins/datasource/loki/configuration/DerivedFields.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { useCallback, useState } from 'react'; import { GrafanaTheme2, VariableOrigin, DataLinkBuiltInVars } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/plugin-ui'; import { Button, useTheme2 } from '@grafana/ui'; import { DerivedFieldConfig } from '../types'; diff --git a/public/app/plugins/datasource/loki/configuration/QuerySettings.tsx b/public/app/plugins/datasource/loki/configuration/QuerySettings.tsx index c5f956bf472..9aed106dca0 100644 --- a/public/app/plugins/datasource/loki/configuration/QuerySettings.tsx +++ b/public/app/plugins/datasource/loki/configuration/QuerySettings.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { Badge, InlineField, InlineFieldRow, Input } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/loki/modifyQuery.ts b/public/app/plugins/datasource/loki/modifyQuery.ts index af192631f83..429f451ab14 100644 --- a/public/app/plugins/datasource/loki/modifyQuery.ts +++ b/public/app/plugins/datasource/loki/modifyQuery.ts @@ -1,7 +1,6 @@ import { NodeType, SyntaxNode } from '@lezer/common'; import { sortBy } from 'lodash'; -import { QueryBuilderLabelFilter } from '@grafana/experimental'; import { Identifier, LabelFilter, @@ -23,6 +22,7 @@ import { Expr, LabelFormatExpr, } from '@grafana/lezer-logql'; +import { QueryBuilderLabelFilter } from '@grafana/plugin-ui'; import { unescapeLabelValue } from './languageUtils'; import { getNodePositionsFromQuery } from './queryUtils'; diff --git a/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts b/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts index ebb5aeefbb7..079d037a40c 100644 --- a/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts +++ b/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts @@ -4,7 +4,7 @@ import { VisualQuery, QueryBuilderOperation, VisualQueryBinary, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; import { operationDefinitions } from './operations'; import { LokiOperationId, LokiQueryPattern, LokiQueryPatternType, LokiVisualQueryOperationCategory } from './types'; diff --git a/public/app/plugins/datasource/loki/querybuilder/binaryScalarOperations.ts b/public/app/plugins/datasource/loki/querybuilder/binaryScalarOperations.ts index e41955641fd..287efd21fe1 100644 --- a/public/app/plugins/datasource/loki/querybuilder/binaryScalarOperations.ts +++ b/public/app/plugins/datasource/loki/querybuilder/binaryScalarOperations.ts @@ -2,7 +2,7 @@ import { QueryBuilderOperation, QueryBuilderOperationDefinition, QueryBuilderOperationParamDef, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; import { defaultAddOperationHandler } from './operationUtils'; import { LokiOperationId, LokiVisualQueryOperationCategory } from './types'; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.test.tsx index 853039fdd64..7095c769d64 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { ComponentProps } from 'react'; import { DataSourceApi } from '@grafana/data'; -import { QueryBuilderOperation, QueryBuilderOperationParamDef } from '@grafana/experimental'; +import { QueryBuilderOperation, QueryBuilderOperationParamDef } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { createLokiDatasource } from '../../__mocks__/datasource'; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.tsx index f691a652e72..4391f42a2ef 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LabelParamEditor.tsx @@ -7,7 +7,7 @@ import { QueryBuilderOperationParamValue, VisualQuery, VisualQueryModeller, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; import { getOperationParamId } from '../operationUtils'; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index d364ae12dce..96e7a1fa79c 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -14,7 +14,7 @@ import { RawQuery, QueryBuilderLabelFilter, QueryBuilderOperation, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { testIds } from '../../components/LokiQueryEditor'; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx index 5af777c20e7..7829316538a 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; -import { OperationExplainedBox, OperationListExplained, RawQuery } from '@grafana/experimental'; +import { OperationExplainedBox, OperationListExplained, RawQuery } from '@grafana/plugin-ui'; import { Stack } from '@grafana/ui'; import { lokiGrammar } from '../../syntax'; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx index 29d02331867..2d36a70f1af 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx @@ -11,7 +11,7 @@ import { SelectableValue, store, } from '@grafana/data'; -import { EditorField, EditorRow, QueryOptionGroup } from '@grafana/experimental'; +import { EditorField, EditorRow, QueryOptionGroup } from '@grafana/plugin-ui'; import { config, getAppEvents, reportInteraction } from '@grafana/runtime'; import { Alert, AutoSizeInput, RadioButtonGroup, Select } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/NestedQuery.tsx b/public/app/plugins/datasource/loki/querybuilder/components/NestedQuery.tsx index 7ab95dcb781..a22eb8144ec 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/NestedQuery.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/NestedQuery.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { memo } from 'react'; import { GrafanaTheme2, toOption } from '@grafana/data'; -import { EditorRows, FlexItem } from '@grafana/experimental'; +import { EditorRows, FlexItem } from '@grafana/plugin-ui'; import { AutoSizeInput, IconButton, Select, useStyles2 } from '@grafana/ui'; import { LokiDatasource } from '../../datasource'; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/QueryPattern.tsx b/public/app/plugins/datasource/loki/querybuilder/components/QueryPattern.tsx index 574a70509af..7edaf3ead6d 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/QueryPattern.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/QueryPattern.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { RawQuery } from '@grafana/experimental'; +import { RawQuery } from '@grafana/plugin-ui'; import { Button, Card, useStyles2 } from '@grafana/ui'; import logqlGrammar from '../../syntax'; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx b/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx index e5f5b51a482..e84096c9789 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx @@ -1,4 +1,4 @@ -import { EditorRow, EditorFieldGroup, RawQuery } from '@grafana/experimental'; +import { EditorRow, EditorFieldGroup, RawQuery } from '@grafana/plugin-ui'; import { lokiGrammar } from '../../syntax'; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.test.tsx index 467808c92a1..bf69d3587fb 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { ComponentProps } from 'react'; import { DataFrame, DataSourceApi, FieldType, toDataFrame } from '@grafana/data'; -import { QueryBuilderOperation, QueryBuilderOperationParamDef } from '@grafana/experimental'; +import { QueryBuilderOperation, QueryBuilderOperationParamDef } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { createLokiDatasource } from '../../__mocks__/datasource'; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx b/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx index c50b8b5ae54..630cb285568 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { SelectableValue, getDefaultTimeRange, toOption } from '@grafana/data'; -import { QueryBuilderOperationParamEditorProps, VisualQueryModeller } from '@grafana/experimental'; +import { QueryBuilderOperationParamEditorProps, VisualQueryModeller } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { Select } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/loki/querybuilder/operationUtils.test.ts b/public/app/plugins/datasource/loki/querybuilder/operationUtils.test.ts index 4ea7b5fed24..c6c98c1d478 100644 --- a/public/app/plugins/datasource/loki/querybuilder/operationUtils.test.ts +++ b/public/app/plugins/datasource/loki/querybuilder/operationUtils.test.ts @@ -1,4 +1,4 @@ -import { QueryBuilderOperation, QueryBuilderOperationDefinition } from '@grafana/experimental'; +import { QueryBuilderOperation, QueryBuilderOperationDefinition } from '@grafana/plugin-ui'; import { createAggregationOperation, diff --git a/public/app/plugins/datasource/loki/querybuilder/operationUtils.ts b/public/app/plugins/datasource/loki/querybuilder/operationUtils.ts index 2ac580b9dc3..a3eb6702008 100644 --- a/public/app/plugins/datasource/loki/querybuilder/operationUtils.ts +++ b/public/app/plugins/datasource/loki/querybuilder/operationUtils.ts @@ -8,7 +8,7 @@ import { QueryBuilderOperationParamValue, VisualQuery, VisualQueryModeller, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; import { escapeLabelValueInExactSelector } from '../languageUtils'; import { FUNCTIONS } from '../syntax'; diff --git a/public/app/plugins/datasource/loki/querybuilder/operations.ts b/public/app/plugins/datasource/loki/querybuilder/operations.ts index 5fb91ce0f2f..65c40392005 100644 --- a/public/app/plugins/datasource/loki/querybuilder/operations.ts +++ b/public/app/plugins/datasource/loki/querybuilder/operations.ts @@ -1,4 +1,4 @@ -import { QueryBuilderOperationDefinition, QueryBuilderOperationParamValue } from '@grafana/experimental'; +import { QueryBuilderOperationDefinition, QueryBuilderOperationParamValue } from '@grafana/plugin-ui'; import { binaryScalarOperations } from './binaryScalarOperations'; import { UnwrapParamEditor } from './components/UnwrapParamEditor'; diff --git a/public/app/plugins/datasource/loki/querybuilder/parsing.ts b/public/app/plugins/datasource/loki/querybuilder/parsing.ts index ea2f0003e84..200746b154f 100644 --- a/public/app/plugins/datasource/loki/querybuilder/parsing.ts +++ b/public/app/plugins/datasource/loki/querybuilder/parsing.ts @@ -1,6 +1,5 @@ import { SyntaxNode } from '@lezer/common'; -import { QueryBuilderLabelFilter, QueryBuilderOperation, QueryBuilderOperationParamValue } from '@grafana/experimental'; import { And, BinOpExpr, @@ -54,6 +53,7 @@ import { OnOrIgnoringModifier, OrFilter, } from '@grafana/lezer-logql'; +import { QueryBuilderLabelFilter, QueryBuilderOperation, QueryBuilderOperationParamValue } from '@grafana/plugin-ui'; import { binaryScalarDefs } from './binaryScalarOperations'; import { checkParamsAreValid, getDefinitionById } from './operations'; diff --git a/public/app/plugins/datasource/loki/querybuilder/parsingUtils.ts b/public/app/plugins/datasource/loki/querybuilder/parsingUtils.ts index adc17113435..bb545d4e053 100644 --- a/public/app/plugins/datasource/loki/querybuilder/parsingUtils.ts +++ b/public/app/plugins/datasource/loki/querybuilder/parsingUtils.ts @@ -1,6 +1,6 @@ import { SyntaxNode, TreeCursor } from '@lezer/common'; -import { QueryBuilderOperation, QueryBuilderOperationParamValue } from '@grafana/experimental'; +import { QueryBuilderOperation, QueryBuilderOperationParamValue } from '@grafana/plugin-ui'; // Although 0 isn't explicitly provided in the @grafana/lezer-logql library as the error node ID, it does appear to be the ID of error nodes within lezer. export const ErrorId = 0; diff --git a/public/app/plugins/datasource/loki/querybuilder/state.test.ts b/public/app/plugins/datasource/loki/querybuilder/state.test.ts index 8335414f6e6..89fbc81b4d4 100644 --- a/public/app/plugins/datasource/loki/querybuilder/state.test.ts +++ b/public/app/plugins/datasource/loki/querybuilder/state.test.ts @@ -1,4 +1,4 @@ -import { QueryEditorMode } from '@grafana/experimental'; +import { QueryEditorMode } from '@grafana/plugin-ui'; import { changeEditorMode, getQueryWithDefaults } from './state'; diff --git a/public/app/plugins/datasource/loki/querybuilder/state.ts b/public/app/plugins/datasource/loki/querybuilder/state.ts index 590aeffdd8a..5f8aaecad55 100644 --- a/public/app/plugins/datasource/loki/querybuilder/state.ts +++ b/public/app/plugins/datasource/loki/querybuilder/state.ts @@ -1,4 +1,4 @@ -import { QueryEditorMode } from '@grafana/experimental'; +import { QueryEditorMode } from '@grafana/plugin-ui'; import { LokiQuery, LokiQueryType } from '../types'; diff --git a/public/app/plugins/datasource/loki/querybuilder/types.ts b/public/app/plugins/datasource/loki/querybuilder/types.ts index 055edb20dc8..0d655c54172 100644 --- a/public/app/plugins/datasource/loki/querybuilder/types.ts +++ b/public/app/plugins/datasource/loki/querybuilder/types.ts @@ -3,7 +3,7 @@ import { QueryBuilderLabelFilter, QueryBuilderOperation, BINARY_OPERATIONS_KEY, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; /** * Visual query model diff --git a/public/app/plugins/datasource/loki/tracking.test.ts b/public/app/plugins/datasource/loki/tracking.test.ts index 7048692c7f1..2c46e75ffe2 100644 --- a/public/app/plugins/datasource/loki/tracking.test.ts +++ b/public/app/plugins/datasource/loki/tracking.test.ts @@ -1,5 +1,5 @@ import { CoreApp, DashboardLoadedEvent, DataQueryRequest, dateTime } from '@grafana/data'; -import { QueryEditorMode } from '@grafana/experimental'; +import { QueryEditorMode } from '@grafana/plugin-ui'; import { reportInteraction } from '@grafana/runtime'; import pluginJson from './plugin.json'; diff --git a/public/app/plugins/datasource/loki/tracking.ts b/public/app/plugins/datasource/loki/tracking.ts index 1b1ebfcc7cc..6810ce40876 100644 --- a/public/app/plugins/datasource/loki/tracking.ts +++ b/public/app/plugins/datasource/loki/tracking.ts @@ -1,5 +1,5 @@ import { CoreApp, DashboardLoadedEvent, DataQueryRequest, DataQueryResponse } from '@grafana/data'; -import { QueryEditorMode } from '@grafana/experimental'; +import { QueryEditorMode } from '@grafana/plugin-ui'; import { reportInteraction, config } from '@grafana/runtime'; import { From 1e3783cc116de3f2624796a768cbaacf3e8649c1 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 30 Jan 2025 13:40:37 +0100 Subject: [PATCH 230/894] Grafana Advisor: Add links to suggested actions (#99764) Grafana Advisor: Add links to proposed actions --- apps/advisor/pkg/app/checks/datasourcecheck/check.go | 10 ++++++---- .../pkg/app/checks/datasourcecheck/check_test.go | 4 ++-- apps/advisor/pkg/app/checks/plugincheck/check.go | 8 +++++--- apps/advisor/pkg/app/checks/plugincheck/check_test.go | 10 +++++----- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go index 7aede0c65ec..8ff4c1a1dc3 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -56,8 +56,8 @@ func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.Check if err != nil { dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{ Severity: advisor.CheckStatusSeverityLow, - Reason: fmt.Sprintf("Invalid UID: %s", ds.UID), - Action: "Change UID", + Reason: fmt.Sprintf("Invalid UID '%s' for data source %s", ds.UID, ds.Name), + Action: "Check the documentation for more information.", }) } @@ -83,8 +83,10 @@ func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.Check if resp.Status != backend.HealthStatusOk { dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{ Severity: advisor.CheckStatusSeverityHigh, - Reason: fmt.Sprintf("Health check failed: %s", ds.Name), - Action: "Check datasource", + Reason: fmt.Sprintf("Health check failed for %s", ds.Name), + Action: fmt.Sprintf( + "Go to the data source configuration"+ + " and address the issues reported.", ds.UID), }) } } diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go index ee489483bd2..f51fdb77fbb 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go @@ -56,7 +56,7 @@ func TestCheck_Run(t *testing.T) { assert.NoError(t, err) assert.Equal(t, int64(1), report.Count) assert.Len(t, report.Errors, 1) - assert.Equal(t, "Invalid UID: invalid uid", report.Errors[0].Reason) + assert.Equal(t, "Invalid UID 'invalid uid' for data source Prometheus", report.Errors[0].Reason) }) t.Run("should return errors when datasource health check fails", func(t *testing.T) { @@ -79,7 +79,7 @@ func TestCheck_Run(t *testing.T) { assert.NoError(t, err) assert.Equal(t, int64(1), report.Count) assert.Len(t, report.Errors, 1) - assert.Equal(t, "Health check failed: Prometheus", report.Errors[0].Reason) + assert.Equal(t, "Health check failed for Prometheus", report.Errors[0].Reason) }) } diff --git a/apps/advisor/pkg/app/checks/plugincheck/check.go b/apps/advisor/pkg/app/checks/plugincheck/check.go index 2d0f10f7a72..793fc935946 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check.go @@ -59,7 +59,7 @@ func (c *check) Run(ctx context.Context, _ *advisor.CheckSpec) (*advisor.CheckV0 errs = append(errs, advisor.CheckV0alpha1StatusReportErrors{ Severity: advisor.CheckStatusSeverityHigh, Reason: fmt.Sprintf("Plugin deprecated: %s", p.ID), - Action: "Look for alternatives", + Action: "Check the documentation for recommended steps.", }) } @@ -75,8 +75,10 @@ func (c *check) Run(ctx context.Context, _ *advisor.CheckSpec) (*advisor.CheckV0 if hasUpdate(p, info) { errs = append(errs, advisor.CheckV0alpha1StatusReportErrors{ Severity: advisor.CheckStatusSeverityLow, - Reason: fmt.Sprintf("New version available: %s", p.ID), - Action: "Update plugin", + Reason: fmt.Sprintf("New version available for %s", p.ID), + Action: fmt.Sprintf( + "Go to the plugin admin page"+ + " and upgrade to the latest version.", p.ID), }) } } diff --git a/apps/advisor/pkg/app/checks/plugincheck/check_test.go b/apps/advisor/pkg/app/checks/plugincheck/check_test.go index a831f41b67f..3fa7f1b20cb 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check_test.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check_test.go @@ -43,7 +43,7 @@ func TestRun(t *testing.T) { { Severity: advisor.CheckStatusSeverityHigh, Reason: "Plugin deprecated: plugin1", - Action: "Look for alternatives", + Action: "Check the documentation for recommended steps.", }, }, }, @@ -61,8 +61,8 @@ func TestRun(t *testing.T) { expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{ { Severity: advisor.CheckStatusSeverityLow, - Reason: "New version available: plugin2", - Action: "Update plugin", + Reason: "New version available for plugin2", + Action: "Go to the plugin admin page and upgrade to the latest version.", }, }, }, @@ -80,8 +80,8 @@ func TestRun(t *testing.T) { expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{ { Severity: advisor.CheckStatusSeverityLow, - Reason: "New version available: plugin2", - Action: "Update plugin", + Reason: "New version available for plugin2", + Action: "Go to the plugin admin page and upgrade to the latest version.", }, }, }, From 800c9fa3e6c2d448a91111f923132bc5d972a4cb Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Thu, 30 Jan 2025 14:24:37 +0100 Subject: [PATCH 231/894] SchemaV2: Rows in dashboard schema v2 (#99239) * Testing out rows in schemav2 * update schema * loading sort of works * descibe position in relation to row * add row repeats by variable * explain ts-expect-error * Save repeats as well * Update tests for repeat behavior of rows * Don't add the clones of the repeated rows * Add row support for response transformer for V2 * Add row actions * fix panel name * fix merge issue * fix tests * Implement ensureV1 * set key of GridRow * fix lint issue * When going from V2 to V1 rows should be assigned unique ids following max panel id * remove old comment * Add panel repeats in V2 -> V1 transform --- .../dashboard/v2alpha0/dashboard.schema.cue | 20 +- .../src/schema/dashboard/v2alpha0/examples.ts | 86 +++- .../schema/dashboard/v2alpha0/types.gen.ts | 37 +- ...sformSceneToSaveModelSchemaV2.test.ts.snap | 57 ++- .../dashboard-scene/serialization/const.ts | 2 + .../transformSaveModelSchemaV2ToScene.test.ts | 21 +- .../transformSaveModelSchemaV2ToScene.ts | 59 ++- .../transformSceneToSaveModel.ts | 1 + .../transformSceneToSaveModelSchemaV2.test.ts | 29 ++ .../transformSceneToSaveModelSchemaV2.ts | 66 ++- .../api/ResponseTransformers.test.ts | 119 +++++- .../dashboard/api/ResponseTransformers.ts | 399 ++++++++++++------ public/app/features/dashboard/api/utils.ts | 2 + 13 files changed, 717 insertions(+), 181 deletions(-) diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 5f164f1aded..170094040c1 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -483,6 +483,11 @@ RepeatOptions: { maxPerRow?: int } +RowRepeatOptions: { + mode: RepeatMode, + value: string +} + GridLayoutItemSpec: { x: int y: int @@ -497,8 +502,21 @@ GridLayoutItemKind: { spec: GridLayoutItemSpec } +GridLayoutRowKind: { + kind: "GridLayoutRow" + spec: GridLayoutRowSpec +} + +GridLayoutRowSpec: { + y: int + collapsed: bool + title: string + elements: [...GridLayoutItemKind] // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard. + repeat?: RowRepeatOptions +} + GridLayoutSpec: { - items: [...GridLayoutItemKind] + items: [...GridLayoutItemKind | GridLayoutRowKind] } GridLayoutKind: { diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts index ed513da75c4..340ffeeef12 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts @@ -191,6 +191,60 @@ export const handyTestingSchema: DashboardV2Spec = { }, }, }, + 'panel-3': { + kind: 'Panel', + spec: { + data: { + kind: 'QueryGroup', + spec: { + queries: [ + { + kind: 'PanelQuery', + spec: { + refId: 'A', + datasource: { + type: 'prometheus', + uid: 'datasource1', + }, + query: { + kind: 'prometheus', + spec: { + expr: 'test-query', + }, + }, + hidden: false, + }, + }, + ], + queryOptions: { + timeFrom: '1h', + maxDataPoints: 100, + timeShift: '1h', + queryCachingTTL: 60, + interval: '1m', + cacheTimeout: '1m', + hideTimeOverride: false, + }, + transformations: [], + }, + }, + description: 'Test Description', + links: [], + title: 'Test Panel 3', + id: 3, + vizConfig: { + kind: 'timeseries', + spec: { + fieldConfig: { + defaults: {}, + overrides: [], + }, + options: {}, + pluginVersion: '7.0.0', + }, + }, + }, + }, }, layout: { kind: 'GridLayout', @@ -203,8 +257,8 @@ export const handyTestingSchema: DashboardV2Spec = { kind: 'ElementReference', name: 'panel-1', }, - height: 100, - width: 200, + height: 10, + width: 10, x: 0, y: 0, repeat: { @@ -221,18 +275,42 @@ export const handyTestingSchema: DashboardV2Spec = { kind: 'ElementReference', name: 'panel-2', }, - height: 100, + height: 10, width: 200, x: 0, y: 2, }, }, + { + kind: 'GridLayoutRow', + spec: { + y: 20, + collapsed: false, + title: 'Row 1', + repeat: { value: 'customVar', mode: 'variable' }, + elements: [ + { + kind: 'GridLayoutItem', + spec: { + element: { + kind: 'ElementReference', + name: 'panel-3', + }, + height: 10, + width: 10, + x: 0, + y: 0, + }, + }, + ], + }, + }, ], }, }, links: [ { - asDropdown: false, + asDropdown: true, icon: '', includeVars: false, keepTime: false, diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index 7b7ef1cda07..0e41bb7fedc 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -700,6 +700,16 @@ export const defaultRepeatOptions = (): RepeatOptions => ({ value: "", }); +export interface RowRepeatOptions { + mode: "variable"; + value: string; +} + +export const defaultRowRepeatOptions = (): RowRepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + export interface GridLayoutItemSpec { x: number; y: number; @@ -728,8 +738,33 @@ export const defaultGridLayoutItemKind = (): GridLayoutItemKind => ({ spec: defaultGridLayoutItemSpec(), }); +export interface GridLayoutRowKind { + kind: "GridLayoutRow"; + spec: GridLayoutRowSpec; +} + +export const defaultGridLayoutRowKind = (): GridLayoutRowKind => ({ + kind: "GridLayoutRow", + spec: defaultGridLayoutRowSpec(), +}); + +export interface GridLayoutRowSpec { + y: number; + collapsed: boolean; + title: string; + elements: GridLayoutItemKind[]; + repeat?: RowRepeatOptions; +} + +export const defaultGridLayoutRowSpec = (): GridLayoutRowSpec => ({ + y: 0, + collapsed: false, + title: "", + elements: [], +}); + export interface GridLayoutSpec { - items: GridLayoutItemKind[]; + items: (GridLayoutItemKind | GridLayoutRowKind)[]; } export const defaultGridLayoutSpec = (): GridLayoutSpec => ({ diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap index 3ba7caea2c7..a62ba3c1daf 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap @@ -88,6 +88,34 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model }, }, }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "queryOptions": {}, + "transformations": [], + }, + }, + "description": "Test Description 2", + "id": 2, + "links": [], + "title": "Test Panel 2", + "vizConfig": { + "kind": "graph", + "spec": { + "fieldConfig": { + "defaults": {}, + "overrides": [], + }, + "options": {}, + "pluginVersion": "7.0.0", + }, + }, + }, + }, }, "layout": { "kind": "GridLayout", @@ -100,12 +128,39 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model "kind": "ElementReference", "name": "panel-1", }, - "height": 0, + "height": 10, "width": 0, "x": 0, "y": 0, }, }, + { + "kind": "GridLayoutRow", + "spec": { + "collapsed": false, + "elements": [ + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2", + }, + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + }, + ], + "repeat": { + "mode": "variable", + "value": "customVar", + }, + "title": "Test Row", + "y": 10, + }, + }, ], }, }, diff --git a/public/app/features/dashboard-scene/serialization/const.ts b/public/app/features/dashboard-scene/serialization/const.ts index f7e03e4328b..96ba9a094d3 100644 --- a/public/app/features/dashboard-scene/serialization/const.ts +++ b/public/app/features/dashboard-scene/serialization/const.ts @@ -2,3 +2,5 @@ export const GRAFANA_DATASOURCE_REF = { name: 'grafana', uid: 'grafana', }; + +export const GRID_ROW_HEIGHT = 1; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 0b8fc1ee77d..64df731435d 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -13,6 +13,7 @@ import { GroupByVariable, AdHocFiltersVariable, SceneDataTransformer, + SceneGridRow, } from '@grafana/scenes'; import { AdhocVariableKind, @@ -20,6 +21,7 @@ import { CustomVariableKind, DashboardV2Spec, DatasourceVariableKind, + GridLayoutItemSpec, GroupByVariableKind, IntervalVariableKind, QueryVariableKind, @@ -44,7 +46,7 @@ import { } from './transformSaveModelSchemaV2ToScene'; import { transformCursorSynctoEnum } from './transformToV2TypesUtils'; -const defaultDashboard: DashboardWithAccessInfo = { +export const defaultDashboard: DashboardWithAccessInfo = { kind: 'DashboardWithAccessInfo', metadata: { name: 'dashboard-uid', @@ -220,16 +222,16 @@ describe('transformSaveModelSchemaV2ToScene', () => { // VizPanel const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels).toHaveLength(2); + expect(vizPanels).toHaveLength(3); // Layout const layout = scene.state.body as DefaultGridLayoutManager; // Panel const panel = getPanelElement(dash, 'panel-1')!; - expect(layout.state.grid.state.children.length).toBe(2); + expect(layout.state.grid.state.children.length).toBe(3); expect(layout.state.grid.state.children[0].state.key).toBe(`grid-item-${panel.spec.id}`); - const gridLayoutItemSpec = dash.layout.spec.items[0].spec; + const gridLayoutItemSpec = dash.layout.spec.items[0].spec as GridLayoutItemSpec; expect(layout.state.grid.state.children[0].state.width).toBe(gridLayoutItemSpec.width); expect(layout.state.grid.state.children[0].state.height).toBe(gridLayoutItemSpec.height); expect(layout.state.grid.state.children[0].state.x).toBe(gridLayoutItemSpec.x); @@ -240,7 +242,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { // Library Panel const libraryPanel = getLibraryPanelElement(dash, 'panel-2')!; expect(layout.state.grid.state.children[1].state.key).toBe(`grid-item-${libraryPanel.spec.id}`); - const libraryGridLayoutItemSpec = dash.layout.spec.items[1].spec; + const libraryGridLayoutItemSpec = dash.layout.spec.items[1].spec as GridLayoutItemSpec; expect(layout.state.grid.state.children[1].state.width).toBe(libraryGridLayoutItemSpec.width); expect(layout.state.grid.state.children[1].state.height).toBe(libraryGridLayoutItemSpec.height); expect(layout.state.grid.state.children[1].state.x).toBe(libraryGridLayoutItemSpec.x); @@ -248,6 +250,9 @@ describe('transformSaveModelSchemaV2ToScene', () => { const vizLibraryPanel = vizPanels.find((p) => p.state.key === 'panel-2')!; validateVizPanel(vizLibraryPanel, dash); + expect((layout.state.grid.state.children[2] as SceneGridRow).state.isCollapsed).toBe(false); + expect((layout.state.grid.state.children[2] as SceneGridRow).state.y).toBe(20); + // Transformations const panelWithTransformations = vizPanels.find((p) => p.state.key === 'panel-1')!; expect((panelWithTransformations.state.$data as SceneDataTransformer)?.state.transformations[0]).toEqual( @@ -278,7 +283,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { const scene = transformSaveModelSchemaV2ToScene(dashboard); const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels.length).toBe(2); + expect(vizPanels.length).toBe(3); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.type).toBe('mixed'); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME); }); @@ -306,7 +311,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { const scene = transformSaveModelSchemaV2ToScene(dashboard); const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels.length).toBe(2); + expect(vizPanels.length).toBe(3); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource).toBeUndefined(); }); @@ -330,7 +335,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { const scene = transformSaveModelSchemaV2ToScene(dashboard); const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels.length).toBe(2); + expect(vizPanels.length).toBe(3); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.type).toBe('mixed'); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 579f74b44d3..4a7b005805d 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -16,6 +16,7 @@ import { SceneDataTransformer, SceneGridItemLike, SceneGridLayout, + SceneGridRow, SceneObject, SceneQueryRunner, SceneRefreshPicker, @@ -44,6 +45,7 @@ import { defaultIntervalVariableKind, defaultQueryVariableKind, defaultTextVariableKind, + GridLayoutItemSpec, GroupByVariableKind, IntervalVariableKind, LibraryPanelKind, @@ -79,13 +81,16 @@ import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { PanelNotices } from '../scene/PanelNotices'; import { PanelTimeRange } from '../scene/PanelTimeRange'; +import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { AngularDeprecation } from '../scene/angular/AngularDeprecation'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; +import { RowActions } from '../scene/row-actions/RowActions'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState'; import { getDashboardSceneFor, getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils'; +import { GRID_ROW_HEIGHT } from './const'; import { SnapshotVariable } from './custom-variables/SnapshotVariable'; import { registerPanelInteractionsReporter } from './transformSaveModelToScene'; import { @@ -228,6 +233,22 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo { + const panel = dashboard.elements[gridElement.spec.element.name]; + if (panel.kind === 'Panel') { + return buildGridItem(gridElement.spec, panel, element.spec.y + GRID_ROW_HEIGHT + gridElement.spec.y); + } else { + throw new Error(`Unknown element kind: ${gridElement.kind}`); + } + }); + let behaviors: SceneObject[] | undefined; + if (element.spec.repeat) { + behaviors = [new RowRepeaterBehavior({ variableName: element.spec.repeat.value })]; + } + return new SceneGridRow({ + y: element.spec.y, + isCollapsed: element.spec.collapsed, + title: element.spec.title, + $behaviors: behaviors, + actions: new RowActions({}), + children, + }); } else { + // If this has been validated by the schema we should never reach this point, which is why TS is telling us this is an error. + //@ts-expect-error throw new Error(`Unknown layout element kind: ${element.kind}`); } }); diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index c1b6c96ec07..c5a945c86fc 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -194,6 +194,7 @@ export function vizPanelToPanel( name: libPanel!.state.name, uid: libPanel!.state.uid, }, + type: 'library-panel-ref', } as Panel; return panel; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index ce5a898e5ed..de87184820a 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -10,6 +10,7 @@ import { IntervalVariable, QueryVariable, SceneGridLayout, + SceneGridRow, SceneRefreshPicker, SceneTimePicker, SceneTimeRange, @@ -29,6 +30,7 @@ import { DashboardControls } from '../scene/DashboardControls'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; +import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; @@ -140,6 +142,8 @@ describe('transformSceneToSaveModelSchemaV2', () => { isLazy: false, children: [ new DashboardGridItem({ + y: 0, + height: 10, body: new VizPanel({ key: 'panel-1', pluginId: 'timeseries', @@ -172,6 +176,31 @@ describe('transformSceneToSaveModelSchemaV2', () => { // repeatDirection?: RepeatDirection, // maxPerRow?: number, }), + new SceneGridRow({ + key: 'panel-4', + title: 'Test Row', + y: 10, + $behaviors: [new RowRepeaterBehavior({ variableName: 'customVar' })], + children: [ + new DashboardGridItem({ + y: 11, + body: new VizPanel({ + key: 'panel-2', + pluginId: 'graph', + title: 'Test Panel 2', + description: 'Test Description 2', + fieldConfig: { defaults: {}, overrides: [] }, + displayMode: 'transparent', + pluginVersion: '7.0.0', + $timeRange: new SceneTimeRange({ + timeZone: 'UTC', + from: 'now-3h', + to: 'now', + }), + }), + }), + ], + }), ], }), }), diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index fc1a1845d59..6253cf9a187 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -7,6 +7,7 @@ import { dataLayers, SceneDataQuery, SceneDataTransformer, + SceneGridRow, SceneVariableSet, VizPanel, } from '@grafana/scenes'; @@ -38,6 +39,7 @@ import { LibraryPanelKind, Element, RepeatOptions, + GridLayoutRowKind, DashboardCursorSync, FieldConfig, FieldColor, @@ -45,6 +47,7 @@ import { import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { PanelTimeRange } from '../scene/PanelTimeRange'; +import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; @@ -57,6 +60,7 @@ import { calculateGridItemDimensions, } from '../utils/utils'; +import { GRID_ROW_HEIGHT } from './const'; import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables'; import { colorIdEnumToColorIdV2, transformCursorSynctoEnum } from './transformToV2TypesUtils'; @@ -154,9 +158,12 @@ function getLiveNow(state: DashboardSceneState) { return Boolean(liveNow); } -function getGridLayoutItems(state: DashboardSceneState, isSnapshot?: boolean): GridLayoutItemKind[] { +function getGridLayoutItems( + state: DashboardSceneState, + isSnapshot?: boolean +): Array { const body = state.body; - let elements: GridLayoutItemKind[] = []; + let elements: Array = []; if (body instanceof DefaultGridLayoutManager) { for (const child of body.state.grid.state.children) { if (child instanceof DashboardGridItem) { @@ -166,23 +173,24 @@ function getGridLayoutItems(state: DashboardSceneState, isSnapshot?: boolean): G } else { elements.push(gridItemToGridLayoutItemKind(child, isSnapshot)); } + } else if (child instanceof SceneGridRow) { + if (child.state.key!.indexOf('-clone-') > 0 && !isSnapshot) { + // Skip repeat rows + continue; + } + elements.push(gridRowToLayoutRowKind(child, isSnapshot)); } - - // TODO: OLD transformer code - // if (child instanceof SceneGridRow) { - // // Skip repeat clones or when generating a snapshot - // if (child.state.key!.indexOf('-clone-') > 0 && !isSnapshot) { - // continue; - // } - // gridRowToSaveModel(child, panels, isSnapshot); - // } } } return elements; } -export function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, isSnapshot = false): GridLayoutItemKind { +export function gridItemToGridLayoutItemKind( + gridItem: DashboardGridItem, + isSnapshot = false, + yOverride?: number +): GridLayoutItemKind { let elementGridItem: GridLayoutItemKind | undefined; let x = 0, y = 0, @@ -208,7 +216,7 @@ export function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, isSnap kind: 'GridLayoutItem', spec: { x, - y, + y: yOverride ?? y, width: width, height: height, element: { @@ -242,6 +250,38 @@ export function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, isSnap return elementGridItem; } +function getRowRepeat(row: SceneGridRow): RepeatOptions | undefined { + if (row.state.$behaviors) { + for (const behavior of row.state.$behaviors) { + if (behavior instanceof RowRepeaterBehavior) { + return { value: behavior.state.variableName, mode: 'variable' }; + } + } + } + return undefined; +} + +function gridRowToLayoutRowKind(row: SceneGridRow, isSnapshot = false): GridLayoutRowKind { + const children = row.state.children.map((child) => { + if (!(child instanceof DashboardGridItem)) { + throw new Error('Unsupported row child type'); + } + const y = (child.state.y ?? 0) - (row.state.y ?? 0) - GRID_ROW_HEIGHT; + return gridItemToGridLayoutItemKind(child, isSnapshot, y); + }); + + return { + kind: 'GridLayoutRow', + spec: { + title: row.state.title, + y: row.state.y ?? 0, + collapsed: Boolean(row.state.isCollapsed), + elements: children, + repeat: getRowRepeat(row), + }, + }; +} + function getElements(state: DashboardSceneState) { const panels = state.body.getVizPanels() ?? []; diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index 95034c757ac..3a677bf658e 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -1,5 +1,12 @@ import { AnnotationQuery, DataQuery, VariableModel, VariableRefresh, Panel } from '@grafana/schema'; -import { DashboardV2Spec, PanelKind, VariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; +import { + DashboardV2Spec, + GridLayoutItemKind, + GridLayoutItemSpec, + GridLayoutRowSpec, + PanelKind, + VariableKind, +} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/examples'; import { AnnoKeyCreatedBy, @@ -310,6 +317,8 @@ describe('ResponseTransformers', () => { transparent: false, links: [], transformations: [], + repeat: 'var1', + repeatDirection: 'h', }, { id: 2, @@ -321,6 +330,69 @@ describe('ResponseTransformers', () => { }, gridPos: { x: 0, y: 8, w: 12, h: 8 }, }, + { + id: 3, + type: 'row', + title: 'Row test title', + gridPos: { x: 0, y: 16, w: 12, h: 1 }, + panels: [], + collapsed: false, + }, + { + id: 4, + type: 'timeseries', + title: 'Panel in row', + gridPos: { x: 0, y: 17, w: 16, h: 8 }, + targets: [ + { + refId: 'A', + datasource: 'datasource1', + expr: 'test-query', + hide: false, + }, + ], + datasource: { + type: 'prometheus', + uid: 'datasource1', + }, + fieldConfig: { defaults: {}, overrides: [] }, + options: {}, + transparent: false, + links: [], + transformations: [], + }, + { + id: 5, + type: 'row', + title: 'Collapsed row title', + gridPos: { x: 0, y: 25, w: 12, h: 1 }, + panels: [ + { + id: 5, + type: 'timeseries', + title: 'Panel in collapsed row', + gridPos: { x: 0, y: 26, w: 16, h: 8 }, + targets: [ + { + refId: 'A', + datasource: 'datasource1', + expr: 'test-query', + hide: false, + }, + ], + datasource: { + type: 'prometheus', + uid: 'datasource1', + }, + fieldConfig: { defaults: {}, overrides: [] }, + options: {}, + transparent: false, + links: [], + transformations: [], + }, + ], + collapsed: true, + }, ], }; @@ -396,7 +468,7 @@ describe('ResponseTransformers', () => { expect(spec.annotations).toEqual([]); // Panel - expect(spec.layout.spec.items).toHaveLength(2); + expect(spec.layout.spec.items).toHaveLength(4); expect(spec.layout.spec.items[0].spec).toEqual({ element: { kind: 'ElementReference', @@ -406,6 +478,7 @@ describe('ResponseTransformers', () => { y: 0, width: 12, height: 8, + repeat: { value: 'var1', direction: 'h', mode: 'variable', maxPerRow: undefined }, }); expect(spec.elements['1']).toEqual({ kind: 'Panel', @@ -481,6 +554,43 @@ describe('ResponseTransformers', () => { }, }); + const rowSpec = spec.layout.spec.items[2].spec as GridLayoutRowSpec; + + expect(rowSpec.collapsed).toBe(false); + expect(rowSpec.title).toBe('Row test title'); + expect(rowSpec.repeat).toBeUndefined(); + + const panelInRow = rowSpec.elements[0].spec as GridLayoutItemSpec; + + expect(panelInRow).toEqual({ + element: { + kind: 'ElementReference', + name: '4', + }, + x: 0, + y: 0, + width: 16, + height: 8, + }); + + const collapsedRowSpec = spec.layout.spec.items[3].spec as GridLayoutRowSpec; + expect(collapsedRowSpec.collapsed).toBe(true); + expect(collapsedRowSpec.title).toBe('Collapsed row title'); + expect(collapsedRowSpec.repeat).toBeUndefined(); + + const panelInCollapsedRow = collapsedRowSpec.elements[0].spec as GridLayoutItemSpec; + + expect(panelInCollapsedRow).toEqual({ + element: { + kind: 'ElementReference', + name: '5', + }, + x: 0, + y: 0, + width: 16, + height: 8, + }); + // Variables validateVariablesV1ToV2(spec.variables[0], dashboardV1.templating?.list?.[0]); validateVariablesV1ToV2(spec.variables[1], dashboardV1.templating?.list?.[1]); @@ -645,6 +755,9 @@ describe('ResponseTransformers', () => { uid: 'uid-for-library-panel', name: 'Library Panel', }); + expect(dashboard.panels![2].type).toBe('row'); + expect(dashboard.panels![2].id).toBe(4); // Row id should be assigned to unique number following the highest id of panels. + expect(dashboard.panels![3].type).toBe('timeseries'); }); describe('getPanelQueries', () => { @@ -756,7 +869,7 @@ describe('ResponseTransformers', () => { expect(v1.transformations).toEqual(v2Spec.data.spec.transformations.map((t) => t.spec)); const layoutElement = layoutV2.spec.items.find( (item) => item.kind === 'GridLayoutItem' && item.spec.element.name === panelKey - ); + ) as GridLayoutItemKind; expect(v1.gridPos?.x).toEqual(layoutElement?.spec.x); expect(v1.gridPos?.y).toEqual(layoutElement?.spec.y); expect(v1.gridPos?.w).toEqual(layoutElement?.spec.width); diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index fa352b029c7..1831507291b 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -5,6 +5,7 @@ import { DataQuery, DataSourceRef, Panel, + RowPanel, VariableModel, VariableType, FieldConfigSource as FieldConfigSourceV1, @@ -34,6 +35,10 @@ import { IntervalVariableKind, TextVariableKind, GroupByVariableKind, + LibraryPanelKind, + PanelKind, + GridLayoutRowKind, + GridLayoutItemKind, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { DashboardLink, DataTransformerConfig } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen'; import { @@ -47,6 +52,7 @@ import { AnnoKeyUpdatedTimestamp, DeprecatedInternalId, } from 'app/features/apiserver/types'; +import { GRID_ROW_HEIGHT } from 'app/features/dashboard-scene/serialization/const'; import { TypedVariableModelV2 } from 'app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene'; import { getDefaultDataSourceRef } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2'; import { @@ -268,8 +274,9 @@ export const ResponseTransformers = { ensureV1Response, }; -// TODO[schema v2]: handle rows -function getElementsFromPanels(panels: Panel[]): [DashboardV2Spec['elements'], DashboardV2Spec['layout']] { +function getElementsFromPanels( + panels: Array +): [DashboardV2Spec['elements'], DashboardV2Spec['layout']] { const elements: DashboardV2Spec['elements'] = {}; const layout: DashboardV2Spec['layout'] = { kind: 'GridLayout', @@ -282,94 +289,156 @@ function getElementsFromPanels(panels: Panel[]): [DashboardV2Spec['elements'], D return [elements, layout]; } + let currentRow: GridLayoutRowKind | null = null; + // iterate over panels for (const p of panels) { - const elementName = p.id!.toString(); - - // LibraryPanelKind - if (p.libraryPanel) { - elements[elementName] = { - kind: 'LibraryPanel', - spec: { - libraryPanel: { - uid: p.libraryPanel.uid, - name: p.libraryPanel.name, - }, - id: p.id!, - title: p.title ?? '', - }, - }; - // PanelKind - } else { - // FIXME: for now we should skip row panels - if (p.type === 'row') { - continue; + if (isRowPanel(p)) { + if (currentRow) { + // Flush current row to layout before we create a new one + layout.spec.items.push(currentRow); } - const queries = getPanelQueries( - (p.targets as unknown as DataQuery[]) || [], - p.datasource || getDefaultDatasource() - ); - const transformations = getPanelTransformations(p.transformations || []); + const rowElements = []; - elements[elementName] = { - kind: 'Panel', - spec: { - title: p.title || '', - description: p.description || '', - vizConfig: { - kind: p.type, - spec: { - fieldConfig: (p.fieldConfig as any) || defaultFieldConfigSource(), - options: p.options as any, - pluginVersion: p.pluginVersion!, - }, - }, - links: - p.links?.map((l) => ({ - title: l.title, - url: l.url || '', - targetBlank: l.targetBlank, - })) || [], - id: p.id!, - data: { - kind: 'QueryGroup', - spec: { - queries, - transformations, // TODO[schema v2]: handle transformations - queryOptions: { - cacheTimeout: p.cacheTimeout, - maxDataPoints: p.maxDataPoints, - interval: p.interval, - hideTimeOverride: p.hideTimeOverride, - queryCachingTTL: p.queryCachingTTL, - timeFrom: p.timeFrom, - timeShift: p.timeShift, - }, - }, - }, - }, - }; + for (const panel of p.panels) { + const [element, name] = buildElement(panel); + elements[name] = element; + rowElements.push(buildGridItemKind(panel, name, yOffsetInRows(panel, p.gridPos!.y))); + } + + currentRow = buildRowKind(p, rowElements); + } else { + const [element, elementName] = buildElement(p); + + elements[elementName] = element; + + if (currentRow) { + // Collect panels to current layout row + currentRow.spec.elements.push(buildGridItemKind(p, elementName, yOffsetInRows(p, currentRow.spec.y))); + } else { + layout.spec.items.push(buildGridItemKind(p, elementName)); + } } + } - layout.spec.items.push({ - kind: 'GridLayoutItem', - spec: { - x: p.gridPos!.x, - y: p.gridPos!.y, - width: p.gridPos!.w, - height: p.gridPos!.h, - element: { - kind: 'ElementReference', - name: elementName, - }, - }, - }); + if (currentRow) { + // Flush last row to layout + layout.spec.items.push(currentRow); } return [elements, layout]; } +function isRowPanel(panel: Panel | RowPanel): panel is RowPanel { + return panel.type === 'row'; +} + +function buildRowKind(p: RowPanel, elements: GridLayoutItemKind[]): GridLayoutRowKind { + return { + kind: 'GridLayoutRow', + spec: { + collapsed: p.collapsed, + title: p.title ?? '', + repeat: p.repeat ? { value: p.repeat, mode: 'variable' } : undefined, + y: p.gridPos?.y ?? 0, + elements, + }, + }; +} + +function buildGridItemKind(p: Panel, elementName: string, yOverride?: number): GridLayoutItemKind { + return { + kind: 'GridLayoutItem', + spec: { + x: p.gridPos!.x, + y: yOverride ?? p.gridPos!.y, + width: p.gridPos!.w, + height: p.gridPos!.h, + repeat: p.repeat + ? { value: p.repeat, mode: 'variable', direction: p.repeatDirection, maxPerRow: p.maxPerRow } + : undefined, + element: { + kind: 'ElementReference', + name: elementName!, + }, + }, + }; +} + +function yOffsetInRows(p: Panel, rowY: number): number { + return p.gridPos!.y - rowY - GRID_ROW_HEIGHT; +} + +function buildElement(p: Panel): [PanelKind | LibraryPanelKind, string] { + if (p.libraryPanel) { + // LibraryPanelKind + const panelKind: LibraryPanelKind = { + kind: 'LibraryPanel', + spec: { + libraryPanel: { + uid: p.libraryPanel.uid, + name: p.libraryPanel.name, + }, + id: p.id!, + title: p.title ?? '', + }, + }; + + return [panelKind, p.id!.toString()]; + } else { + // PanelKind + + const queries = getPanelQueries( + (p.targets as unknown as DataQuery[]) || [], + p.datasource || getDefaultDatasource() + ); + + const transformations = getPanelTransformations(p.transformations || []); + + const panelKind: PanelKind = { + kind: 'Panel', + spec: { + title: p.title || '', + description: p.description || '', + vizConfig: { + kind: p.type, + spec: { + fieldConfig: (p.fieldConfig as any) || defaultFieldConfigSource(), + options: p.options as any, + pluginVersion: p.pluginVersion!, + }, + }, + links: + p.links?.map((l) => ({ + title: l.title, + url: l.url || '', + targetBlank: l.targetBlank, + })) || [], + id: p.id!, + data: { + kind: 'QueryGroup', + spec: { + queries, + transformations, + queryOptions: { + cacheTimeout: p.cacheTimeout, + maxDataPoints: p.maxDataPoints, + interval: p.interval, + hideTimeOverride: p.hideTimeOverride, + queryCachingTTL: p.queryCachingTTL, + timeFrom: p.timeFrom, + timeShift: p.timeShift, + }, + }, + }, + }, + }; + + return [panelKind, p.id!.toString()]; + } +} + function getDefaultDatasourceType() { // if there is no default datasource, return 'grafana' as default return getDefaultDataSourceRef()?.type ?? 'grafana'; @@ -778,72 +847,130 @@ function getAnnotationsV1(annotations: DashboardV2Spec['annotations']): Annotati }); } -interface LibraryPanelDTO extends Pick {} +interface LibraryPanelDTO extends Pick {} function getPanelsV1( panels: DashboardV2Spec['elements'], layout: DashboardV2Spec['layout'] ): Array { - return Object.entries(panels).map(([key, p]) => { - const layoutElement = layout.spec.items.find( - (item) => item.kind === 'GridLayoutItem' && item.spec.element.name === key - ); - const { x, y, width, height, repeat } = layoutElement?.spec || { x: 0, y: 0, width: 0, height: 0 }; - const gridPos = { x, y, w: width, h: height }; - if (p.kind === 'Panel') { - const panel = p.spec; - return { - id: panel.id, - type: panel.vizConfig.kind, - title: panel.title, - description: panel.description, - fieldConfig: transformMappingsToV1(panel.vizConfig.spec.fieldConfig), - options: panel.vizConfig.spec.options, - pluginVersion: panel.vizConfig.spec.pluginVersion, - links: - // @ts-expect-error - Panel link is wrongly typed as DashboardLink - panel.links?.map((l) => ({ - title: l.title, - url: l.url, - ...(l.targetBlank && { targetBlank: l.targetBlank }), - })) || [], - targets: panel.data.spec.queries.map((q) => { - return { - refId: q.spec.refId, - hide: q.spec.hidden, - datasource: q.spec.datasource, - ...q.spec.query.spec, - }; - }), - transformations: panel.data.spec.transformations.map((t) => t.spec), - gridPos, - cacheTimeout: panel.data.spec.queryOptions.cacheTimeout, - maxDataPoints: panel.data.spec.queryOptions.maxDataPoints, - interval: panel.data.spec.queryOptions.interval, - hideTimeOverride: panel.data.spec.queryOptions.hideTimeOverride, - queryCachingTTL: panel.data.spec.queryOptions.queryCachingTTL, - timeFrom: panel.data.spec.queryOptions.timeFrom, - timeShift: panel.data.spec.queryOptions.timeShift, - transparent: panel.transparent, - ...(repeat?.value && { repeat: repeat.value }), - ...(repeat?.direction && { repeatDirection: repeat.direction }), - ...(repeat?.maxPerRow && { maxPerRow: repeat.maxPerRow }), - }; - } else if (p.kind === 'LibraryPanel') { - const panel = p.spec; - return { - id: panel.id, - title: panel.title, - gridPos, - libraryPanel: { - uid: panel.libraryPanel.uid, - name: panel.libraryPanel.name, + const panelsV1: Array = []; + + let maxPanelId = 0; + + for (const item of layout.spec.items) { + if (item.kind === 'GridLayoutItem') { + const panel = panels[item.spec.element.name]; + const v1Panel = transformV2PanelToV1Panel(panel, item); + panelsV1.push(v1Panel); + if (v1Panel.id ?? 0 > maxPanelId) { + maxPanelId = v1Panel.id ?? 0; + } + } else if (item.kind === 'GridLayoutRow') { + const row: RowPanel = { + id: -1, // Temporarily set to -1, updated later to be unique + type: 'row', + title: item.spec.title, + collapsed: item.spec.collapsed, + repeat: item.spec.repeat ? item.spec.repeat.value : undefined, + gridPos: { + x: 0, + y: item.spec.y, + w: 24, + h: GRID_ROW_HEIGHT, }, + panels: [], }; - } else { - throw new Error(`Unknown element kind: ${p}`); + + const rowPanels = []; + for (const panel of item.spec.elements) { + const panelElement = panels[panel.spec.element.name]; + const v1Panel = transformV2PanelToV1Panel(panelElement, panel, item.spec.y + GRID_ROW_HEIGHT + panel.spec.y); + rowPanels.push(v1Panel); + if (v1Panel.id ?? 0 > maxPanelId) { + maxPanelId = v1Panel.id ?? 0; + } + } + if (item.spec.collapsed) { + // When a row is collapsed, panels inside it are stored in the panels property. + row.panels = rowPanels; + panelsV1.push(row); + } else { + panelsV1.push(row); + panelsV1.push(...rowPanels); + } } - }); + } + + // Update row panel ids to be unique + for (const panel of panelsV1) { + if (panel.type === 'row' && panel.id === -1) { + panel.id = ++maxPanelId; + } + } + return panelsV1; +} + +function transformV2PanelToV1Panel( + p: PanelKind | LibraryPanelKind, + layoutElement: GridLayoutItemKind, + yOverride?: number +): Panel | LibraryPanelDTO { + const { x, y, width, height, repeat } = layoutElement?.spec || { x: 0, y: 0, width: 0, height: 0 }; + const gridPos = { x, y: yOverride ?? y, w: width, h: height }; + if (p.kind === 'Panel') { + const panel = p.spec; + return { + id: panel.id, + type: panel.vizConfig.kind, + title: panel.title, + description: panel.description, + fieldConfig: transformMappingsToV1(panel.vizConfig.spec.fieldConfig), + options: panel.vizConfig.spec.options, + pluginVersion: panel.vizConfig.spec.pluginVersion, + links: + // @ts-expect-error - Panel link is wrongly typed as DashboardLink + panel.links?.map((l) => ({ + title: l.title, + url: l.url, + ...(l.targetBlank && { targetBlank: l.targetBlank }), + })) || [], + targets: panel.data.spec.queries.map((q) => { + return { + refId: q.spec.refId, + hide: q.spec.hidden, + datasource: q.spec.datasource, + ...q.spec.query.spec, + }; + }), + transformations: panel.data.spec.transformations.map((t) => t.spec), + gridPos, + cacheTimeout: panel.data.spec.queryOptions.cacheTimeout, + maxDataPoints: panel.data.spec.queryOptions.maxDataPoints, + interval: panel.data.spec.queryOptions.interval, + hideTimeOverride: panel.data.spec.queryOptions.hideTimeOverride, + queryCachingTTL: panel.data.spec.queryOptions.queryCachingTTL, + timeFrom: panel.data.spec.queryOptions.timeFrom, + timeShift: panel.data.spec.queryOptions.timeShift, + transparent: panel.transparent, + ...(repeat?.value && { repeat: repeat.value }), + ...(repeat?.direction && { repeatDirection: repeat.direction }), + ...(repeat?.maxPerRow && { maxPerRow: repeat.maxPerRow }), + }; + } else if (p.kind === 'LibraryPanel') { + const panel = p.spec; + return { + id: panel.id, + title: panel.title, + gridPos, + libraryPanel: { + uid: panel.libraryPanel.uid, + name: panel.libraryPanel.name, + }, + type: 'library-panel-ref', + }; + } else { + throw new Error(`Unknown element kind: ${p}`); + } } export function transformMappingsToV1(fieldConfig: FieldConfigSource): FieldConfigSourceV1 { diff --git a/public/app/features/dashboard/api/utils.ts b/public/app/features/dashboard/api/utils.ts index 1b23e9a086e..89d0c9219d3 100644 --- a/public/app/features/dashboard/api/utils.ts +++ b/public/app/features/dashboard/api/utils.ts @@ -7,6 +7,8 @@ import { SaveDashboardCommand } from '../components/SaveDashboard/types'; import { DashboardWithAccessInfo } from './types'; +export const GRID_ROW_HEIGHT = 1; + export function getDashboardsApiVersion() { const forcingOldDashboardArch = locationService.getSearch().get('scenes') === 'false'; From 6392542db4cb9d5dc0ab29e337af11bee69c5dcc Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Thu, 30 Jan 2025 14:42:11 +0100 Subject: [PATCH 232/894] Alerting: Fix inheritance of the timing options for policy tree (#99398) --- .../unified/NotificationPoliciesPage.test.tsx | 11 ++-- .../NotificationPoliciesList.tsx | 55 +++++++++---------- .../notification-policies/Policy.test.tsx | 8 +-- .../notification-policies/Policy.tsx | 15 ++--- 4 files changed, 40 insertions(+), 49 deletions(-) diff --git a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx index 2bbb102b58b..dd124c0f9a6 100644 --- a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx +++ b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx @@ -296,14 +296,14 @@ describe.each([ }); it('allows user to reload and update policies if its been changed by another user', async () => { - jest.retryTimes(2); const { user } = renderNotificationPolicies(); + const NEW_INTERVAL = '12h'; await getRootRoute(); const existingConfig = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME); const modifiedConfig = produce(existingConfig, (draft) => { - draft.alertmanager_config.route!.group_interval = '12h'; + draft.alertmanager_config.route!.group_interval = NEW_INTERVAL; }); setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, modifiedConfig); @@ -317,11 +317,8 @@ describe.each([ await user.click(screen.getByRole('button', { name: /cancel/i })); await user.click(screen.getByRole('button', { name: /reload policies/i })); - await openDefaultPolicyEditModal(); - await user.click(await screen.findByRole('button', { name: /update default policy/i })); - expect(await screen.findByText(/updated notification policies/i)).toBeInTheDocument(); - // TODO: Check if test flakiness/length can be improved - }, 60000); + expect((await screen.findAllByTestId('timing-options'))[0]).toHaveTextContent(NEW_INTERVAL); + }); it('Should be able to delete an empty route', async () => { const defaultConfig: AlertManagerCortexConfig = { diff --git a/public/app/features/alerting/unified/components/notification-policies/NotificationPoliciesList.tsx b/public/app/features/alerting/unified/components/notification-policies/NotificationPoliciesList.tsx index 2e3e8d3bebd..a65dd8daf36 100644 --- a/public/app/features/alerting/unified/components/notification-policies/NotificationPoliciesList.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/NotificationPoliciesList.tsx @@ -1,3 +1,4 @@ +import { defaults } from 'lodash'; import { useEffect, useMemo, useState } from 'react'; import { useAsyncFn } from 'react-use'; @@ -23,6 +24,7 @@ import { InsertPosition } from './../../utils/routeTree'; import { NotificationPoliciesFilter, findRoutesByMatchers, findRoutesMatchingPredicate } from './Filters'; import { useAddPolicyModal, useAlertGroupsModal, useDeletePolicyModal, useEditPolicyModal } from './Modals'; import { Policy } from './Policy'; +import { TIMING_OPTIONS_DEFAULTS } from './timingOptions'; import { useAddNotificationPolicy, useDeleteNotificationPolicy, @@ -218,34 +220,31 @@ export const NotificationPoliciesList = () => { )} {hasPoliciesData && ( - {rootRoute && ( - - )} - {rootRoute && ( - - )} + + )} {addModal} diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx index c2e43ab77b2..89272e58572 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx @@ -136,7 +136,7 @@ describe('Policy', () => { expect(within(firstPolicy).getByTestId('contact-point')).toHaveTextContent('provisioned-contact-point'); expect(within(firstPolicy).getByTestId('mute-timings')).toHaveTextContent('Muted when mt-1'); expect(within(firstPolicy).getByTestId('active-timings')).toHaveTextContent('Active when mt-2'); - expect(within(firstPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited2 properties'); + expect(within(firstPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited4 properties'); // second custom policy should be correct const secondPolicy = customPolicies[1]; @@ -144,7 +144,7 @@ describe('Policy', () => { expect(within(secondPolicy).queryByTestId('continue-matching')).not.toBeInTheDocument(); expect(within(secondPolicy).queryByTestId('mute-timings')).not.toBeInTheDocument(); expect(within(secondPolicy).queryByTestId('active-timings')).not.toBeInTheDocument(); - expect(within(secondPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited3 properties'); + expect(within(secondPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited5 properties'); // third custom policy should be correct const thirdPolicy = customPolicies[2]; @@ -381,8 +381,8 @@ const mockRoutes: RouteWithID = { }, ], group_wait: '30s', - group_interval: undefined, - repeat_interval: undefined, + group_interval: '5m', + repeat_interval: '4h', }; describe('isAutoGeneratedRootAndSimplifiedEnabled', () => { diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index 7f2d800390d..befb2cfe27b 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { defaults, isArray, sumBy, uniqueId } from 'lodash'; +import { isArray, sumBy, uniqueId } from 'lodash'; import pluralize from 'pluralize'; import * as React from 'react'; import { FC, Fragment, ReactNode, useState } from 'react'; @@ -51,7 +51,9 @@ import { GrafanaPoliciesExporter } from '../export/GrafanaPoliciesExporter'; import { Matchers } from './Matchers'; import { RoutesMatchingFilters } from './NotificationPoliciesList'; -import { TIMING_OPTIONS_DEFAULTS, TimingOptions } from './timingOptions'; +import { TimingOptions } from './timingOptions'; + +const POLICIES_PER_PAGE = 20; interface PolicyComponentProps { receivers?: Receiver[]; @@ -172,8 +174,6 @@ const Policy = (props: PolicyComponentProps) => { errors.push(error); }); - const POLICIES_PER_PAGE = 20; - const [visibleChildPolicies, setVisibleChildPolicies] = useState(POLICIES_PER_PAGE); // build the menu actions for our policy @@ -511,12 +511,7 @@ function MetadataRow({ )} - {timingOptions && ( - // for the default policy we will also merge the default timings, that way a user can observe what the timing options would be - - )} + {timingOptions && } {hasInheritedProperties && ( <> From a92c8145f18eba15e77569ca7ce9aa4de6c0c0a6 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Thu, 30 Jan 2025 14:43:26 +0100 Subject: [PATCH 233/894] TopNav: Move news into profile menu (#99535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove news icon from topnav Signed-off-by: bergquist * TopNav: Move rss feed and kiosk action into profile menu * Update language keys * Update * review fixes * Update * Update --------- Signed-off-by: bergquist Co-authored-by: Torkel Ödegaard --- .betterer.results | 11 +-- .../AppChrome/News/NewsContainer.test.tsx | 19 ----- .../AppChrome/News/NewsContainer.tsx | 84 ------------------- .../components/AppChrome/News/NewsDrawer.tsx | 78 +++++++++++++++++ .../AppChrome/TopBar/ProfileButton.tsx | 71 ++++++++++++++++ .../AppChrome/TopBar/SingleTopBar.tsx | 23 +---- .../AppChrome/TopBar/TopNavBarMenu.tsx | 4 +- public/locales/en-US/grafana.json | 3 +- public/locales/pseudo-LOCALE/grafana.json | 3 +- 9 files changed, 164 insertions(+), 132 deletions(-) delete mode 100644 public/app/core/components/AppChrome/News/NewsContainer.test.tsx delete mode 100644 public/app/core/components/AppChrome/News/NewsContainer.tsx create mode 100644 public/app/core/components/AppChrome/News/NewsDrawer.tsx create mode 100644 public/app/core/components/AppChrome/TopBar/ProfileButton.tsx diff --git a/.betterer.results b/.betterer.results index 01d1a68627b..8e643b38615 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1003,9 +1003,8 @@ exports[`better eslint`] = { "public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], - "public/app/core/components/AppChrome/News/NewsContainer.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"] + "public/app/core/components/AppChrome/News/NewsDrawer.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], "public/app/core/components/AppChrome/News/NewsWrapper.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] @@ -1016,10 +1015,12 @@ exports[`better eslint`] = { "public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], + "public/app/core/components/AppChrome/TopBar/ProfileButton.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] + ], "public/app/core/components/AppChrome/TopBar/SingleTopBar.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()", "1"] ], "public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/themes/mixins\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] diff --git a/public/app/core/components/AppChrome/News/NewsContainer.test.tsx b/public/app/core/components/AppChrome/News/NewsContainer.test.tsx deleted file mode 100644 index 3b1ed747283..00000000000 --- a/public/app/core/components/AppChrome/News/NewsContainer.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import { NewsContainer } from './NewsContainer'; - -const setup = () => { - const { container } = render(); - - return { container }; -}; - -describe('News', () => { - it('should render the drawer when the drawer button is clicked', async () => { - setup(); - - await userEvent.click(screen.getByRole('button')); - expect(screen.getByText('Latest from the blog')).toBeInTheDocument(); - }); -}); diff --git a/public/app/core/components/AppChrome/News/NewsContainer.tsx b/public/app/core/components/AppChrome/News/NewsContainer.tsx deleted file mode 100644 index 2d4cec5a628..00000000000 --- a/public/app/core/components/AppChrome/News/NewsContainer.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { css } from '@emotion/css'; -import { useToggle } from 'react-use'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { IconButton, Drawer, ToolbarButton, useStyles2, Text } from '@grafana/ui'; -import { t } from 'app/core/internationalization'; -import { DEFAULT_FEED_URL } from 'app/plugins/panel/news/constants'; - -import { NewsWrapper } from './NewsWrapper'; - -interface NewsContainerProps { - className?: string; -} - -export function NewsContainer({ className }: NewsContainerProps) { - const [showNewsDrawer, onToggleShowNewsDrawer] = useToggle(false); - const styles = useStyles2(getStyles); - - return ( - <> - - {showNewsDrawer && ( - - {t('news.title', 'Latest from the blog')} - - Grot reading news - -
- -
-
- } - onClose={onToggleShowNewsDrawer} - size="md" - > - - - )} - - ); -} - -const getStyles = (theme: GrafanaTheme2) => { - return { - title: css({ - display: `flex`, - alignItems: `center`, - justifyContent: `center`, - gap: theme.spacing(2), - borderBottom: `1px solid ${theme.colors.border.weak}`, - }), - grot: css({ - display: `flex`, - alignItems: `center`, - justifyContent: `center`, - padding: theme.spacing(2, 0), - - img: { - width: `75px`, - height: `75px`, - }, - }), - actions: css({ - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(2), - }), - }; -}; diff --git a/public/app/core/components/AppChrome/News/NewsDrawer.tsx b/public/app/core/components/AppChrome/News/NewsDrawer.tsx new file mode 100644 index 00000000000..7211823430d --- /dev/null +++ b/public/app/core/components/AppChrome/News/NewsDrawer.tsx @@ -0,0 +1,78 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { IconButton, Drawer, useStyles2, Text } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; +import { DEFAULT_FEED_URL } from 'app/plugins/panel/news/constants'; + +import { NewsWrapper } from './NewsWrapper'; + +interface NewsContainerProps { + className?: string; + onClose: () => void; +} + +export function NewsContainer({ onClose }: NewsContainerProps) { + const styles = useStyles2(getStyles); + + return ( + + {t('news.title', 'Latest from the blog')} + + Grot reading news + +
+ +
+
+ } + onClose={onClose} + size="md" + > + + + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + title: css({ + display: `flex`, + alignItems: `center`, + justifyContent: `center`, + gap: theme.spacing(2), + borderBottom: `1px solid ${theme.colors.border.weak}`, + }), + grot: css({ + display: `flex`, + alignItems: `center`, + justifyContent: `center`, + padding: theme.spacing(2, 0), + + img: { + width: `75px`, + height: `75px`, + }, + }), + actions: css({ + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(2), + }), + }; +}; diff --git a/public/app/core/components/AppChrome/TopBar/ProfileButton.tsx b/public/app/core/components/AppChrome/TopBar/ProfileButton.tsx new file mode 100644 index 00000000000..3faa5f24876 --- /dev/null +++ b/public/app/core/components/AppChrome/TopBar/ProfileButton.tsx @@ -0,0 +1,71 @@ +import { css } from '@emotion/css'; +import { cloneDeep } from 'lodash'; +import { useToggle } from 'react-use'; + +import { GrafanaTheme2, NavModelItem } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { Dropdown, Menu, MenuItem, ToolbarButton, useStyles2 } from '@grafana/ui'; +import { contextSrv } from 'app/core/core'; +import { t } from 'app/core/internationalization'; + +import { enrichWithInteractionTracking } from '../MegaMenu/utils'; +import { NewsContainer } from '../News/NewsDrawer'; + +import { TopNavBarMenu } from './TopNavBarMenu'; + +export interface Props { + profileNode: NavModelItem; +} + +export function ProfileButton({ profileNode }: Props) { + const styles = useStyles2(getStyles); + const node = enrichWithInteractionTracking(cloneDeep(profileNode), false); + const [showNewsDrawer, onToggleShowNewsDrawer] = useToggle(false); + + if (!node) { + return null; + } + + const renderMenu = () => ( + + {config.newsFeedEnabled && ( + <> + + + + )} + + ); + + return ( + <> + + + + {showNewsDrawer && } + + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + profileButton: css({ + padding: theme.spacing(0, 0.5), + img: { + borderRadius: theme.shape.radius.circle, + height: '24px', + marginRight: 0, + width: '24px', + }, + }), + }; +}; diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx index 819ab7371af..225db772a11 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx @@ -16,10 +16,10 @@ import { Breadcrumbs } from '../../Breadcrumbs/Breadcrumbs'; import { buildBreadcrumbs } from '../../Breadcrumbs/utils'; import { HistoryContainer } from '../History/HistoryContainer'; import { enrichHelpItem } from '../MegaMenu/utils'; -import { NewsContainer } from '../News/NewsContainer'; import { QuickAdd } from '../QuickAdd/QuickAdd'; import { TOP_BAR_LEVEL_HEIGHT } from '../types'; +import { ProfileButton } from './ProfileButton'; import { SignInLink } from './SignInLink'; import { TopNavBarMenu } from './TopNavBarMenu'; import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger'; @@ -80,7 +80,6 @@ export const SingleTopBar = memo(function SingleTopBar({ )} - {config.newsFeedEnabled && } {!contextSrv.user.isSignedIn && } - {profileNode && ( - } placement="bottom-end"> - - - )} + {profileNode && }
); @@ -132,15 +122,6 @@ const getStyles = (theme: GrafanaTheme2, menuDockedAndOpen: boolean) => ({ height: theme.spacing(3), width: theme.spacing(3), }), - profileButton: css({ - padding: theme.spacing(0, 0.5), - img: { - borderRadius: theme.shape.radius.circle, - height: '24px', - marginRight: 0, - width: '24px', - }, - }), kioskToggle: css({ [theme.breakpoints.down('lg')]: { display: 'none', diff --git a/public/app/core/components/AppChrome/TopBar/TopNavBarMenu.tsx b/public/app/core/components/AppChrome/TopBar/TopNavBarMenu.tsx index 27abaf86a77..7afb4c50bcc 100644 --- a/public/app/core/components/AppChrome/TopBar/TopNavBarMenu.tsx +++ b/public/app/core/components/AppChrome/TopBar/TopNavBarMenu.tsx @@ -8,9 +8,10 @@ import { enrichWithInteractionTracking } from '../MegaMenu/utils'; export interface TopNavBarMenuProps { node: NavModelItem; + children?: React.ReactNode; } -export function TopNavBarMenu({ node: nodePlain }: TopNavBarMenuProps) { +export function TopNavBarMenu({ node: nodePlain, children }: TopNavBarMenuProps) { const styles = useStyles2(getStyles); const node = enrichWithInteractionTracking(cloneDeep(nodePlain), false); @@ -37,6 +38,7 @@ export function TopNavBarMenu({ node: nodePlain }: TopNavBarMenuProps) { ); })} + {children} ); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6f9cda10abe..7781727270f 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2430,7 +2430,8 @@ "list-label": "Navigation", "open": "Open menu", "undock": "Undock menu" - } + }, + "rss-button": "Latest from the blog" }, "news": { "drawer": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 6b3f2d081a7..006e0f4a07f 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2430,7 +2430,8 @@ "list-label": "Ńävįģäŧįőʼn", "open": "Øpęʼn męʼnū", "undock": "Ůʼnđőčĸ męʼnū" - } + }, + "rss-button": "Ŀäŧęşŧ ƒřőm ŧĥę þľőģ" }, "news": { "drawer": { From 86a68627dd90cfb101085744de23e6f7066eb315 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Thu, 30 Jan 2025 14:53:01 +0100 Subject: [PATCH 234/894] QueryLibrary: Make query library available outside of Explore (#99319) --- .betterer.results | 6 +- .../src/services/dataSourceSrv.ts | 2 +- .../PanelDataPane/PanelDataQueriesTab.tsx | 74 ++++++- public/app/features/explore/ExplorePage.tsx | 76 +------ .../explore/ExploreRunQueryButton.tsx | 6 +- .../app/features/explore/ExploreToolbar.tsx | 6 +- .../QueriesDrawer/QueriesDrawerContext.tsx | 22 +- .../QueriesDrawer/QueriesDrawerDropdown.tsx | 104 +++++++--- .../features/explore/QueriesDrawer/mocks.tsx | 3 +- .../QueryLibrary/AddToQueryLibraryModal.tsx | 36 ++++ .../explore/QueryLibrary/QueryLibrary.tsx | 6 +- .../QueryLibrary/QueryLibraryContext.test.tsx | 79 +++++++ .../QueryLibrary/QueryLibraryContext.tsx | 118 +++++++++++ .../QueryLibrary/QueryLibraryDrawer.tsx | 53 +++++ .../QueryLibrary/QueryTemplateForm.tsx | 14 +- .../QueryLibrary/QueryTemplatesList.test.tsx | 139 +++++++++++++ .../QueryLibrary/QueryTemplatesList.tsx | 193 ++++++++---------- .../QueryTemplatesTable/ActionsCell.tsx | 25 ++- .../QueryTemplatesTable/index.tsx | 40 ++-- .../explore/QueryLibrary/SaveQueryButton.tsx | 44 ++++ .../features/explore/QueryLibrary/types.ts | 11 + .../QueryLibrary/utils/dataFetching.ts | 106 ++++++++++ .../explore/RichHistory/RichHistory.tsx | 19 +- .../RichHistory/RichHistoryContainer.tsx | 2 +- .../explore/spec/helper/interactions.ts | 21 +- .../features/explore/spec/helper/setup.tsx | 43 ++-- .../explore/spec/queryHistory.test.tsx | 2 + .../explore/spec/queryLibrary.test.tsx | 7 +- .../query/components/QueryEditorRow.tsx | 5 +- public/app/routes/RoutesWrapper.tsx | 33 +-- public/locales/en-US/grafana.json | 10 +- public/locales/pseudo-LOCALE/grafana.json | 10 +- 32 files changed, 968 insertions(+), 347 deletions(-) create mode 100644 public/app/features/explore/QueryLibrary/AddToQueryLibraryModal.tsx create mode 100644 public/app/features/explore/QueryLibrary/QueryLibraryContext.test.tsx create mode 100644 public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx create mode 100644 public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx create mode 100644 public/app/features/explore/QueryLibrary/QueryTemplatesList.test.tsx create mode 100644 public/app/features/explore/QueryLibrary/SaveQueryButton.tsx create mode 100644 public/app/features/explore/QueryLibrary/types.ts create mode 100644 public/app/features/explore/QueryLibrary/utils/dataFetching.ts diff --git a/.betterer.results b/.betterer.results index 8e643b38615..bc0dd082f2a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2545,8 +2545,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "11"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "10"] ], "public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -4644,9 +4643,6 @@ exports[`better eslint`] = { [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"] ], - "public/app/features/explore/ExplorePage.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], "public/app/features/explore/ExploreRunQueryButton.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"] diff --git a/packages/grafana-runtime/src/services/dataSourceSrv.ts b/packages/grafana-runtime/src/services/dataSourceSrv.ts index 36f7664489b..cd918209080 100644 --- a/packages/grafana-runtime/src/services/dataSourceSrv.ts +++ b/packages/grafana-runtime/src/services/dataSourceSrv.ts @@ -13,7 +13,7 @@ import { RuntimeDataSource } from './RuntimeDataSource'; export interface DataSourceSrv { /** * Returns the requested dataSource. If it cannot be found it rejects the promise. - * @param ref - The datasource identifier, typically an object with UID and type, + * @param ref - The datasource identifier, it can be a name, UID or DataSourceRef (an object with UID), * @param scopedVars - variables used to interpolate a templated passed as name. */ get(ref?: DataSourceRef | string | null, scopedVars?: ScopedVars): Promise; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index 75abcf9ff84..cef20ef129f 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -13,6 +13,7 @@ import { } from '@grafana/scenes'; import { DataQuery } from '@grafana/schema'; import { Button, Stack, Tab } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; import { addQuery } from 'app/core/utils/query'; import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard'; import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils'; @@ -24,6 +25,8 @@ import { updateQueries } from 'app/features/query/state/updateQueries'; import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard/runSharedRequest'; import { QueryGroupOptions } from 'app/types'; +import { useQueryLibraryContext } from '../../../explore/QueryLibrary/QueryLibraryContext'; +import { QueryActionButtonProps } from '../../../explore/QueryLibrary/types'; import { PanelTimeRange } from '../../scene/PanelTimeRange'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../../utils/utils'; import { getUpdatedHoverHeader } from '../getPanelFrameOptions'; @@ -306,13 +309,20 @@ export class PanelDataQueriesTab extends SceneObjectBase) { const { datasource, dsSettings } = model.useState(); const { data, queries } = model.queryRunner.useState(); + const { openDrawer: openQueryLibraryDrawer } = useQueryLibraryContext(); if (!datasource || !dsSettings || !data) { return null; } - const showAddButton = !isSharedDashboardQuery(dsSettings.name); + // Make the final query library action button by injecting actual addQuery functionality into the button. + const addQueryActionButton = makeQueryActionButton((queries) => { + for (const query of queries) { + model.onQueriesChange(addQuery(model.getQueries(), query)); + } + }); + return (
{showAddButton && ( - + <> + + {config.featureToggles.queryLibrary && ( + + )} + )} {config.expressionsEnabled && model.isExpressionsSupported(dsSettings) && ( + ); + }; +} + +function getDatasourceNames(datasource: DataSourceApi, queries: DataQuery[]): string[] { + if (datasource.uid === '-- Mixed --') { + // If datasource is mixed, the datasource UID is on the query. Here we map the UIDs to datasource names. + const dsSrv = getDataSourceSrv(); + return queries.map((ds) => dsSrv.getInstanceSettings(ds.datasource)?.name).filter((name) => name !== undefined); + } else { + return [datasource.name]; + } +} + interface QueriesTabProps extends PanelDataTabHeaderProps { model: PanelDataQueriesTab; } diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index 1764a81ed16..96b5d540d84 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -1,30 +1,22 @@ import { css, cx } from '@emotion/css'; -import { useEffect, useState } from 'react'; -import { useLocalStorage } from 'react-use'; +import { useEffect } from 'react'; -import { CoreApp, GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { DataQuery } from '@grafana/schema/dist/esm/index'; -import { Badge, ErrorBoundaryAlert, Modal, useStyles2, useTheme2 } from '@grafana/ui'; -import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction'; +import { ErrorBoundaryAlert, useStyles2, useTheme2 } from '@grafana/ui'; import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { useNavModel } from 'app/core/hooks/useNavModel'; -import { Trans, t } from 'app/core/internationalization'; +import { Trans } from 'app/core/internationalization'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { useSelector } from 'app/types'; import { ExploreQueryParams } from 'app/types/explore'; -import { RowActionComponents } from '../query/components/QueryActionComponent'; - import { CorrelationEditorModeBar } from './CorrelationEditorModeBar'; import { ExploreActions } from './ExploreActions'; import { ExploreDrawer } from './ExploreDrawer'; import { ExplorePaneContainer } from './ExplorePaneContainer'; import { useQueriesDrawerContext } from './QueriesDrawer/QueriesDrawerContext'; -import { QUERY_LIBRARY_LOCAL_STORAGE_KEYS } from './QueryLibrary/QueryLibrary'; -import { queryLibraryTrackAddFromQueryRow } from './QueryLibrary/QueryLibraryAnalyticsEvents'; -import { QueryTemplateForm } from './QueryLibrary/QueryTemplateForm'; import RichHistoryContainer from './RichHistory/RichHistoryContainer'; import { useExplorePageTitle } from './hooks/useExplorePageTitle'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; @@ -34,7 +26,6 @@ import { useTimeSrvFix } from './hooks/useTimeSrvFix'; import { isSplit, selectCorrelationDetails, selectPanesEntries } from './state/selectors'; const MIN_PANE_WIDTH = 200; -const QUERY_LIBRARY_ACTION_KEY = 'queryLibraryAction'; export default function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) { return ; @@ -58,13 +49,8 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa const panes = useSelector(selectPanesEntries); const hasSplit = useSelector(isSplit); const correlationDetails = useSelector(selectCorrelationDetails); - const { drawerOpened, setDrawerOpened, queryLibraryAvailable } = useQueriesDrawerContext(); + const { drawerOpened, setDrawerOpened } = useQueriesDrawerContext(); const showCorrelationEditorBar = config.featureToggles.correlations && (correlationDetails?.editorMode || false); - const [queryToAdd, setQueryToAdd] = useState(); - const [showQueryLibraryBadgeButton, setShowQueryLibraryBadgeButton] = useLocalStorage( - QUERY_LIBRARY_LOCAL_STORAGE_KEYS.explore.newButton, - true - ); useEffect(() => { //This is needed for breadcrumbs and topnav. @@ -74,38 +60,6 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa }); }, [chrome, navModel]); - useEffect(() => { - const hasQueryLibrary = config.featureToggles.queryLibrary || false; - if (hasQueryLibrary) { - RowActionComponents.addKeyedExtraRenderAction(QUERY_LIBRARY_ACTION_KEY, { - scope: CoreApp.Explore, - queryActionComponent: (props) => - showQueryLibraryBadgeButton ? ( - { - setQueryToAdd(props.query); - setShowQueryLibraryBadgeButton(false); - }} - style={{ cursor: 'pointer' }} - /> - ) : ( - { - setQueryToAdd(props.query); - }} - /> - ), - }); - } - }, [showQueryLibraryBadgeButton, setShowQueryLibraryBadgeButton]); - useKeyboardShortcuts(); return ( @@ -139,7 +93,7 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa })} {drawerOpened && ( - + { setDrawerOpened(false); @@ -147,24 +101,6 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa /> )} - setQueryToAdd(undefined)} - > - { - setQueryToAdd(undefined); - }} - onSave={(isSuccess) => { - if (isSuccess) { - setQueryToAdd(undefined); - queryLibraryTrackAddFromQueryRow(queryToAdd?.datasource?.type || ''); - } - }} - queryToAdd={queryToAdd!} - /> -
); } diff --git a/public/app/features/explore/ExploreRunQueryButton.tsx b/public/app/features/explore/ExploreRunQueryButton.tsx index 7cb37a62a45..48c7831c9d2 100644 --- a/public/app/features/explore/ExploreRunQueryButton.tsx +++ b/public/app/features/explore/ExploreRunQueryButton.tsx @@ -3,7 +3,7 @@ import { ConnectedProps, connect } from 'react-redux'; import { config, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; -import { Button, ButtonVariant, Dropdown, Menu, ToolbarButton } from '@grafana/ui'; +import { Button, Dropdown, Menu, ToolbarButton } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { useSelector } from 'app/types'; @@ -22,7 +22,6 @@ interface ExploreRunQueryButtonProps { queries: DataQuery[]; rootDatasourceUid?: string; disabled?: boolean; - variant?: ButtonVariant; onClick?: () => void; } @@ -37,7 +36,6 @@ export function ExploreRunQueryButton({ rootDatasourceUid, queries, disabled = false, - variant = 'secondary', onClick, changeDatasource, setQueries, @@ -84,7 +82,7 @@ export function ExploreRunQueryButton({ const buttonText = runQueryText(exploreId, rootDatasourceUid); return ( + + {/* Show either a drops down button so that user can select QL or QH, or show a close button if one of them is + already open.*/} + {drawerOpened || isQueryLibraryDrawerOpen ? ( + ) : ( - + )} diff --git a/public/app/features/explore/QueriesDrawer/mocks.tsx b/public/app/features/explore/QueriesDrawer/mocks.tsx index 37bf17fada4..3aba930f566 100644 --- a/public/app/features/explore/QueriesDrawer/mocks.tsx +++ b/public/app/features/explore/QueriesDrawer/mocks.tsx @@ -8,13 +8,12 @@ type Props = { } & PropsWithChildren; export function QueriesDrawerContextProviderMock(props: Props) { - const [selectedTab, setSelectedTab] = useState(Tabs.QueryLibrary); + const [selectedTab, setSelectedTab] = useState(Tabs.RichHistory); const [drawerOpened, setDrawerOpened] = useState(false); return ( void; + query?: DataQuery; +}; + +export function AddToQueryLibraryModal({ query, close, isOpen }: Props) { + return ( + close()} + > + { + close(); + }} + onSave={(isSuccess) => { + if (isSuccess) { + close(); + queryLibraryTrackAddFromQueryRow(query?.datasource?.type || ''); + } + }} + queryToAdd={query!} + /> + + ); +} diff --git a/public/app/features/explore/QueryLibrary/QueryLibrary.tsx b/public/app/features/explore/QueryLibrary/QueryLibrary.tsx index fffc454ee3a..c9b81574b1f 100644 --- a/public/app/features/explore/QueryLibrary/QueryLibrary.tsx +++ b/public/app/features/explore/QueryLibrary/QueryLibrary.tsx @@ -2,11 +2,13 @@ import { useLocalStorage } from 'react-use'; import { QueryLibraryExpmInfo } from './QueryLibraryExpmInfo'; import { QueryTemplatesList } from './QueryTemplatesList'; +import { QueryActionButton } from './types'; export interface QueryLibraryProps { // List of active datasources to filter the query library by // E.g in Explore the active datasources are the datasources that are currently selected in the query editor activeDatasources?: string[]; + queryActionButton?: QueryActionButton; } export const QUERY_LIBRARY_LOCAL_STORAGE_KEYS = { @@ -16,7 +18,7 @@ export const QUERY_LIBRARY_LOCAL_STORAGE_KEYS = { }, }; -export function QueryLibrary({ activeDatasources }: QueryLibraryProps) { +export function QueryLibrary({ activeDatasources, queryActionButton }: QueryLibraryProps) { const [notifyUserAboutQueryLibrary, setNotifyUserAboutQueryLibrary] = useLocalStorage( QUERY_LIBRARY_LOCAL_STORAGE_KEYS.explore.notifyUserAboutQueryLibrary, true @@ -28,7 +30,7 @@ export function QueryLibrary({ activeDatasources }: QueryLibraryProps) { isOpen={notifyUserAboutQueryLibrary || false} onDismiss={() => setNotifyUserAboutQueryLibrary(false)} /> - + ); } diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryContext.test.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryContext.test.tsx new file mode 100644 index 00000000000..c48495b9103 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryLibraryContext.test.tsx @@ -0,0 +1,79 @@ +import { act, render, screen, waitFor } from '@testing-library/react'; +import { ComponentType } from 'react'; + +import { PromQuery } from '@grafana/prometheus'; + +import { useQueryLibraryContext, QueryLibraryContextProvider, QueryLibraryContextType } from './QueryLibraryContext'; + +// Bit of mocking here mainly so we don't have to mock too much of the API calls here and keep this test focused on the +// context state management and correct rendering. + +jest.mock('./AddToQueryLibraryModal', () => ({ + __esModule: true, + AddToQueryLibraryModal: (props: { isOpen: boolean; query: unknown }) => + props.isOpen &&
QUERY_MODAL {JSON.stringify(props.query)}
, +})); + +jest.mock('./QueryLibraryDrawer', () => ({ + __esModule: true, + QueryLibraryDrawer: (props: { + isOpen: boolean; + activeDatasources: string[] | undefined; + queryActionButton: ComponentType; + }) => + props.isOpen && ( +
+ QUERY_DRAWER {JSON.stringify(props.activeDatasources)} {props.queryActionButton && } +
+ ), +})); + +function setup() { + let ctx: { current: QueryLibraryContextType | undefined } = { current: undefined }; + function TestComp() { + ctx.current = useQueryLibraryContext(); + return
; + } + // rendering instead of just using renderHook so we can check if the modal and drawer actually render. + const renderResult = render( + + + + ); + + return { ctx, renderResult }; +} + +describe('QueryLibraryContext', () => { + it('should not render modal or drawer by default', () => { + setup(); + // should catch both modal and drawer + expect(screen.queryByText(/QUERY_MODAL/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/QUERY_DRAWER/i)).not.toBeInTheDocument(); + }); + + it('should be able to open modal', async () => { + const { ctx } = setup(); + act(() => { + ctx.current!.openAddQueryModal({ refId: 'A', expr: 'http_requests_total{job="test"}' } as PromQuery); + }); + + await waitFor(() => { + expect(screen.queryByText(/QUERY_MODAL/i)).toBeInTheDocument(); + expect(screen.queryByText(/http_requests_total\{job=\\"test\\"}/i)).toBeInTheDocument(); + }); + }); + + it('should be able to open drawer', async () => { + const { ctx } = setup(); + act(() => { + ctx.current!.openDrawer(['PROM_TEST_DS'], () =>
QUERY_ACTION_BUTTON
); + }); + + await waitFor(() => { + expect(screen.queryByText(/QUERY_DRAWER/i)).toBeInTheDocument(); + expect(screen.queryByText(/PROM_TEST_DS/i)).toBeInTheDocument(); + expect(screen.queryByText(/QUERY_ACTION_BUTTON/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx new file mode 100644 index 00000000000..65a815c7401 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx @@ -0,0 +1,118 @@ +import { PropsWithChildren, useState, createContext, useContext, useCallback, useMemo } from 'react'; + +import { DataQuery } from '@grafana/schema'; + +import { AddToQueryLibraryModal } from './AddToQueryLibraryModal'; +import { QueryLibraryDrawer } from './QueryLibraryDrawer'; +import { QueryActionButton, QueryActionButtonProps } from './types'; + +/** + * Context with state and action to interact with Query Library. The Query Library feature consists of a drawer + * that shows existing queries and allows users to use them and manage them and then an AddQueryModal which allows + * users to save a query into the library. Both of those are included in Grafana AppChrome component. + * + * Use this context to interact with those components, showing, hiding and setting initial state for them. + */ +export type QueryLibraryContextType = { + /** + * Opens a drawer with query library. + * @param datasourceFilters Data source names that will be used for initial filter in the library. + * @param queryActionButton Action button will be shown in the library next to the query and can implement context + * specific actions with the library, like running the query or updating some query in the current app. + */ + openDrawer: (datasourceFilters: string[], queryActionButton: QueryActionButton) => void; + closeDrawer: () => void; + isDrawerOpen: boolean; + + /** + * Opens a modal for adding a query to the library. + * @param query + */ + openAddQueryModal: (query: DataQuery) => void; + closeAddQueryModal: () => void; +}; + +export const QueryLibraryContext = createContext({ + openDrawer: () => {}, + closeDrawer: () => {}, + isDrawerOpen: false, + + openAddQueryModal: () => {}, + closeAddQueryModal: () => {}, +}); + +export function useQueryLibraryContext() { + return useContext(QueryLibraryContext); +} + +export function QueryLibraryContextProvider({ children }: PropsWithChildren) { + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [activeDatasources, setActiveDatasources] = useState([]); + const [isAddQueryModalOpen, setIsAddQueryModalOpen] = useState(false); + const [activeQuery, setActiveQuery] = useState(undefined); + const [queryActionButton, setQueryActionButton] = useState(undefined); + + const openDrawer = useCallback((datasourceFilters: string[], queryActionButton: QueryActionButton) => { + setActiveDatasources(datasourceFilters); + // Because the queryActionButton can be a function component it would be called as a callback if just passed in. + setQueryActionButton(() => queryActionButton); + setIsDrawerOpen(true); + }, []); + + const closeDrawer = useCallback(() => { + setActiveDatasources([]); + setQueryActionButton(undefined); + setIsDrawerOpen(false); + }, []); + + const openAddQueryModal = useCallback((query: DataQuery) => { + setActiveQuery(query); + setIsAddQueryModalOpen(true); + }, []); + + const closeAddQueryModal = useCallback(() => { + setActiveQuery(undefined); + setIsAddQueryModalOpen(false); + }, []); + + // We wrap the action button one time to add the closeDrawer behaviour. This way whoever injects the action button + // does not need to remember to do it nor the query table inside that renders it needs to know about the drawer. + const finalActionButton = useMemo(() => { + if (!queryActionButton) { + return queryActionButton; + } + return (props: QueryActionButtonProps) => { + const QButton = queryActionButton; + return ( + { + props.onClick(); + closeDrawer(); + }} + /> + ); + }; + }, [closeDrawer, queryActionButton]); + + return ( + + {children} + + + + ); +} diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx new file mode 100644 index 00000000000..18e2caead3d --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx @@ -0,0 +1,53 @@ +import { skipToken } from '@reduxjs/toolkit/query/react'; + +import { selectors } from '@grafana/e2e-selectors'; +import { TabbedContainer, TabConfig } from '@grafana/ui'; + +import { t } from '../../../core/internationalization'; +import { useListQueryTemplateQuery } from '../../query-library'; +import { QUERY_LIBRARY_GET_LIMIT } from '../../query-library/api/factory'; +import { ExploreDrawer } from '../ExploreDrawer'; + +import { QueryLibrary } from './QueryLibrary'; +import { QueryActionButton } from './types'; + +type Props = { + isOpen: boolean; + // List of datasource names to filter query templates by + activeDatasources: string[] | undefined; + close: () => void; + queryActionButton?: QueryActionButton; +}; + +/** + * Drawer with query library feature. Handles its own state and should be included in some top level component. + */ +export function QueryLibraryDrawer({ isOpen, activeDatasources, close, queryActionButton }: Props) { + const { data } = useListQueryTemplateQuery(isOpen ? {} : skipToken); + const queryTemplatesCount = data?.items?.length ?? 0; + + // TODO: the tabbed container is here mainly for close button and some margins maybe make sense to use something + // else as there is only one tab. + const tabs: TabConfig[] = [ + { + label: `${t('explore.rich-history.query-library', 'Query library')} (${queryTemplatesCount}/${QUERY_LIBRARY_GET_LIMIT})`, + value: 'Query library', + content: , + icon: 'book', + }, + ]; + + return ( + isOpen && ( + + + + ) + ); +} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx index 801b9899802..06dcf044093 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx @@ -28,11 +28,6 @@ export type QueryDetails = { description: string; }; -const VisibilityOptions = [ - { value: 'Public', label: t('explore.query-library.public', 'Public') }, - { value: 'Private', label: t('explore.query-library.private', 'Private') }, -]; - const getInstuctions = (isAdd: boolean) => { return isAdd ? t( @@ -155,7 +150,14 @@ export const QueryTemplateForm = ({ onCancel, onSave, queryToAdd, templateData } - + { + const actual = jest.requireActual('app/features/query-library'); + return { + ...actual, + useDeleteQueryTemplateMutation: () => [() => {}], + useListQueryTemplateQuery: () => { + return { + data: data, + isLoading: false, + error: null, + }; + }, + }; +}); + +jest.mock('./utils/dataFetching', () => { + return { + __esModule: true, + useLoadQueryMetadata: () => { + return { + loading: false, + value: [ + { + index: '0', + uid: '0', + datasourceName: 'prometheus', + datasourceRef: { type: 'prometheus', uid: 'Prometheus0' }, + datasourceType: 'prometheus', + createdAtTimestamp: 0, + query: { refId: 'A' }, + queryText: 'http_requests_total{job="test"}', + description: 'template0', + user: { + uid: 'viewer:JohnDoe', + displayName: 'John Doe', + avatarUrl: '', + }, + error: undefined, + }, + ], + }; + }, + useLoadUsers: () => { + return { + value: { + display: [ + { + avatarUrl: '', + displayName: 'john doe', + identity: { + name: 'JohnDoe', + type: 'viewer', + }, + }, + ], + }, + loading: false, + error: null, + }; + }, + }; +}); + +describe('QueryTemplatesList', () => { + it('renders empty state', async () => { + data = {}; + render(); + await waitFor(() => { + expect(screen.getByText(/You haven't saved any queries to your library yet/)).toBeInTheDocument(); + }); + }); + + it('renders query', async () => { + data.items = testItems; + render(); + await waitFor(() => { + // We don't really show query template title for some reason so creator name + expect(screen.getByText(/John Doe/)).toBeInTheDocument(); + }); + }); + + it('renders actionButton for query', async () => { + data.items = testItems; + let passedProps: QueryActionButtonProps; + + const queryActionButton = (props: QueryActionButtonProps) => { + passedProps = props; + return ; + }; + + render(); + await waitFor(() => { + // We don't really show query template title for some reason so creator name + expect(screen.getByText(/John Doe/)).toBeInTheDocument(); + expect(screen.getByText(/TEST_ACTION_BUTTON/)).toBeInTheDocument(); + // We didn't put much else into the query object but should be enough to check the prop + expect(passedProps.queries).toMatchObject([{ refId: 'A' }]); + }); + }); +}); + +const testItems = [ + { + metadata: { + name: 'TEST_QUERY', + creationTimestamp: '2025-01-01T11:11:11.00Z', + annotations: { + [CREATED_BY_KEY]: 'viewer:JohnDoe', + }, + }, + spec: { + title: 'Test Query title', + targets: [ + { + variables: {}, + properties: { + refId: 'A', + datasource: { + uid: 'Prometheus', + type: 'prometheus', + }, + }, + }, + ], + }, + }, +]; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx index 7f238354704..54dc5951b88 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx @@ -1,24 +1,22 @@ import { css } from '@emotion/css'; -import { compact, uniq, uniqBy } from 'lodash'; +import { uniqBy } from 'lodash'; import { useEffect, useMemo, useState } from 'react'; import { AppEvents, GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { getAppEvents, getDataSourceSrv } from '@grafana/runtime'; +import { getAppEvents } from '@grafana/runtime'; import { EmptyState, FilterInput, InlineLabel, MultiSelect, Spinner, useStyles2, Stack, Badge } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; -import { createQueryText } from 'app/core/utils/richHistory'; import { useListQueryTemplateQuery } from 'app/features/query-library'; -import { getUserInfo } from 'app/features/query-library/api/user'; import { QueryTemplate } from 'app/features/query-library/types'; -import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { convertDataQueryResponseToQueryTemplates } from '../../query-library/api/mappers'; +import { UserDataQueryResponse } from '../../query-library/api/types'; import { QueryLibraryProps } from './QueryLibrary'; import { queryLibraryTrackFilterDatasource } from './QueryLibraryAnalyticsEvents'; import { QueryLibraryExpmInfo } from './QueryLibraryExpmInfo'; import QueryTemplatesTable from './QueryTemplatesTable'; -import { QueryTemplateRow } from './QueryTemplatesTable/types'; +import { useLoadQueryMetadata, useLoadUsers } from './utils/dataFetching'; import { searchQueryLibrary } from './utils/search'; interface QueryTemplatesListProps extends QueryLibraryProps {} @@ -31,115 +29,29 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) { const [datasourceFilters, setDatasourceFilters] = useState>>( props.activeDatasources?.map((ds) => ({ value: ds, label: ds })) || [] ); - const [userData, setUserData] = useState([]); const [userFilters, setUserFilters] = useState>>([]); - - const [allQueryTemplateRows, setAllQueryTemplateRows] = useState([]); - const [isRowsLoading, setIsRowsLoading] = useState(true); const styles = useStyles2(getStyles); - useEffect(() => { - let shouldCancel = true; + const loadUsersResult = useLoadUsersWithError(data); + const userNames = loadUsersResult.value ? loadUsersResult.value.display.map((user) => user.displayName) : []; - const fetchRows = async () => { - if (!data) { - setIsRowsLoading(false); - return; - } + const loadQueryMetadataResult = useLoadQueryMetadataWithError(data, loadUsersResult.value); - let userDataList; - const userQtList = uniq(compact(data.map((qt) => qt.user?.uid))); - const usersParam = userQtList.map((userUid) => `key=${encodeURIComponent(userUid)}`).join('&'); - try { - userDataList = await getUserInfo(`?${usersParam}`); - } catch (error) { - getAppEvents().publish({ - type: AppEvents.alertError.name, - payload: [ - t('query-library.user-info-get-error', 'Error attempting to get user info from the library: {{error}}', { - error: JSON.stringify(error), - }), - ], - }); - setIsRowsLoading(false); - return; - } - - setUserData(userDataList.display.map((user) => user.displayName)); - - const rowsPromises = data.map(async (queryTemplate: QueryTemplate, index: number) => { - try { - const datasourceRef = queryTemplate.targets[0]?.datasource; - const datasourceApi = await getDataSourceSrv().get(datasourceRef); - const datasourceType = getDatasourceSrv().getInstanceSettings(datasourceRef)?.meta.name || ''; - const query = queryTemplate.targets[0]; - const queryText = createQueryText(query, datasourceApi); - const datasourceName = datasourceApi?.name || ''; - const extendedUserData = userDataList.display.find( - (user) => `${user?.identity.type}:${user?.identity.name}` === queryTemplate.user?.uid - ); - - return { - index: index.toString(), - uid: queryTemplate.uid, - datasourceName, - datasourceRef, - datasourceType, - createdAtTimestamp: queryTemplate?.createdAtTimestamp || 0, - query, - queryText, - description: queryTemplate.title, - user: { - uid: queryTemplate.user?.uid || '', - displayName: extendedUserData?.displayName || '', - avatarUrl: extendedUserData?.avatarUrl || '', - }, - }; - } catch (error) { - getAppEvents().publish({ - type: AppEvents.alertError.name, - payload: [ - t( - 'query-library.query-template-get-error', - 'Error attempting to get query template from the library: {{error}}', - { error: JSON.stringify(error) } - ), - ], - }); - return { index: index.toString(), error }; - } - }); - - const results = await Promise.allSettled(rowsPromises); - const rows = results.filter((result) => result.status === 'fulfilled').map((result) => result.value); - - if (shouldCancel) { - setAllQueryTemplateRows(rows); - setIsRowsLoading(false); - } - }; - - fetchRows(); - - return () => { - shouldCancel = false; - }; - }, [data]); - - const queryTemplateRows = useMemo( + // Filtering right now is done just on the frontend until there is better backend support for this. + const filteredRows = useMemo( () => searchQueryLibrary( - allQueryTemplateRows, + loadQueryMetadataResult.value || [], searchQuery, datasourceFilters.map((f) => f.value || ''), userFilters.map((f) => f.value || '') ), - [allQueryTemplateRows, searchQuery, datasourceFilters, userFilters] + [loadQueryMetadataResult.value, searchQuery, datasourceFilters, userFilters] ); const datasourceNames = useMemo(() => { - return uniqBy(allQueryTemplateRows, 'datasourceName').map((row) => row.datasourceName); - }, [allQueryTemplateRows]); + return uniqBy(loadQueryMetadataResult.value, 'datasourceName').map((row) => row.datasourceName); + }, [loadQueryMetadataResult.value]); if (error) { return ( @@ -149,7 +61,7 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) { ); } - if (isLoading || isRowsLoading) { + if (isLoading || loadUsersResult.loading || loadQueryMetadataResult.loading) { return ; } @@ -197,13 +109,14 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) { User name(s): { setUserFilters(items); actionMeta.action === 'select-option' && queryLibraryTrackFilterDatasource(); }} value={userFilters} - options={userData.map((r) => { + options={userNames.map((r) => { return { value: r, label: r }; })} placeholder={'Filter queries for user name(s)'} @@ -219,11 +132,83 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) { onClick={() => setIsModalOpen(true)} /> - + ); } +/** + * Wrap useLoadUsers with error handling. + * @param data + */ +function useLoadUsersWithError(data: QueryTemplate[] | undefined) { + const userUIDs = useMemo(() => data?.map((qt) => qt.user?.uid).filter((uid) => uid !== undefined), [data]); + const loadUsersResult = useLoadUsers(userUIDs); + useEffect(() => { + if (loadUsersResult.error) { + getAppEvents().publish({ + type: AppEvents.alertError.name, + payload: [ + t('query-library.user-info-get-error', 'Error attempting to get user info from the library: {{error}}', { + error: JSON.stringify(loadUsersResult.error), + }), + ], + }); + } + }, [loadUsersResult.error]); + return loadUsersResult; +} + +/** + * Wrap useLoadQueryMetadata with error handling. + * @param queryTemplates + * @param userDataList + */ +function useLoadQueryMetadataWithError( + queryTemplates: QueryTemplate[] | undefined, + userDataList: UserDataQueryResponse | undefined +) { + const result = useLoadQueryMetadata(queryTemplates, userDataList); + + // useLoadQueryMetadata returns errors in the values so we filter and group them and later alert only one time for + // all the errors. This way we show data that is loaded even if some rows errored out. + // TODO: maybe we could show the rows with incomplete data to see exactly which ones errored out. I assume this + // can happen for example when data source for saved query was deleted. Would be nice if user would still be able + // to delete such row or decide what to do. + const [values, errors] = useMemo(() => { + let errors: Error[] = []; + let values = []; + if (!result.loading) { + for (const value of result.value!) { + if (value.error) { + errors.push(value.error); + } else { + values.push(value); + } + } + } + return [values, errors]; + }, [result]); + + useEffect(() => { + if (errors.length) { + getAppEvents().publish({ + type: AppEvents.alertError.name, + payload: [ + t('query-library.query-template-get-error', 'Error attempting to load query template metadata: {{error}}', { + error: JSON.stringify(errors), + }), + ], + }); + } + }, [errors]); + + return { + loading: result.loading, + value: values, + }; +} + const getStyles = (theme: GrafanaTheme2) => ({ searchInput: css({ maxWidth: theme.spacing(55), diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx index 88174485be1..575f10bd063 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx @@ -9,14 +9,13 @@ import { useDeleteQueryTemplateMutation } from 'app/features/query-library'; import { dispatch } from 'app/store/store'; import { ShowConfirmModalEvent } from 'app/types/events'; -import ExploreRunQueryButton from '../../ExploreRunQueryButton'; -import { useQueriesDrawerContext } from '../../QueriesDrawer/QueriesDrawerContext'; import { queryLibaryTrackDeleteQuery, queryLibraryTrackAddOrEditDescription, queryLibraryTrackRunQuery, } from '../QueryLibraryAnalyticsEvents'; import { QueryTemplateForm } from '../QueryTemplateForm'; +import { QueryActionButton } from '../types'; import { useQueryLibraryListStyles } from './styles'; import { QueryTemplateRow } from './types'; @@ -25,12 +24,12 @@ interface ActionsCellProps { queryUid?: string; queryTemplate: QueryTemplateRow; rootDatasourceUid?: string; + QueryActionButton?: QueryActionButton; } -function ActionsCell({ queryTemplate, rootDatasourceUid, queryUid }: ActionsCellProps) { +function ActionsCell({ queryTemplate, rootDatasourceUid, queryUid, QueryActionButton }: ActionsCellProps) { const [deleteQueryTemplate] = useDeleteQueryTemplateMutation(); const [editFormOpen, setEditFormOpen] = useState(false); - const { setDrawerOpened } = useQueriesDrawerContext(); const styles = useQueryLibraryListStyles(); const onDeleteQuery = (queryUid: string) => { @@ -82,15 +81,15 @@ function ActionsCell({ queryTemplate, rootDatasourceUid, queryUid }: ActionsCell queryLibraryTrackAddOrEditDescription(); }} /> - { - setDrawerOpened(false); - queryLibraryTrackRunQuery(queryTemplate.datasourceType || ''); - }} - /> + {QueryActionButton && ( + { + queryLibraryTrackRunQuery(queryTemplate.datasourceType || ''); + }} + /> + )} = (rowA, rowB, _, desc) => { return desc ? timeA - timeB : timeB - timeA; }; -const columns: Array> = [ - { id: 'description', header: 'Data source and query', cell: QueryDescriptionCell }, - { id: 'addedBy', header: 'Added by', cell: ({ row: { original } }) => }, - { id: 'datasourceType', header: 'Datasource type', cell: DatasourceTypeCell, sortType: 'string' }, - { id: 'createdAtTimestamp', header: 'Date added', cell: DateAddedCell, sortType: timestampSort }, - { - id: 'actions', - header: '', - cell: ({ row: { original } }) => ( - - ), - }, -]; +function createColumns(queryActionButton?: QueryActionButton): Array> { + return [ + { id: 'description', header: 'Data source and query', cell: QueryDescriptionCell }, + { id: 'addedBy', header: 'Added by', cell: ({ row: { original } }) => }, + { id: 'datasourceType', header: 'Datasource type', cell: DatasourceTypeCell, sortType: 'string' }, + { id: 'createdAtTimestamp', header: 'Date added', cell: DateAddedCell, sortType: timestampSort }, + { + id: 'actions', + header: '', + cell: ({ row: { original } }) => ( + + ), + }, + ]; +} type Props = { queryTemplateRows: QueryTemplateRow[]; + queryActionButton?: QueryActionButton; }; -export default function QueryTemplatesTable({ queryTemplateRows }: Props) { +export default function QueryTemplatesTable({ queryTemplateRows, queryActionButton }: Props) { const styles = useStyles2(getStyles); + const columns = createColumns(queryActionButton); + return ( { + openAddQueryModal(query); + setShowQueryLibraryBadgeButton(false); + }} + style={{ cursor: 'pointer' }} + /> + ) : ( + { + openAddQueryModal(query); + }} + /> + ); +} diff --git a/public/app/features/explore/QueryLibrary/types.ts b/public/app/features/explore/QueryLibrary/types.ts new file mode 100644 index 00000000000..b36c7280fe8 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/types.ts @@ -0,0 +1,11 @@ +import { ComponentType } from 'react'; + +import { DataQuery } from '@grafana/schema'; + +export type QueryActionButtonProps = { + queries: DataQuery[]; + datasourceUid?: string; + onClick: () => void; +}; + +export type QueryActionButton = ComponentType; diff --git a/public/app/features/explore/QueryLibrary/utils/dataFetching.ts b/public/app/features/explore/QueryLibrary/utils/dataFetching.ts new file mode 100644 index 00000000000..67078936d93 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/utils/dataFetching.ts @@ -0,0 +1,106 @@ +import { compact, uniq } from 'lodash'; +import { useAsync } from 'react-use'; +import { AsyncState } from 'react-use/lib/useAsync'; + +import { getDataSourceSrv } from '@grafana/runtime'; +import { DataQuery, DataSourceRef } from '@grafana/schema'; + +import { createQueryText } from '../../../../core/utils/richHistory'; +import { getDatasourceSrv } from '../../../plugins/datasource_srv'; +import { UserDataQueryResponse } from '../../../query-library/api/types'; +import { getUserInfo } from '../../../query-library/api/user'; +import { QueryTemplate } from '../../../query-library/types'; + +export function useLoadUsers(userUIDs: string[] | undefined) { + return useAsync(async () => { + if (!userUIDs) { + return undefined; + } + const userQtList = uniq(compact(userUIDs)); + const usersParam = userQtList.map((userUid) => `key=${encodeURIComponent(userUid)}`).join('&'); + return await getUserInfo(`?${usersParam}`); + }, [userUIDs]); +} + +// Explicitly type the result so TS knows to discriminate between the error result and good result by the error prop +// value. +type MetadataValue = + | { + index: string; + uid: string; + datasourceName: string; + datasourceRef: DataSourceRef | undefined | null; + datasourceType: string; + createdAtTimestamp: number; + query: DataQuery; + queryText: string; + description: string; + user: { + uid: string; + displayName: string; + avatarUrl: string; + }; + error: undefined; + } + | { + index: string; + error: Error; + }; + +/** + * Map metadata to query templates we get from the DB. + * @param queryTemplates + * @param userDataList + */ +export function useLoadQueryMetadata( + queryTemplates: QueryTemplate[] | undefined, + userDataList: UserDataQueryResponse | undefined +): AsyncState { + return useAsync(async () => { + if (!(queryTemplates && userDataList)) { + return []; + } + + const rowsPromises = queryTemplates.map( + async (queryTemplate: QueryTemplate, index: number): Promise => { + try { + const datasourceRef = queryTemplate.targets[0]?.datasource; + const datasourceApi = await getDataSourceSrv().get(datasourceRef); + const datasourceType = getDatasourceSrv().getInstanceSettings(datasourceRef)?.meta.name || ''; + const query = queryTemplate.targets[0]; + const queryText = createQueryText(query, datasourceApi); + const datasourceName = datasourceApi?.name || ''; + const extendedUserData = userDataList.display.find( + (user) => `${user?.identity.type}:${user?.identity.name}` === queryTemplate.user?.uid + ); + + return { + index: index.toString(), + uid: queryTemplate.uid, + datasourceName, + datasourceRef, + datasourceType, + createdAtTimestamp: queryTemplate?.createdAtTimestamp || 0, + query, + queryText, + description: queryTemplate.title, + user: { + uid: queryTemplate.user?.uid || '', + displayName: extendedUserData?.displayName || '', + avatarUrl: extendedUserData?.avatarUrl || '', + }, + error: undefined, + }; + } catch (error) { + // Instead of throwing we collect the errors in the result so upstream code can decide what to do. + return { + index: index.toString(), + error: error instanceof Error ? error : new Error('unknown error ' + JSON.stringify(error)), + }; + } + } + ); + + return Promise.all(rowsPromises); + }, [queryTemplates, userDataList]); +} diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index b614401cde6..60bce0a6ec3 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -11,15 +11,12 @@ import { RichHistorySettings, createDatasourcesList, } from 'app/core/utils/richHistory'; -import { QUERY_LIBRARY_GET_LIMIT } from 'app/features/query-library/api/factory'; import { useSelector } from 'app/types'; import { RichHistoryQuery } from 'app/types/explore'; import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider'; -import { useListQueryTemplateQuery } from '../../query-library'; -import { Tabs, useQueriesDrawerContext } from '../QueriesDrawer/QueriesDrawerContext'; +import { Tabs } from '../QueriesDrawer/QueriesDrawerContext'; import { i18n } from '../QueriesDrawer/utils'; -import { QueryLibrary } from '../QueryLibrary/QueryLibrary'; import { selectExploreDSMaps } from '../state/selectors'; import { RichHistoryQueriesTab } from './RichHistoryQueriesTab'; @@ -55,8 +52,6 @@ export function RichHistory(props: RichHistoryProps) { const [loading, setLoading] = useState(false); - const { queryLibraryAvailable } = useQueriesDrawerContext(); - const updateSettings = (settingsToUpdate: Partial) => { props.updateHistorySettings({ ...props.richHistorySettings, ...settingsToUpdate }); }; @@ -98,16 +93,6 @@ export function RichHistory(props: RichHistoryProps) { .map((eDs) => listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name) .filter((name): name is string => !!name); - const { data } = useListQueryTemplateQuery({}); - const queryTemplatesCount = data?.items?.length ?? 0; - - const QueryLibraryTab: TabConfig = { - label: `${i18n.queryLibrary} (${queryTemplatesCount}/${QUERY_LIBRARY_GET_LIMIT})`, - value: Tabs.QueryLibrary, - content: , - icon: 'book', - }; - const QueriesTab: TabConfig = { label: i18n.queryHistory, value: Tabs.RichHistory, @@ -164,7 +149,7 @@ export function RichHistory(props: RichHistoryProps) { icon: 'sliders-v-alt', }; - let tabs = (queryLibraryAvailable ? [QueryLibraryTab] : []).concat([QueriesTab, StarredTab, SettingsTab]); + let tabs = [QueriesTab, StarredTab, SettingsTab]; return ( Loading... diff --git a/public/app/features/explore/spec/helper/interactions.ts b/public/app/features/explore/spec/helper/interactions.ts index a74ffb783c1..41b3f41ae73 100644 --- a/public/app/features/explore/spec/helper/interactions.ts +++ b/public/app/features/explore/spec/helper/interactions.ts @@ -26,9 +26,17 @@ export const runQuery = async (exploreId = 'left') => { }; export const openQueryHistory = async () => { - const button = screen.getByRole('button', { name: 'Query history' }); - await userEvent.click(button); - expect(await screen.findByPlaceholderText('Search queries')).toBeInTheDocument(); + let button = screen.queryByRole('button', { name: 'Query history' }); + if (button) { + await userEvent.click(button); + expect(await screen.findByPlaceholderText('Search queries')).toBeInTheDocument(); + } else { + button = screen.getByRole('button', { name: 'Open query library or query history' }); + await userEvent.click(button); + button = await screen.findByRole('menuitem', { name: 'Query history' }); + await userEvent.click(button); + expect(await screen.findByPlaceholderText('Search queries')).toBeInTheDocument(); + } }; export const openQueryLibrary = async () => { @@ -41,13 +49,6 @@ export const openQueryLibrary = async () => { }); }; -export const switchToQueryHistory = async () => { - const tab = screen.getByRole('tab', { - name: /query history/i, - }); - await userEvent.click(tab); -}; - export const addQueryHistoryToQueryLibrary = async () => { const button = withinQueryHistory().getByRole('button', { name: /add to library/i }); await userEvent.click(button); diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index 2ec9733a4d4..c57cf52c435 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -47,6 +47,7 @@ import { ExploreQueryParams } from '../../../../types'; import { initialUserState } from '../../../profile/state/reducers'; import ExplorePage from '../../ExplorePage'; import { QueriesDrawerContextProvider } from '../../QueriesDrawer/QueriesDrawerContext'; +import { QueryLibraryContextProvider } from '../../QueryLibrary/QueryLibraryContext'; type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; @@ -182,25 +183,29 @@ export function setupExplore(options?: SetupOptions): { - - {options?.withAppChrome ? ( - - - } - /> - - - ) : ( - } - /> - )} - + + + {options?.withAppChrome ? ( + + + ( + + )} + /> + + + ) : ( + } + /> + )} + + diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx index 90d889c4625..1e3d7b3a75d 100644 --- a/public/app/features/explore/spec/queryHistory.test.tsx +++ b/public/app/features/explore/spec/queryHistory.test.tsx @@ -7,6 +7,7 @@ import store from 'app/core/store'; import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput'; import * as localStorage from '../../../core/history/RichHistoryLocalStorage'; +import { Tabs } from '../QueriesDrawer/QueriesDrawerContext'; import { assertDataSourceFilterVisibility, @@ -127,6 +128,7 @@ describe('Explore: Query History', () => { expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_opened', { queryHistoryEnabled: false, + selectedTab: Tabs.RichHistory, }); }); diff --git a/public/app/features/explore/spec/queryLibrary.test.tsx b/public/app/features/explore/spec/queryLibrary.test.tsx index dc942fa21a8..a9cc5345b23 100644 --- a/public/app/features/explore/spec/queryLibrary.test.tsx +++ b/public/app/features/explore/spec/queryLibrary.test.tsx @@ -16,7 +16,6 @@ import { openQueryHistory, openQueryLibrary, submitAddToQueryLibrary, - switchToQueryHistory, } from './helper/interactions'; import { setupExplore, waitForExplore } from './helper/setup'; @@ -115,8 +114,7 @@ describe('QueryLibrary', () => { it('Shows add to query library button only when the toggle is enabled', async () => { setupQueryLibrary(); await waitForExplore(); - await openQueryLibrary(); - await switchToQueryHistory(); + await openQueryHistory(); await assertQueryHistory(['{"expr":"TEST"}']); await assertAddToQueryLibraryButtonExists(true); }); @@ -134,8 +132,7 @@ describe('QueryLibrary', () => { it('Shows a notification when a template is added and hides the add button', async () => { setupQueryLibrary(); await waitForExplore(); - await openQueryLibrary(); - await switchToQueryHistory(); + await openQueryHistory(); await assertQueryHistory(['{"expr":"TEST"}']); await addQueryHistoryToQueryLibrary(); await submitAddToQueryLibrary({ description: 'Test' }); diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 7094901511f..a3dc05930d7 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -24,7 +24,7 @@ import { toLegacyResponseData, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { AngularComponent, getAngularLoader, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { AngularComponent, config, getAngularLoader, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { Badge, ErrorBoundaryAlert } from '@grafana/ui'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; import { @@ -40,6 +40,8 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; +import { SaveQueryButton as SaveQueryToQueryLibraryButton } from '../../explore/QueryLibrary/SaveQueryButton'; + import { QueryActionComponent, RowActionComponents } from './QueryActionComponent'; import { QueryEditorRowHeader } from './QueryEditorRowHeader'; import { QueryErrorAlert } from './QueryErrorAlert'; @@ -487,6 +489,7 @@ export class QueryEditorRow extends PureComponent )} {this.renderExtraActions()} + {config.featureToggles.queryLibrary && } - - - - - - {props.pageBanners.map((Banner, index) => ( - + + + + + + + {props.pageBanners.map((Banner, index) => ( + + ))} + {props.routes} + + {props.bodyRenderHooks.map((Hook, index) => ( + ))} - {props.routes} - - {props.bodyRenderHooks.map((Hook, index) => ( - - ))} - - - + + + + diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 7781727270f..3a270e122c3 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1077,6 +1077,12 @@ "angular-deprecation-description": "Angular panels options can only be edited using the JSON editor.", "angular-deprecation-heading": "Panel options" }, + "panel-queries": { + "add-query-from-library": "Add query from library" + }, + "query-library": { + "add-query-button": "Add query" + }, "settings": { "variables": { "dependencies": { @@ -1188,6 +1194,7 @@ "close-tooltip": "Close query history", "datasource-a-z": "Data source A-Z", "datasource-z-a": "Data source Z-A", + "library-history-dropdown": "Open query library or query history", "newest-first": "Newest first", "oldest-first": "Oldest first", "query-history": "Query history", @@ -2796,7 +2803,7 @@ "query-library": { "datasource-names": "Datasource name(s):", "delete-query-button": "Delete query", - "query-template-get-error": "Error attempting to get query template from the library: {{error}}", + "query-template-get-error": "Error attempting to load query template metadata: {{error}}", "search": "Search by data source, query content or description", "user-info-get-error": "Error attempting to get user info from the library: {{error}}", "user-names": "User name(s):" @@ -2811,6 +2818,7 @@ "hide-response": "Hide response", "remove-query": "Remove query", "save-to-query-library": "Save to query library", + "save-to-query-library-new": "New: Save to query library", "show-response": "Show response", "toggle-edit-mode": "Toggle text edit mode" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 006e0f4a07f..1495a78caee 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1077,6 +1077,12 @@ "angular-deprecation-description": "Åʼnģūľäř päʼnęľş őpŧįőʼnş čäʼn őʼnľy þę ęđįŧęđ ūşįʼnģ ŧĥę ĴŜØŃ ęđįŧőř.", "angular-deprecation-heading": "Päʼnęľ őpŧįőʼnş" }, + "panel-queries": { + "add-query-from-library": "Åđđ qūęřy ƒřőm ľįþřäřy" + }, + "query-library": { + "add-query-button": "Åđđ qūęřy" + }, "settings": { "variables": { "dependencies": { @@ -1188,6 +1194,7 @@ "close-tooltip": "Cľőşę qūęřy ĥįşŧőřy", "datasource-a-z": "Đäŧä şőūřčę Å-Ż", "datasource-z-a": "Đäŧä şőūřčę Ż-Å", + "library-history-dropdown": "Øpęʼn qūęřy ľįþřäřy őř qūęřy ĥįşŧőřy", "newest-first": "Ńęŵęşŧ ƒįřşŧ", "oldest-first": "Øľđęşŧ ƒįřşŧ", "query-history": "Qūęřy ĥįşŧőřy", @@ -2796,7 +2803,7 @@ "query-library": { "datasource-names": "Đäŧäşőūřčę ʼnämę(ş):", "delete-query-button": "Đęľęŧę qūęřy", - "query-template-get-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ģęŧ qūęřy ŧęmpľäŧę ƒřőm ŧĥę ľįþřäřy: {{error}}", + "query-template-get-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ľőäđ qūęřy ŧęmpľäŧę męŧäđäŧä: {{error}}", "search": "Ŝęäřčĥ þy đäŧä şőūřčę, qūęřy čőʼnŧęʼnŧ őř đęşčřįpŧįőʼn", "user-info-get-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ģęŧ ūşęř įʼnƒő ƒřőm ŧĥę ľįþřäřy: {{error}}", "user-names": "Ůşęř ʼnämę(ş):" @@ -2811,6 +2818,7 @@ "hide-response": "Ħįđę řęşpőʼnşę", "remove-query": "Ŗęmővę qūęřy", "save-to-query-library": "Ŝävę ŧő qūęřy ľįþřäřy", + "save-to-query-library-new": "Ńęŵ: Ŝävę ŧő qūęřy ľįþřäřy", "show-response": "Ŝĥőŵ řęşpőʼnşę", "toggle-edit-mode": "Ŧőģģľę ŧęχŧ ęđįŧ mőđę" }, From 7e1a8cb984dc18fc6d0497a131bee8648b6a4d95 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:55:08 +0000 Subject: [PATCH 235/894] Update dependency dompurify to v3.2.4 (#99799) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/grafana-data/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 2d5576fa453..383401eb4a7 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -41,7 +41,7 @@ "@types/string-hash": "1.1.3", "d3-interpolate": "3.0.1", "date-fns": "4.1.0", - "dompurify": "3.2.3", + "dompurify": "3.2.4", "eventemitter3": "5.0.1", "fast_array_intersect": "1.1.0", "history": "4.10.1", diff --git a/yarn.lock b/yarn.lock index cbc2f604fa8..d015ea5b6e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3214,7 +3214,7 @@ __metadata: "@types/tinycolor2": "npm:1.4.6" d3-interpolate: "npm:3.0.1" date-fns: "npm:4.1.0" - dompurify: "npm:3.2.3" + dompurify: "npm:3.2.4" esbuild: "npm:0.24.2" eventemitter3: "npm:5.0.1" fast_array_intersect: "npm:1.1.0" @@ -15026,15 +15026,15 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:3.2.3, dompurify@npm:^3.0.0": - version: 3.2.3 - resolution: "dompurify@npm:3.2.3" +"dompurify@npm:3.2.4, dompurify@npm:^3.0.0": + version: 3.2.4 + resolution: "dompurify@npm:3.2.4" dependencies: "@types/trusted-types": "npm:^2.0.7" dependenciesMeta: "@types/trusted-types": optional: true - checksum: 10/aad472bcdff40afdbb307fd02abbca86acefee9c39cb35e9634ebbc5e047750a7eeb021b02cd66894d60cf75ad021f69394de2e9e8786b0dd91c5832f497a9af + checksum: 10/98570c53385518a2f9b617f796926338856acfdd3369c88b5905bddf96bd7d391bf8a5433127155e0046e6faa2bfb767185fcd571b865dfabe624c099e2537f5 languageName: node linkType: hard From b683724becfadb1c7138b8dd1fd1f762d66b2106 Mon Sep 17 00:00:00 2001 From: Igor Suleymanov Date: Thu, 30 Jan 2025 16:10:42 +0200 Subject: [PATCH 236/894] Upgrade grafana-app-sdk to 0.31.0 (#99739) * Upgrade grafana-app-sdk to 0.31.0 What This commit upgrades the app SDK to 0.31.0 and re-generates codegen files. It doesn't touch alerting schemas, because those are quite old and should be upgraded separately. This commit slightly alters the schemas for the investigations app, because the codegen is not happy with the current syntax, for some reason (probably has to do with CUE upgrades in `cog`). Why To make sure we use up-to-date SDK version and remove the workaround for the `defencoding=none` bug that required us to clean up generated CRD files. Signed-off-by: Igor Suleymanov * Revert changes to golden file for store tests Signed-off-by: Igor Suleymanov --------- Signed-off-by: Igor Suleymanov --- apps/advisor/Makefile | 7 +- apps/advisor/go.mod | 2 +- apps/alerting/notifications/go.mod | 30 +- apps/alerting/notifications/go.sum | 60 ++-- apps/investigation/Makefile | 2 +- apps/investigation/go.mod | 32 +- apps/investigation/go.sum | 64 ++-- apps/investigation/kinds/investigation.cue | 155 ++++---- apps/investigation/kinds/manifest.cue | 9 + .../apis/investigation/v1alpha1/constants.go | 18 + .../v1alpha1/investigation_metadata_gen.go | 30 +- .../v1alpha1/investigation_object_gen.go | 14 +- .../v1alpha1/investigation_spec_gen.go | 167 +++++---- .../v1alpha1/investigation_status_gen.go | 74 ++-- .../investigation/v1alpha1/zz_openapi_gen.go | 332 ++++++++---------- ...{manifest.go => investigation_manifest.go} | 0 apps/playlist/Makefile | 7 +- apps/playlist/go.mod | 32 +- apps/playlist/go.sum | 64 ++-- go.mod | 38 +- go.sum | 76 ++-- go.work.sum | 32 ++ pkg/aggregator/go.mod | 28 +- pkg/aggregator/go.sum | 56 +-- pkg/apimachinery/go.mod | 14 +- pkg/apimachinery/go.sum | 32 +- pkg/apiserver/go.mod | 28 +- pkg/apiserver/go.sum | 56 +-- pkg/build/go.mod | 20 +- pkg/build/go.sum | 40 +-- 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 | 22 +- pkg/promlib/go.sum | 44 +-- pkg/semconv/go.mod | 2 +- pkg/semconv/go.sum | 4 +- pkg/storage/unified/apistore/go.mod | 30 +- pkg/storage/unified/apistore/go.sum | 64 ++-- pkg/storage/unified/resource/go.mod | 28 +- pkg/storage/unified/resource/go.sum | 56 +-- 42 files changed, 882 insertions(+), 899 deletions(-) create mode 100644 apps/investigation/kinds/manifest.cue create mode 100644 apps/investigation/pkg/apis/investigation/v1alpha1/constants.go rename apps/investigation/pkg/apis/{manifest.go => investigation_manifest.go} (100%) diff --git a/apps/advisor/Makefile b/apps/advisor/Makefile index 926f5a2840f..7a5f1c99365 100644 --- a/apps/advisor/Makefile +++ b/apps/advisor/Makefile @@ -1,8 +1,3 @@ .PHONY: generate generate: - @grafana-app-sdk generate -g ./pkg/apis --grouping=group --postprocess - # HACK: Clean up generated CRD files. - # TODO: The SDK currently omits generating the manifest Go file with `--defencoding=none`, - # which we would normally use here to skip generating the CRD files. - # This needs to be addressed. - @rm -rf definitions + @grafana-app-sdk generate -g ./pkg/apis --grouping=group --postprocess --defencoding=none diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 8fcc336a52b..df422dc8f68 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/advisor go 1.23.4 require ( - github.com/grafana/grafana-app-sdk v0.30.0 + github.com/grafana/grafana-app-sdk v0.31.0 k8s.io/apimachinery v0.32.0 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index f98229e5d4a..fa03852fea1 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -6,9 +6,9 @@ replace github.com/grafana/grafana => ../../.. require ( github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible - github.com/grafana/grafana-app-sdk v0.30.0 - k8s.io/apimachinery v0.32.0 - k8s.io/apiserver v0.32.0 + github.com/grafana/grafana-app-sdk v0.31.0 + k8s.io/apimachinery v0.32.1 + k8s.io/apiserver v0.32.1 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f ) @@ -63,12 +63,12 @@ require ( go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect @@ -80,16 +80,16 @@ require ( golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.9.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // 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/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect + google.golang.org/protobuf v1.36.3 // 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 - k8s.io/api v0.32.0 // indirect - k8s.io/client-go v0.32.0 // indirect - k8s.io/component-base v0.32.0 // indirect + k8s.io/api v0.32.1 // indirect + k8s.io/client-go v0.32.1 // indirect + k8s.io/component-base v0.32.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index b95d7af4f7b..df3c4bdd06f 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -71,8 +71,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/grafana-app-sdk v0.30.0 h1:Hqn2pETu2mQ4RpWkZYEQfu01P7xd1Z1Gj+HX/8aB0tw= -github.com/grafana/grafana-app-sdk v0.30.0/go.mod h1:jhfqNIovb+Mes2vdMf9iMCWQkp1GTNtyNuExONtiNuk= +github.com/grafana/grafana-app-sdk v0.31.0 h1:/mFCcx+YqG8cWAi9hePDJQxIdtXDClDIDRgZwHkksFk= +github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d h1:0WJ4uZ3gtbX7S5RGMarA+0TccmB7qEPhhNm34NcfBzs= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d/go.mod h1:PNmbi49lVrv2b2I8pdu46dTs2728lKEbnVuQD8I5MnM= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= @@ -182,20 +182,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -262,15 +262,15 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= 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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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= @@ -283,16 +283,16 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= -k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.0 h1:VJ89ZvQZ8p1sLeiWdRJpRD6oLozNZD2+qVSLi+ft5Qs= -k8s.io/apiserver v0.32.0/go.mod h1:HFh+dM1/BE/Hm4bS4nTXHVfN6Z6tFIZPi649n83b4Ag= -k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= -k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= -k8s.io/component-base v0.32.0 h1:d6cWHZkCiiep41ObYQS6IcgzOUQUNpywm39KVYaUqzU= -k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= +k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= +k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= diff --git a/apps/investigation/Makefile b/apps/investigation/Makefile index b4ea1f97c89..7a5f1c99365 100644 --- a/apps/investigation/Makefile +++ b/apps/investigation/Makefile @@ -1,3 +1,3 @@ .PHONY: generate generate: - @grafana-app-sdk generate -g ./pkg/apis --kindgrouping=group --postprocess --crdencoding none + @grafana-app-sdk generate -g ./pkg/apis --grouping=group --postprocess --defencoding=none diff --git a/apps/investigation/go.mod b/apps/investigation/go.mod index 3368592a726..531dfcc4346 100644 --- a/apps/investigation/go.mod +++ b/apps/investigation/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/investigation go 1.23.4 require ( - github.com/grafana/grafana-app-sdk v0.30.0 - k8s.io/apimachinery v0.32.0 + github.com/grafana/grafana-app-sdk v0.31.0 + k8s.io/apimachinery v0.32.1 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f ) @@ -31,7 +31,7 @@ require ( 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 - github.com/grafana/grafana-app-sdk/logging v0.29.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -53,13 +53,13 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.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 v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect @@ -69,15 +69,15 @@ require ( golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.9.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // 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/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect + google.golang.org/protobuf v1.36.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.32.0 // indirect - k8s.io/apiextensions-apiserver v0.32.0 // indirect - k8s.io/client-go v0.32.0 // indirect + k8s.io/api v0.32.1 // indirect + k8s.io/apiextensions-apiserver v0.32.1 // indirect + k8s.io/client-go v0.32.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect diff --git a/apps/investigation/go.sum b/apps/investigation/go.sum index 81f238b9979..bdb711181f5 100644 --- a/apps/investigation/go.sum +++ b/apps/investigation/go.sum @@ -49,10 +49,10 @@ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/Z github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.30.0 h1:Hqn2pETu2mQ4RpWkZYEQfu01P7xd1Z1Gj+HX/8aB0tw= -github.com/grafana/grafana-app-sdk v0.30.0/go.mod h1:jhfqNIovb+Mes2vdMf9iMCWQkp1GTNtyNuExONtiNuk= -github.com/grafana/grafana-app-sdk/logging v0.29.0 h1:mgbXaAf33aFwqwGVeaX30l8rkeAJH0iACgX5Rn6YkN4= -github.com/grafana/grafana-app-sdk/logging v0.29.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM= +github.com/grafana/grafana-app-sdk v0.31.0 h1:/mFCcx+YqG8cWAi9hePDJQxIdtXDClDIDRgZwHkksFk= +github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs= +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/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -124,22 +124,22 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -187,14 +187,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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= @@ -204,14 +204,14 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= -k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= -k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0= -k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= -k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= +k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= diff --git a/apps/investigation/kinds/investigation.cue b/apps/investigation/kinds/investigation.cue index 64d4b9b3105..0e0ed4bdf4c 100644 --- a/apps/investigation/kinds/investigation.cue +++ b/apps/investigation/kinds/investigation.cue @@ -1,96 +1,87 @@ -package kinds +package investigation // This is our Investigation definition, which contains metadata about the kind, and the kind's schema investigation: { - group: "investigation" kind: "Investigation" pluralName: "Investigations" - scope: "Namespaced" - - apiResource: { - groupOverride: "investigation.grafana.app" - } - - codegen: { - frontend: false - backend: true - } - - current: "v1alpha1" + current: "v1alpha1" versions: { "v1alpha1": { - version: "v1alpha1" + codegen: { + frontend: false + backend: true + } schema: { + #InvestigationSpec: { + title: string + status: "open" | "closed" + items: [...#InvestigationItem] + } + + // InvestigationItem is an item in an investigation. + #InvestigationItem: { + id: string + title: string + // type is the type of the item "timeseries", "heatmap", "log-table" (not an enum to allow for future extensions). + type: string + // url is the URL to the item. + url: string + // origin is where the item was created from. + origin: string // "explore-metrics", "explore-logs", "explore-traces" (not an enum to allow for future extensions) + // iconPath (optional) is the path to the icon for the item. + iconPath?: string + // timeRange (optional) is the time range of the item. + timeRange: #AbsoluteTimeRange + // note (optional) is a comment on the item. + note?: [...#Comment] + // queryType is the type of the query used to generate this item. + queryType: "logs" | "metrics" + // dataQuery contains the query used to generate this item. + dataQuery: #DataQueryLogs | #DataQueryMetrics + } + + // DataQueryLogs is a data query for logs. + #DataQueryLogs: { + // refId is the reference ID of the query. + refId: string + // datasource is the datasource of the query. + datasource: #DatasourceRef + // expr is the expression of the query. + expr: string + // maxLines (optional) is used to limit the number of log rows returned. + maxLines?: int64 + } + + // DataQueryMetrics is a data query for metrics. + #DataQueryMetrics: { + refId: string + datasource: #DatasourceRef + expr: string + } + + // Comment is a comment on an investigation item. + #Comment: { + authorUserID: string + bodyMarkdown: string + } + + // DatasourceRef is a reference to a datasource. + #DatasourceRef: { + uid: string + type: string + apiVersion: string + name: string + } + + // AbsoluteTimeRange is a time range specified by absolute timestamps. + #AbsoluteTimeRange: { + from: number + to: number + } + // spec is the schema of our resource. The spec should include all the user-ediable information for the kind. spec: #InvestigationSpec } } } } - -#InvestigationSpec: { - title: string - status: "open" | "closed" - items: [...#InvestigationItem] -} - -// InvestigationItem is an item in an investigation. -#InvestigationItem: { - id: string - title: string - // type is the type of the item "timeseries", "heatmap", "log-table" (not an enum to allow for future extensions). - type: string - // url is the URL to the item. - url: string - // origin is where the item was created from. - origin: string // "explore-metrics", "explore-logs", "explore-traces" (not an enum to allow for future extensions) - // iconPath (optional) is the path to the icon for the item. - iconPath?: string - // timeRange (optional) is the time range of the item. - timeRange: #AbsoluteTimeRange - // note (optional) is a comment on the item. - note?: [...#Comment] - // queryType is the type of the query used to generate this item. - queryType: "logs" | "metrics" - // dataQuery contains the query used to generate this item. - dataQuery: #DataQueryLogs | #DataQueryMetrics -} - -// DataQueryLogs is a data query for logs. -#DataQueryLogs: { - // refId is the reference ID of the query. - refId: string - // datasource is the datasource of the query. - datasource: #DatasourceRef - // expr is the expression of the query. - expr: string - // maxLines (optional) is used to limit the number of log rows returned. - maxLines?: int64 -} - -// DataQueryMetrics is a data query for metrics. -#DataQueryMetrics: { - refId: string - datasource: #DatasourceRef - expr: string -} - -// Comment is a comment on an investigation item. -#Comment: { - authorUserID: string - bodyMarkdown: string -} - -// DatasourceRef is a reference to a datasource. -#DatasourceRef: { - uid: string - type: string - apiVersion: string - name: string -} - -// AbsoluteTimeRange is a time range specified by absolute timestamps. -#AbsoluteTimeRange: { - from: number - to: number -} diff --git a/apps/investigation/kinds/manifest.cue b/apps/investigation/kinds/manifest.cue new file mode 100644 index 00000000000..0bd233016c6 --- /dev/null +++ b/apps/investigation/kinds/manifest.cue @@ -0,0 +1,9 @@ +package investigation + +manifest: { + appName: "investigation" + groupOverride: "investigation.grafana.app" + kinds: [ + investigation, + ] +} diff --git a/apps/investigation/pkg/apis/investigation/v1alpha1/constants.go b/apps/investigation/pkg/apis/investigation/v1alpha1/constants.go new file mode 100644 index 00000000000..b6be754fa23 --- /dev/null +++ b/apps/investigation/pkg/apis/investigation/v1alpha1/constants.go @@ -0,0 +1,18 @@ +package v1alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // Group is the API group used by all kinds in this package + Group = "investigation.grafana.app" + // Version is the API version used by all kinds in this package + Version = "v1alpha1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: Group, + Version: Version, + } +) diff --git a/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_metadata_gen.go b/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_metadata_gen.go index 8a8e44d35ac..5a80281d304 100644 --- a/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_metadata_gen.go +++ b/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_metadata_gen.go @@ -1,32 +1,28 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + package v1alpha1 import ( - "time" + time "time" ) -// InvestigationMetadata defines model for InvestigationMetadata. +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. type InvestigationMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` CreationTimestamp time.Time `json:"creationTimestamp"` DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` Finalizers []string `json:"finalizers"` - Generation int64 `json:"generation"` - Labels map[string]string `json:"labels"` ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` - UpdateTimestamp time.Time `json:"updateTimestamp"` + Generation int64 `json:"generation"` UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` } -// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -// It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -// As it is also intended to be generic enough to function with any API Server. -type InvestigationKubeObjectMetadata struct { - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - Finalizers []string `json:"finalizers"` - Generation int64 `json:"generation"` - Labels map[string]string `json:"labels"` - ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` +// NewInvestigationMetadata creates a new InvestigationMetadata object. +func NewInvestigationMetadata() *InvestigationMetadata { + return &InvestigationMetadata{} } diff --git a/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_object_gen.go b/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_object_gen.go index c97fc75eec1..9d0dcf2cce7 100644 --- a/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_object_gen.go +++ b/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_object_gen.go @@ -16,10 +16,10 @@ import ( // +k8s:openapi-gen=true type Investigation struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata"` - Spec InvestigationSpec `json:"spec"` - InvestigationStatus InvestigationStatus `json:"status"` + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + Spec InvestigationSpec `json:"spec" yaml:"spec"` + InvestigationStatus InvestigationStatus `json:"status" yaml:"status"` } func (o *Investigation) GetSpec() any { @@ -224,9 +224,9 @@ var _ resource.Object = &Investigation{} // +k8s:openapi-gen=true type InvestigationList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata"` - Items []Investigation `json:"items"` + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Investigation `json:"items" yaml:"items"` } func (o *InvestigationList) DeepCopyObject() runtime.Object { diff --git a/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_spec_gen.go b/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_spec_gen.go index 1ed1173238b..c0b5fbd38e0 100644 --- a/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_spec_gen.go +++ b/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_spec_gen.go @@ -1,22 +1,17 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + package v1alpha1 -// Defines values for InvestigationInvestigationItemQueryType. -const ( - InvestigationInvestigationItemQueryTypeLogs InvestigationInvestigationItemQueryType = "logs" - InvestigationInvestigationItemQueryTypeMetrics InvestigationInvestigationItemQueryType = "metrics" -) - -// Defines values for InvestigationSpecStatus. -const ( - InvestigationSpecStatusClosed InvestigationSpecStatus = "closed" - InvestigationSpecStatusOpen InvestigationSpecStatus = "open" -) - // AbsoluteTimeRange is a time range specified by absolute timestamps. // +k8s:openapi-gen=true type InvestigationAbsoluteTimeRange struct { - From float32 `json:"from"` - To float32 `json:"to"` + From float64 `json:"from"` + To float64 `json:"to"` +} + +// NewInvestigationAbsoluteTimeRange creates a new InvestigationAbsoluteTimeRange object. +func NewInvestigationAbsoluteTimeRange() *InvestigationAbsoluteTimeRange { + return &InvestigationAbsoluteTimeRange{} } // Comment is a comment on an investigation item. @@ -26,82 +21,116 @@ type InvestigationComment struct { BodyMarkdown string `json:"bodyMarkdown"` } -// DataQueryLogs is a data query for logs. -// +k8s:openapi-gen=true -type InvestigationDataQueryLogs struct { - // DatasourceRef is a reference to a datasource. - Datasource InvestigationDatasourceRef `json:"datasource"` - - // expr is the expression of the query. - Expr string `json:"expr"` - - // maxLines (optional) is used to limit the number of log rows returned. - MaxLines *int64 `json:"maxLines,omitempty"` - - // refId is the reference ID of the query. - RefId string `json:"refId"` -} - -// DataQueryMetrics is a data query for metrics. -// +k8s:openapi-gen=true -type InvestigationDataQueryMetrics struct { - // DatasourceRef is a reference to a datasource. - Datasource InvestigationDatasourceRef `json:"datasource"` - Expr string `json:"expr"` - RefId string `json:"refId"` +// NewInvestigationComment creates a new InvestigationComment object. +func NewInvestigationComment() *InvestigationComment { + return &InvestigationComment{} } // DatasourceRef is a reference to a datasource. // +k8s:openapi-gen=true type InvestigationDatasourceRef struct { + Uid string `json:"uid"` + Type string `json:"type"` ApiVersion string `json:"apiVersion"` Name string `json:"name"` - Type string `json:"type"` - Uid string `json:"uid"` +} + +// NewInvestigationDatasourceRef creates a new InvestigationDatasourceRef object. +func NewInvestigationDatasourceRef() *InvestigationDatasourceRef { + return &InvestigationDatasourceRef{} +} + +// DataQueryLogs is a data query for logs. +// +k8s:openapi-gen=true +type InvestigationDataQueryLogs struct { + // refId is the reference ID of the query. + RefId string `json:"refId"` + // datasource is the datasource of the query. + Datasource InvestigationDatasourceRef `json:"datasource"` + // expr is the expression of the query. + Expr string `json:"expr"` + // maxLines (optional) is used to limit the number of log rows returned. + MaxLines *int64 `json:"maxLines,omitempty"` +} + +// NewInvestigationDataQueryLogs creates a new InvestigationDataQueryLogs object. +func NewInvestigationDataQueryLogs() *InvestigationDataQueryLogs { + return &InvestigationDataQueryLogs{ + Datasource: *NewInvestigationDatasourceRef(), + } +} + +// DataQueryMetrics is a data query for metrics. +// +k8s:openapi-gen=true +type InvestigationDataQueryMetrics struct { + RefId string `json:"refId"` + Datasource InvestigationDatasourceRef `json:"datasource"` + Expr string `json:"expr"` +} + +// NewInvestigationDataQueryMetrics creates a new InvestigationDataQueryMetrics object. +func NewInvestigationDataQueryMetrics() *InvestigationDataQueryMetrics { + return &InvestigationDataQueryMetrics{ + Datasource: *NewInvestigationDatasourceRef(), + } } // InvestigationItem is an item in an investigation. // +k8s:openapi-gen=true type InvestigationInvestigationItem struct { - // dataQuery contains the query used to generate this item. - DataQuery interface{} `json:"dataQuery"` - - // iconPath (optional) is the path to the icon for the item. - IconPath *string `json:"iconPath,omitempty"` - Id string `json:"id"` - - // note (optional) is a comment on the item. - Note []InvestigationComment `json:"note,omitempty"` - - // origin is where the item was created from. - Origin string `json:"origin"` - - // queryType is the type of the query used to generate this item. - QueryType InvestigationInvestigationItemQueryType `json:"queryType"` - - // AbsoluteTimeRange is a time range specified by absolute timestamps. - TimeRange InvestigationAbsoluteTimeRange `json:"timeRange"` - Title string `json:"title"` - + Id string `json:"id"` + Title string `json:"title"` // type is the type of the item "timeseries", "heatmap", "log-table" (not an enum to allow for future extensions). Type string `json:"type"` - // url is the URL to the item. Url string `json:"url"` + // origin is where the item was created from. + // "explore-metrics", "explore-logs", "explore-traces" (not an enum to allow for future extensions) + Origin string `json:"origin"` + // iconPath (optional) is the path to the icon for the item. + IconPath *string `json:"iconPath,omitempty"` + // timeRange (optional) is the time range of the item. + TimeRange InvestigationAbsoluteTimeRange `json:"timeRange"` + // note (optional) is a comment on the item. + Note []InvestigationComment `json:"note,omitempty"` + // queryType is the type of the query used to generate this item. + QueryType InvestigationInvestigationItemQueryType `json:"queryType"` + // dataQuery contains the query used to generate this item. + DataQuery interface{} `json:"dataQuery"` +} + +// NewInvestigationInvestigationItem creates a new InvestigationInvestigationItem object. +func NewInvestigationInvestigationItem() *InvestigationInvestigationItem { + return &InvestigationInvestigationItem{ + TimeRange: *NewInvestigationAbsoluteTimeRange(), + } +} + +// spec is the schema of our resource. The spec should include all the user-ediable information for the kind. +// +k8s:openapi-gen=true +type InvestigationSpec struct { + Title string `json:"title"` + Status InvestigationSpecStatus `json:"status"` + Items []InvestigationInvestigationItem `json:"items"` +} + +// NewInvestigationSpec creates a new InvestigationSpec object. +func NewInvestigationSpec() *InvestigationSpec { + return &InvestigationSpec{} } -// InvestigationInvestigationItemQueryType queryType is the type of the query used to generate this item. // +k8s:openapi-gen=true type InvestigationInvestigationItemQueryType string -// InvestigationSpec defines model for InvestigationSpec. -// +k8s:openapi-gen=true -type InvestigationSpec struct { - Items []InvestigationInvestigationItem `json:"items"` - Status InvestigationSpecStatus `json:"status"` - Title string `json:"title"` -} +const ( + InvestigationInvestigationItemQueryTypeLogs InvestigationInvestigationItemQueryType = "logs" + InvestigationInvestigationItemQueryTypeMetrics InvestigationInvestigationItemQueryType = "metrics" +) -// InvestigationSpecStatus defines model for InvestigationSpec.Status. // +k8s:openapi-gen=true type InvestigationSpecStatus string + +const ( + InvestigationSpecStatusOpen InvestigationSpecStatus = "open" + InvestigationSpecStatusClosed InvestigationSpecStatus = "closed" +) diff --git a/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_status_gen.go b/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_status_gen.go index 3f0870f9c04..4d6f0588207 100644 --- a/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_status_gen.go +++ b/apps/investigation/pkg/apis/investigation/v1alpha1/investigation_status_gen.go @@ -1,70 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + package v1alpha1 -// Defines values for InvestigationOperatorStateState. -const ( - InvestigationOperatorStateStateFailed InvestigationOperatorStateState = "failed" - InvestigationOperatorStateStateInProgress InvestigationOperatorStateState = "in_progress" - InvestigationOperatorStateStateSuccess InvestigationOperatorStateState = "success" -) - -// Defines values for InvestigationstatusOperatorStateState. -const ( - InvestigationstatusOperatorStateStateFailed InvestigationstatusOperatorStateState = "failed" - InvestigationstatusOperatorStateStateInProgress InvestigationstatusOperatorStateState = "in_progress" - InvestigationstatusOperatorStateStateSuccess InvestigationstatusOperatorStateState = "success" -) - -// InvestigationOperatorState defines model for InvestigationOperatorState. // +k8s:openapi-gen=true -type InvestigationOperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]interface{} `json:"details,omitempty"` - +type InvestigationstatusOperatorState struct { // lastEvaluation is the ResourceVersion last evaluated LastEvaluation string `json:"lastEvaluation"` - // state describes the state of the lastEvaluation. // It is limited to three possible states for machine evaluation. - State InvestigationOperatorStateState `json:"state"` + State InvestigationStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` } -// InvestigationOperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. -// +k8s:openapi-gen=true -type InvestigationOperatorStateState string +// NewInvestigationstatusOperatorState creates a new InvestigationstatusOperatorState object. +func NewInvestigationstatusOperatorState() *InvestigationstatusOperatorState { + return &InvestigationstatusOperatorState{} +} -// InvestigationStatus defines model for InvestigationStatus. // +k8s:openapi-gen=true type InvestigationStatus struct { - // additionalFields is reserved for future use - AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` - // operatorStates is a map of operator ID to operator state evaluations. // Any operator which consumes this kind SHOULD add its state evaluation information to this field. OperatorStates map[string]InvestigationstatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` } -// InvestigationstatusOperatorState defines model for Investigationstatus.#OperatorState. -// +k8s:openapi-gen=true -type InvestigationstatusOperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]interface{} `json:"details,omitempty"` - - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State InvestigationstatusOperatorStateState `json:"state"` +// NewInvestigationStatus creates a new InvestigationStatus object. +func NewInvestigationStatus() *InvestigationStatus { + return &InvestigationStatus{} } -// InvestigationstatusOperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. // +k8s:openapi-gen=true -type InvestigationstatusOperatorStateState string +type InvestigationStatusOperatorStateState string + +const ( + InvestigationStatusOperatorStateStateSuccess InvestigationStatusOperatorStateState = "success" + InvestigationStatusOperatorStateStateInProgress InvestigationStatusOperatorStateState = "in_progress" + InvestigationStatusOperatorStateStateFailed InvestigationStatusOperatorStateState = "failed" +) diff --git a/apps/investigation/pkg/apis/investigation/v1alpha1/zz_openapi_gen.go b/apps/investigation/pkg/apis/investigation/v1alpha1/zz_openapi_gen.go index 356c42c01fc..9c6ae46123b 100644 --- a/apps/investigation/pkg/apis/investigation/v1alpha1/zz_openapi_gen.go +++ b/apps/investigation/pkg/apis/investigation/v1alpha1/zz_openapi_gen.go @@ -20,7 +20,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationDatasourceRef": schema_pkg_apis_investigation_v1alpha1_InvestigationDatasourceRef(ref), "github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationInvestigationItem": schema_pkg_apis_investigation_v1alpha1_InvestigationInvestigationItem(ref), "github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationList": schema_pkg_apis_investigation_v1alpha1_InvestigationList(ref), - "github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationOperatorState": schema_pkg_apis_investigation_v1alpha1_InvestigationOperatorState(ref), "github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationSpec": schema_pkg_apis_investigation_v1alpha1_InvestigationSpec(ref), "github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationStatus": schema_pkg_apis_investigation_v1alpha1_InvestigationStatus(ref), "github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationstatusOperatorState": schema_pkg_apis_investigation_v1alpha1_InvestigationstatusOperatorState(ref), @@ -85,14 +84,14 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationAbsoluteTimeRange(ref c SchemaProps: spec.SchemaProps{ Default: 0, Type: []string{"number"}, - Format: "float", + Format: "double", }, }, "to": { SchemaProps: spec.SchemaProps{ Default: 0, Type: []string{"number"}, - Format: "float", + Format: "double", }, }, }, @@ -137,9 +136,17 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationDataQueryLogs(ref commo Description: "DataQueryLogs is a data query for logs.", Type: []string{"object"}, Properties: map[string]spec.Schema{ + "refId": { + SchemaProps: spec.SchemaProps{ + Description: "refId is the reference ID of the query.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "datasource": { SchemaProps: spec.SchemaProps{ - Description: "DatasourceRef is a reference to a datasource.", + Description: "datasource is the datasource of the query.", Default: map[string]interface{}{}, Ref: ref("github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationDatasourceRef"), }, @@ -159,16 +166,8 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationDataQueryLogs(ref commo Format: "int64", }, }, - "refId": { - SchemaProps: spec.SchemaProps{ - Description: "refId is the reference ID of the query.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, }, - Required: []string{"datasource", "expr", "refId"}, + Required: []string{"refId", "datasource", "expr"}, }, }, Dependencies: []string{ @@ -183,11 +182,17 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationDataQueryMetrics(ref co Description: "DataQueryMetrics is a data query for metrics.", Type: []string{"object"}, Properties: map[string]spec.Schema{ + "refId": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "datasource": { SchemaProps: spec.SchemaProps{ - Description: "DatasourceRef is a reference to a datasource.", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationDatasourceRef"), + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationDatasourceRef"), }, }, "expr": { @@ -197,15 +202,8 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationDataQueryMetrics(ref co Format: "", }, }, - "refId": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, }, - Required: []string{"datasource", "expr", "refId"}, + Required: []string{"refId", "datasource", "expr"}, }, }, Dependencies: []string{ @@ -220,6 +218,20 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationDatasourceRef(ref commo Description: "DatasourceRef is a reference to a datasource.", Type: []string{"object"}, Properties: map[string]spec.Schema{ + "uid": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "apiVersion": { SchemaProps: spec.SchemaProps{ Default: "", @@ -234,22 +246,8 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationDatasourceRef(ref commo Format: "", }, }, - "type": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "uid": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, }, - Required: []string{"apiVersion", "name", "type", "uid"}, + Required: []string{"uid", "type", "apiVersion", "name"}, }, }, } @@ -262,20 +260,6 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationInvestigationItem(ref c Description: "InvestigationItem is an item in an investigation.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "dataQuery": { - SchemaProps: spec.SchemaProps{ - Description: "dataQuery contains the query used to generate this item.", - Type: []string{"object"}, - Format: "", - }, - }, - "iconPath": { - SchemaProps: spec.SchemaProps{ - Description: "iconPath (optional) is the path to the icon for the item.", - Type: []string{"string"}, - Format: "", - }, - }, "id": { SchemaProps: spec.SchemaProps{ Default: "", @@ -283,43 +267,6 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationInvestigationItem(ref c Format: "", }, }, - "note": { - SchemaProps: spec.SchemaProps{ - Description: "note (optional) is a comment on the item.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationComment"), - }, - }, - }, - }, - }, - "origin": { - SchemaProps: spec.SchemaProps{ - Description: "origin is where the item was created from.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "queryType": { - SchemaProps: spec.SchemaProps{ - Description: "queryType is the type of the query used to generate this item.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "timeRange": { - SchemaProps: spec.SchemaProps{ - Description: "AbsoluteTimeRange is a time range specified by absolute timestamps.", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationAbsoluteTimeRange"), - }, - }, "title": { SchemaProps: spec.SchemaProps{ Default: "", @@ -343,8 +290,59 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationInvestigationItem(ref c Format: "", }, }, + "origin": { + SchemaProps: spec.SchemaProps{ + Description: "origin is where the item was created from. \"explore-metrics\", \"explore-logs\", \"explore-traces\" (not an enum to allow for future extensions)", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "iconPath": { + SchemaProps: spec.SchemaProps{ + Description: "iconPath (optional) is the path to the icon for the item.", + Type: []string{"string"}, + Format: "", + }, + }, + "timeRange": { + SchemaProps: spec.SchemaProps{ + Description: "timeRange (optional) is the time range of the item.", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationAbsoluteTimeRange"), + }, + }, + "note": { + SchemaProps: spec.SchemaProps{ + Description: "note (optional) is a comment on the item.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/investigation/pkg/apis/investigation/v1alpha1.InvestigationComment"), + }, + }, + }, + }, + }, + "queryType": { + SchemaProps: spec.SchemaProps{ + Description: "queryType is the type of the query used to generate this item.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "dataQuery": { + SchemaProps: spec.SchemaProps{ + Description: "dataQuery contains the query used to generate this item.", + Type: []string{"object"}, + Format: "", + }, + }, }, - Required: []string{"dataQuery", "id", "origin", "queryType", "timeRange", "title", "type", "url"}, + Required: []string{"id", "title", "type", "url", "origin", "timeRange", "queryType", "dataQuery"}, }, }, Dependencies: []string{ @@ -400,65 +398,27 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationList(ref common.Referen } } -func schema_pkg_apis_investigation_v1alpha1_InvestigationOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "InvestigationOperatorState defines model for InvestigationOperatorState.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "descriptiveState": { - SchemaProps: spec.SchemaProps{ - Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", - Type: []string{"string"}, - Format: "", - }, - }, - "details": { - SchemaProps: spec.SchemaProps{ - Description: "details contains any extra information that is operator-specific", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - "lastEvaluation": { - SchemaProps: spec.SchemaProps{ - Description: "lastEvaluation is the ResourceVersion last evaluated", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "state": { - SchemaProps: spec.SchemaProps{ - Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"lastEvaluation", "state"}, - }, - }, - } -} - func schema_pkg_apis_investigation_v1alpha1_InvestigationSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "InvestigationSpec defines model for InvestigationSpec.", + Description: "spec is the schema of our resource. The spec should include all the user-ediable information for the kind.", Type: []string{"object"}, Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "items": { SchemaProps: spec.SchemaProps{ Type: []string{"array"}, @@ -472,22 +432,8 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationSpec(ref common.Referen }, }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "title": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, }, - Required: []string{"items", "status", "title"}, + Required: []string{"title", "status", "items"}, }, }, Dependencies: []string{ @@ -499,24 +445,8 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationStatus(ref common.Refer return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "InvestigationStatus defines model for InvestigationStatus.", - Type: []string{"object"}, + Type: []string{"object"}, Properties: map[string]spec.Schema{ - "additionalFields": { - SchemaProps: spec.SchemaProps{ - Description: "additionalFields is reserved for future use", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, "operatorStates": { SchemaProps: spec.SchemaProps{ Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", @@ -532,6 +462,21 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationStatus(ref common.Refer }, }, }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, }, }, }, @@ -544,9 +489,24 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationstatusOperatorState(ref return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "InvestigationstatusOperatorState defines model for Investigationstatus.#OperatorState.", - Type: []string{"object"}, + Type: []string{"object"}, Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "descriptiveState": { SchemaProps: spec.SchemaProps{ Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", @@ -569,22 +529,6 @@ func schema_pkg_apis_investigation_v1alpha1_InvestigationstatusOperatorState(ref }, }, }, - "lastEvaluation": { - SchemaProps: spec.SchemaProps{ - Description: "lastEvaluation is the ResourceVersion last evaluated", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "state": { - SchemaProps: spec.SchemaProps{ - Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, }, Required: []string{"lastEvaluation", "state"}, }, diff --git a/apps/investigation/pkg/apis/manifest.go b/apps/investigation/pkg/apis/investigation_manifest.go similarity index 100% rename from apps/investigation/pkg/apis/manifest.go rename to apps/investigation/pkg/apis/investigation_manifest.go diff --git a/apps/playlist/Makefile b/apps/playlist/Makefile index 926f5a2840f..7a5f1c99365 100644 --- a/apps/playlist/Makefile +++ b/apps/playlist/Makefile @@ -1,8 +1,3 @@ .PHONY: generate generate: - @grafana-app-sdk generate -g ./pkg/apis --grouping=group --postprocess - # HACK: Clean up generated CRD files. - # TODO: The SDK currently omits generating the manifest Go file with `--defencoding=none`, - # which we would normally use here to skip generating the CRD files. - # This needs to be addressed. - @rm -rf definitions + @grafana-app-sdk generate -g ./pkg/apis --grouping=group --postprocess --defencoding=none diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 47cff1de8dd..3be13946b41 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,9 +3,9 @@ module github.com/grafana/grafana/apps/playlist go 1.23.4 require ( - github.com/grafana/grafana-app-sdk v0.30.0 - k8s.io/apimachinery v0.32.0 - k8s.io/client-go v0.32.0 + github.com/grafana/grafana-app-sdk v0.31.0 + k8s.io/apimachinery v0.32.1 + k8s.io/client-go v0.32.1 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f ) @@ -32,7 +32,7 @@ require ( 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 - github.com/grafana/grafana-app-sdk/logging v0.29.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -54,13 +54,13 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.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 v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect @@ -70,14 +70,14 @@ require ( golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.9.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // 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/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect + google.golang.org/protobuf v1.36.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.32.0 // indirect - k8s.io/apiextensions-apiserver v0.32.0 // indirect + k8s.io/api v0.32.1 // indirect + k8s.io/apiextensions-apiserver v0.32.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 81f238b9979..bdb711181f5 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -49,10 +49,10 @@ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/Z github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.30.0 h1:Hqn2pETu2mQ4RpWkZYEQfu01P7xd1Z1Gj+HX/8aB0tw= -github.com/grafana/grafana-app-sdk v0.30.0/go.mod h1:jhfqNIovb+Mes2vdMf9iMCWQkp1GTNtyNuExONtiNuk= -github.com/grafana/grafana-app-sdk/logging v0.29.0 h1:mgbXaAf33aFwqwGVeaX30l8rkeAJH0iACgX5Rn6YkN4= -github.com/grafana/grafana-app-sdk/logging v0.29.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM= +github.com/grafana/grafana-app-sdk v0.31.0 h1:/mFCcx+YqG8cWAi9hePDJQxIdtXDClDIDRgZwHkksFk= +github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs= +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/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -124,22 +124,22 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -187,14 +187,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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= @@ -204,14 +204,14 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= -k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= -k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0= -k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= -k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= +k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= diff --git a/go.mod b/go.mod index d8bde533370..69b092f4f60 100644 --- a/go.mod +++ b/go.mod @@ -78,8 +78,8 @@ require ( github.com/grafana/e2e v0.1.1 // @grafana-app-platform-squad github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e // @grafana/grafana-operator-experience-squad - github.com/grafana/grafana-app-sdk v0.30.0 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-app-sdk/logging v0.29.0 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk v0.31.0 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk/logging v0.30.0 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-aws-sdk v0.31.5 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad @@ -157,12 +157,12 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 // @grafana/grafana-operator-experience-squad go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel v1.33.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel v1.34.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/sdk v1.33.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/trace v1.33.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/sdk v1.34.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/trace v1.34.0 // @grafana/grafana-backend-group go.uber.org/atomic v1.11.0 // @grafana/alerting-backend go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage go.uber.org/zap v1.27.0 // @grafana/identity-access-team @@ -179,15 +179,15 @@ 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.1 // @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.3 // @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 - k8s.io/api v0.32.0 // @grafana/grafana-app-platform-squad - k8s.io/apimachinery v0.32.0 // @grafana/grafana-app-platform-squad - k8s.io/apiserver v0.32.0 // @grafana/grafana-app-platform-squad - k8s.io/client-go v0.32.0 // @grafana/grafana-app-platform-squad - k8s.io/component-base v0.32.0 // @grafana/grafana-app-platform-squad + k8s.io/api v0.32.1 // @grafana/grafana-app-platform-squad + k8s.io/apimachinery v0.32.1 // @grafana/grafana-app-platform-squad + k8s.io/apiserver v0.32.1 // @grafana/grafana-app-platform-squad + k8s.io/client-go v0.32.1 // @grafana/grafana-app-platform-squad + k8s.io/component-base v0.32.1 // @grafana/grafana-app-platform-squad k8s.io/klog/v2 v2.130.1 // @grafana/grafana-app-platform-squad k8s.io/kube-aggregator v0.32.0 // @grafana/grafana-app-platform-squad k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // @grafana/grafana-app-platform-squad @@ -499,8 +499,8 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.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/proto/otlp v1.5.0 // indirect go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -510,16 +510,16 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // 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 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 gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.32.0 // indirect - k8s.io/kms v0.32.0 // indirect + k8s.io/apiextensions-apiserver v0.32.1 // indirect + k8s.io/kms v0.32.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.55.3 // indirect modernc.org/mathutil v1.6.0 // indirect diff --git a/go.sum b/go.sum index 42231876a94..c1825644258 100644 --- a/go.sum +++ b/go.sum @@ -1516,10 +1516,10 @@ github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 h1:jxJJ5z0GxqhWFbQU github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447/go.mod h1:IxsY6mns6Q5sAnWcrptrgUrSglTZJXH/kXr9nbpb/9I= github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e h1:UlEET0InuoFautfaFp8lDrNF7rPHYXuBMrzwWx9XqFY= github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU= -github.com/grafana/grafana-app-sdk v0.30.0 h1:Hqn2pETu2mQ4RpWkZYEQfu01P7xd1Z1Gj+HX/8aB0tw= -github.com/grafana/grafana-app-sdk v0.30.0/go.mod h1:jhfqNIovb+Mes2vdMf9iMCWQkp1GTNtyNuExONtiNuk= -github.com/grafana/grafana-app-sdk/logging v0.29.0 h1:mgbXaAf33aFwqwGVeaX30l8rkeAJH0iACgX5Rn6YkN4= -github.com/grafana/grafana-app-sdk/logging v0.29.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM= +github.com/grafana/grafana-app-sdk v0.31.0 h1:/mFCcx+YqG8cWAi9hePDJQxIdtXDClDIDRgZwHkksFk= +github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs= +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-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX/Gh3FZKBE= 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= @@ -2444,32 +2444,32 @@ go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0/go.mod h1:IohbtCIY5Erb go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -3241,15 +3241,15 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go. google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -3319,8 +3319,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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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= @@ -3375,19 +3375,19 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= k8s.io/api v0.0.0-20190813020757-36bff7324fb7/go.mod h1:3Iy+myeAORNCLgjd/Xu9ebwN7Vh59Bw0vh9jhoX+V58= -k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= -k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= -k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0= -k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= +k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= k8s.io/apimachinery v0.0.0-20190809020650-423f5d784010/go.mod h1:Waf/xTS2FGRrgXCkO5FP3XxTOWh0qLf2QhL1qFZZ/R8= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.0 h1:VJ89ZvQZ8p1sLeiWdRJpRD6oLozNZD2+qVSLi+ft5Qs= -k8s.io/apiserver v0.32.0/go.mod h1:HFh+dM1/BE/Hm4bS4nTXHVfN6Z6tFIZPi649n83b4Ag= -k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= -k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= -k8s.io/component-base v0.32.0 h1:d6cWHZkCiiep41ObYQS6IcgzOUQUNpywm39KVYaUqzU= -k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= +k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= +k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= @@ -3395,8 +3395,8 @@ k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.32.0 h1:jwOfunHIrcdYl5FRcA+uUKKtg6qiqoPCwmS2T3XTYL4= -k8s.io/kms v0.32.0/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM= +k8s.io/kms v0.32.1 h1:TW6cswRI/fawoQRFGWLmEceO37rZXupdoRdmO019jCc= +k8s.io/kms v0.32.1/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM= k8s.io/kube-aggregator v0.32.0 h1:5ZyMW3QwAbmkasQrROcpa5we3et938DQuyUYHeXSPao= k8s.io/kube-aggregator v0.32.0/go.mod h1:6OKivf6Ypx44qu2v1ZUMrxH8kRp/8LKFKeJU72J18lU= k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= diff --git a/go.work.sum b/go.work.sum index c335966892c..0f433090da0 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,6 +1,7 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230802163732-1c33ebd9ecfa.1 h1:tdpHgTbmbvEIARu+bixzmleMi14+3imnpoFXz+Qzjp4= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230802163732-1c33ebd9ecfa.1/go.mod h1:xafc+XIsTxTy76GJQ1TKgvJWsSugFBqMaN27WhUblew= cel.dev/expr v0.16.2/go.mod h1:gXngZQMkWJoSbE8mOzehJlXQyubn/Vg0vR9/F3W7iw8= +cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go v0.110.6/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= @@ -490,6 +491,8 @@ cloud.google.com/go/grafeas v0.2.0 h1:CYjC+xzdPvbV65gi6Dr4YowKcmLo045pm18L0DhdEL cloud.google.com/go/grafeas v0.3.0/go.mod h1:P7hgN24EyONOTMyeJH6DxG4zD7fwiYa5Q6GUgyFSOU8= cloud.google.com/go/grafeas v0.3.4 h1:D4x32R/cHX3MTofKwirz015uEdVk4uAxvZkZCZkOrF4= cloud.google.com/go/grafeas v0.3.4/go.mod h1:A5m316hcG+AulafjAbPKXBO/+I5itU4LOdKO2R/uDIc= +cloud.google.com/go/grafeas v0.3.10 h1:D9uP/DjVHq9ZzCekVd+aNvQEHb3Hkwp8ki9FDnhRRJ0= +cloud.google.com/go/grafeas v0.3.10/go.mod h1:Mz/AoXmxNhj74VW0fz5Idc3kMN2VZMi4UT5+UPx5Pq0= cloud.google.com/go/gsuiteaddons v1.6.1/go.mod h1:CodrdOqRZcLp5WOwejHWYBjZvfY0kOphkAKpF/3qdZY= cloud.google.com/go/gsuiteaddons v1.6.2/go.mod h1:K65m9XSgs8hTF3X9nNTPi8IQueljSdYo9F+Mi+s4MyU= cloud.google.com/go/gsuiteaddons v1.6.3/go.mod h1:sCFJkZoMrLZT3JTb8uJqgKPNshH2tfXeCwTFRebTq48= @@ -1048,6 +1051,8 @@ 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.24.2 h1:cZpsGsWTIFKymTA0je7IIvi1O7Es7apb9CF3EQlOcfE= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2/go.mod h1:itPGVDKf9cC/ov4MdvJ2QZ0khw4bfoo9jzwTJlaxy2k= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= @@ -1227,6 +1232,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c h1:2zRrJWIt/f9c9HhNHAgrRgq0San5gRRUJTBXLkchal0= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coder/quartz v0.1.0 h1:cLL+0g5l7xTf6ordRnUMMiZtRE8Sq5LxpghS63vEXrQ= @@ -1469,6 +1476,7 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/pprof v0.0.0-20240416155748-26353dc0451f/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= @@ -1476,6 +1484,8 @@ github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/cloud-bigtable-clients-test v0.0.2 h1:S+sCHWAiAc+urcEnvg5JYJUOdlQEm/SEzQ/c/IdAH5M= +github.com/googleapis/cloud-bigtable-clients-test v0.0.2/go.mod h1:mk3CrkrouRgtnhID6UZQDK3DrFFa7cYCAJcEmNsHYrY= github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= @@ -1519,6 +1529,7 @@ github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= @@ -1887,6 +1898,7 @@ github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQ github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/pressly/goose/v3 v3.22.1/go.mod h1:xtMpbstWyCpyH+0cxLTMCENWBG+0CSxvTsXhW95d5eo= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= @@ -2042,6 +2054,8 @@ github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= go.einride.tech/aip v0.66.0 h1:XfV+NQX6L7EOYK11yoHHFtndeaWh3KbD9/cN/6iWEt8= go.einride.tech/aip v0.66.0/go.mod h1:qAhMsfT7plxBX+Oy7Huol6YUvZ0ZzdUz26yZsQwfl1M= +go.einride.tech/aip v0.68.0 h1:4seM66oLzTpz50u4K1zlJyOXQ3tCzcJN7I22tKkjipw= +go.einride.tech/aip v0.68.0/go.mod h1:7y9FF8VtPWqpxuAxl0KQWqaULxW4zFIesD6zF5RIHHg= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -2139,6 +2153,7 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/contrib/propagators/b3 v1.27.0 h1:IjgxbomVrV9za6bRi8fWCNXENs0co37SZedQilP2hm0= go.opentelemetry.io/contrib/propagators/b3 v1.27.0/go.mod h1:Dv9obQz25lCisDvvs4dy28UPh974CxkahRDUPsY7y9E= @@ -2161,7 +2176,9 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 h1:aLmmtjRke7LPDQ3lvpFz+kNEH43faFhzW7v8BFIEydg= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0/go.mod h1:TC1pyCt6G9Sjb4bQpShH+P5R53pO6ZuGnHuuln9xMeE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0/go.mod h1:8GlBGcDk8KKi7n+2S4BT/CPZQYH3erLu0/k64r1MYgo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8= go.opentelemetry.io/otel/exporters/prometheus v0.50.0 h1:2Ewsda6hejmbhGFyUvWZjUThC98Cf8Zy6g0zkIimOng= @@ -2184,6 +2201,7 @@ go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzau go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= @@ -2199,7 +2217,9 @@ go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06F go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= @@ -2208,7 +2228,10 @@ go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwE go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -2227,6 +2250,7 @@ golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+ golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20220328175248-053ad81199eb/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= @@ -2238,6 +2262,7 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQz golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= @@ -2260,10 +2285,12 @@ golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= @@ -2330,6 +2357,7 @@ golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= @@ -2366,6 +2394,7 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= @@ -2512,6 +2541,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= @@ -2570,6 +2600,8 @@ k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQI k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= k8s.io/code-generator v0.32.0 h1:s0lNN8VSWny8LBz5t5iy7MCdgwdOhdg7vAGVxvS+VWU= k8s.io/code-generator v0.32.0/go.mod h1:b7Q7KMZkvsYFy72A79QYjiv4aTz3GvW0f1T3UfhFq4s= +k8s.io/code-generator v0.32.1 h1:4lw1kFNDuFYXquTkB7Sl5EwPMUP2yyW9hh6BnFfRZFY= +k8s.io/code-generator v0.32.1/go.mod h1:zaILfm00CVyP/6/pJMJ3zxRepXkxyDfUV5SNG4CjZI4= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6 h1:4s3/R4+OYYYUKptXPhZKjQ04WJ6EhQQVFdjOFvCazDk= k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks= k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index 3cd7e49a216..463fc7f4d61 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -9,12 +9,12 @@ require ( github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 github.com/stretchr/testify v1.10.0 - go.opentelemetry.io/otel v1.33.0 - k8s.io/api v0.32.0 - k8s.io/apimachinery v0.32.0 - k8s.io/apiserver v0.32.0 - k8s.io/client-go v0.32.0 - k8s.io/component-base v0.32.0 + go.opentelemetry.io/otel v1.34.0 + k8s.io/api v0.32.1 + k8s.io/apimachinery v0.32.1 + k8s.io/apiserver v0.32.1 + k8s.io/client-go v0.32.1 + k8s.io/component-base v0.32.1 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f sigs.k8s.io/structured-merge-diff/v4 v4.5.0 @@ -123,11 +123,11 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect @@ -144,10 +144,10 @@ require ( golang.org/x/tools v0.29.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // 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/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect + google.golang.org/protobuf v1.36.3 // 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 5820a5d083d..85def8afafb 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -362,23 +362,23 @@ go.opentelemetry.io/contrib/propagators/jaeger v1.33.0/go.mod h1:ku/EpGk44S5lyVM go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 h1:Co7WDtZosbvNcG4Nqbs3AEVuHNsN6EMc1/1uGKAvyJk= go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0/go.mod h1:IohbtCIY5Erb6wKnDddXOMNlG7GwyZnkrgcqjPmhpaA= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -481,10 +481,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -493,8 +493,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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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= @@ -517,16 +517,16 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= -k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.0 h1:VJ89ZvQZ8p1sLeiWdRJpRD6oLozNZD2+qVSLi+ft5Qs= -k8s.io/apiserver v0.32.0/go.mod h1:HFh+dM1/BE/Hm4bS4nTXHVfN6Z6tFIZPi649n83b4Ag= -k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= -k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= -k8s.io/component-base v0.32.0 h1:d6cWHZkCiiep41ObYQS6IcgzOUQUNpywm39KVYaUqzU= -k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= +k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= +k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index e06d8028bc2..31a84251c2f 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -6,8 +6,8 @@ require ( github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team github.com/stretchr/testify v1.10.0 - k8s.io/apimachinery v0.32.0 - k8s.io/apiserver v0.32.0 + k8s.io/apimachinery v0.32.1 + k8s.io/apiserver v0.32.1 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f ) @@ -33,17 +33,17 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // 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.1 // indirect + google.golang.org/protobuf v1.36.3 // 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 3b84590ea71..643fdf5480b 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -74,16 +74,16 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -147,12 +147,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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= @@ -161,10 +161,10 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.0 h1:VJ89ZvQZ8p1sLeiWdRJpRD6oLozNZD2+qVSLi+ft5Qs= -k8s.io/apiserver v0.32.0/go.mod h1:HFh+dM1/BE/Hm4bS4nTXHVfN6Z6tFIZPi649n83b4Ag= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= +k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 02c3bf03df3..ef79d050e07 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -9,11 +9,11 @@ require ( github.com/prometheus/client_golang v1.20.5 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 - go.opentelemetry.io/otel v1.33.0 - go.opentelemetry.io/otel/trace v1.33.0 - k8s.io/apimachinery v0.32.0 - k8s.io/apiserver v0.32.0 - k8s.io/component-base v0.32.0 + go.opentelemetry.io/otel v1.34.0 + go.opentelemetry.io/otel/trace v1.34.0 + k8s.io/apimachinery v0.32.1 + k8s.io/apiserver v0.32.1 + k8s.io/component-base v0.32.1 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 sigs.k8s.io/structured-merge-diff/v4 v4.5.0 @@ -71,10 +71,10 @@ require ( go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect @@ -86,15 +86,15 @@ require ( golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.29.0 // indirect google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // 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/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect + google.golang.org/protobuf v1.36.3 // 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 - k8s.io/api v0.32.0 // indirect - k8s.io/client-go v0.32.0 // indirect + k8s.io/api v0.32.1 // indirect + k8s.io/client-go v0.32.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index e7e16ff054a..141f6dc717f 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -202,20 +202,20 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEj go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 h1:Jok/dG8kfp+yod29XKYV/blWgYPlMuRUoRHljrXMF5E= go.opentelemetry.io/contrib/propagators/jaeger v1.33.0/go.mod h1:ku/EpGk44S5lyVMbtJRK2KFOnXEehxf6SDnhu1eZmjA= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -298,10 +298,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -310,8 +310,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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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= @@ -331,16 +331,16 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= -k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.0 h1:VJ89ZvQZ8p1sLeiWdRJpRD6oLozNZD2+qVSLi+ft5Qs= -k8s.io/apiserver v0.32.0/go.mod h1:HFh+dM1/BE/Hm4bS4nTXHVfN6Z6tFIZPi649n83b4Ag= -k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= -k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= -k8s.io/component-base v0.32.0 h1:d6cWHZkCiiep41ObYQS6IcgzOUQUNpywm39KVYaUqzU= -k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= +k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= +k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index cd1a1fa9084..00a85c4814e 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -24,9 +24,9 @@ require ( github.com/urfave/cli v1.22.16 // @grafana/grafana-backend-group github.com/urfave/cli/v2 v2.27.1 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect; @grafana/plugins-platform-backend - go.opentelemetry.io/otel v1.33.0 // indirect; @grafana/grafana-backend-group - go.opentelemetry.io/otel/sdk v1.33.0 // indirect; @grafana/grafana-backend-group - go.opentelemetry.io/otel/trace v1.33.0 // indirect; @grafana/grafana-backend-group + go.opentelemetry.io/otel v1.34.0 // indirect; @grafana/grafana-backend-group + go.opentelemetry.io/otel/sdk v1.34.0 // indirect; @grafana/grafana-backend-group + go.opentelemetry.io/otel/trace v1.34.0 // indirect; @grafana/grafana-backend-group golang.org/x/crypto v0.32.0 // indirect; @grafana/grafana-backend-group golang.org/x/mod v0.22.0 // @grafana/grafana-backend-group golang.org/x/net v0.34.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources @@ -36,7 +36,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.1 // indirect; @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.3 // indirect; @grafana/plugins-platform-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend ) @@ -73,12 +73,12 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect golang.org/x/sys v0.29.0 // indirect google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect; @grafana/grafana-backend-group - google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // 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 gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -101,9 +101,9 @@ require ( go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/log v0.4.0 // indirect go.opentelemetry.io/otel/sdk/log v0.4.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect diff --git a/pkg/build/go.sum b/pkg/build/go.sum index b55ad2f19fa..ac9820f6625 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -235,30 +235,30 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 h1:oM0GTNKGlc5qHctWeIGTVyda4iFFalOzMZ3Ehj5rwB4= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88/go.mod h1:JGG8ebaMO5nXOPnvKEl+DiA4MGwFjCbjsxT1WHIEBPY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0 h1:zBPZAISA9NOc5cE8zydqDiS0itvg/P/0Hn9m72a5gvM= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0/go.mod h1:gcj2fFjEsqpV3fXuzAA+0Ze1p2/4MJ4T7d77AmkvueQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= go.opentelemetry.io/otel/log v0.4.0 h1:/vZ+3Utqh18e8TPjuc3ecg284078KWrR8BRz+PQAj3o= go.opentelemetry.io/otel/log v0.4.0/go.mod h1:DhGnQvky7pHy82MIRV43iXh3FlKN8UUKftn0KbLOq6I= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/log v0.4.0 h1:1mMI22L82zLqf6KtkjrRy5BbagOTWdJsqMY/HSqILAA= go.opentelemetry.io/otel/sdk/log v0.4.0/go.mod h1:AYJ9FVF0hNOgAVzUG/ybg/QttnXhUePWAupmCqtdESo= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= @@ -343,10 +343,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -364,8 +364,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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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/codegen/go.mod b/pkg/codegen/go.mod index 062c842d401..46391ed0872 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -6,7 +6,7 @@ require ( cuelang.org/go v0.11.1 github.com/dave/dst v0.27.3 github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d - github.com/grafana/cog v0.0.16 + github.com/grafana/cog v0.0.18 github.com/grafana/cuetsy v0.1.11 github.com/matryer/is v1.4.1 ) diff --git a/pkg/codegen/go.sum b/pkg/codegen/go.sum index d05bc7caf34..1a4c60c5a84 100644 --- a/pkg/codegen/go.sum +++ b/pkg/codegen/go.sum @@ -31,8 +31,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/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= -github.com/grafana/cog v0.0.16 h1:JdaZOSZD7i5dKE0vjC1k8AgcCovOZqfr58zytUMeixY= -github.com/grafana/cog v0.0.16/go.mod h1:jrS9indvWuDs60RHEZpLaAkmZdgyoLKMOEUT0jiB1t0= +github.com/grafana/cog v0.0.18 h1:pEmzo/yhIFZMHM58ua0M9Eb5frJj6CgTrTTUVlY8e2o= +github.com/grafana/cog v0.0.18/go.mod h1:jrS9indvWuDs60RHEZpLaAkmZdgyoLKMOEUT0jiB1t0= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws= github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk= diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index b36f4d6a8d7..8286b7e04a0 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -7,7 +7,7 @@ replace github.com/grafana/grafana/pkg/codegen => ../../codegen require ( cuelang.org/go v0.11.1 github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d - github.com/grafana/cog v0.0.16 + github.com/grafana/cog v0.0.18 github.com/grafana/cuetsy v0.1.11 github.com/grafana/grafana/pkg/codegen v0.0.0-00010101000000-000000000000 ) diff --git a/pkg/plugins/codegen/go.sum b/pkg/plugins/codegen/go.sum index 71f1ac0241d..40d59e1bad8 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -30,8 +30,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/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= -github.com/grafana/cog v0.0.16 h1:JdaZOSZD7i5dKE0vjC1k8AgcCovOZqfr58zytUMeixY= -github.com/grafana/cog v0.0.16/go.mod h1:jrS9indvWuDs60RHEZpLaAkmZdgyoLKMOEUT0jiB1t0= +github.com/grafana/cog v0.0.18 h1:pEmzo/yhIFZMHM58ua0M9Eb5frJj6CgTrTTUVlY8e2o= +github.com/grafana/cog v0.0.18/go.mod h1:jrS9indvWuDs60RHEZpLaAkmZdgyoLKMOEUT0jiB1t0= github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk= github.com/grafana/cuetsy v0.1.11/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 75cafc79999..5f4803c615e 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -10,9 +10,9 @@ require ( github.com/prometheus/common v0.61.0 github.com/prometheus/prometheus v0.301.0 github.com/stretchr/testify v1.10.0 - go.opentelemetry.io/otel v1.33.0 - go.opentelemetry.io/otel/trace v1.33.0 - k8s.io/apimachinery v0.32.0 + go.opentelemetry.io/otel v1.34.0 + go.opentelemetry.io/otel/trace v1.34.0 + k8s.io/apimachinery v0.32.1 ) require ( @@ -98,10 +98,10 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect @@ -114,14 +114,14 @@ require ( golang.org/x/tools v0.29.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.216.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // 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/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect + google.golang.org/protobuf v1.36.3 // 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 - k8s.io/client-go v0.32.0 // indirect + k8s.io/client-go v0.32.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index c7286c6faf4..9b6453c1080 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -301,23 +301,23 @@ go.opentelemetry.io/contrib/propagators/jaeger v1.33.0/go.mod h1:ku/EpGk44S5lyVM go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 h1:Co7WDtZosbvNcG4Nqbs3AEVuHNsN6EMc1/1uGKAvyJk= go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0/go.mod h1:IohbtCIY5Erb6wKnDddXOMNlG7GwyZnkrgcqjPmhpaA= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -386,14 +386,14 @@ gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= google.golang.org/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY= google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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= @@ -407,10 +407,10 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= -k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= diff --git a/pkg/semconv/go.mod b/pkg/semconv/go.mod index 6692d21afed..b79133b4a5b 100644 --- a/pkg/semconv/go.mod +++ b/pkg/semconv/go.mod @@ -2,7 +2,7 @@ module github.com/grafana/grafana/pkg/semconv go 1.23.1 -require go.opentelemetry.io/otel v1.33.0 +require go.opentelemetry.io/otel v1.34.0 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/pkg/semconv/go.sum b/pkg/semconv/go.sum index 93c4818d777..2b997e160c5 100644 --- a/pkg/semconv/go.sum +++ b/pkg/semconv/go.sum @@ -6,7 +6,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI 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= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 32b82f51bdc..48c6c358f39 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -23,9 +23,9 @@ require ( gocloud.dev v0.40.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 google.golang.org/grpc v1.70.0 - k8s.io/apimachinery v0.32.0 - k8s.io/apiserver v0.32.0 - k8s.io/client-go v0.32.0 + k8s.io/apimachinery v0.32.1 + k8s.io/apiserver v0.32.1 + k8s.io/client-go v0.32.1 k8s.io/klog/v2 v2.130.1 ) @@ -174,7 +174,7 @@ require ( github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // 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.29.0 // indirect + 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.263.0 // indirect @@ -317,13 +317,13 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/mock v0.5.0 // indirect @@ -343,9 +343,9 @@ require ( gonum.org/v1/gonum v0.15.1 // indirect google.golang.org/api v0.216.0 // indirect google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/protobuf v1.36.1 // 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.3 // 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 @@ -354,8 +354,8 @@ require ( gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.32.0 // indirect - k8s.io/component-base v0.32.0 // indirect + k8s.io/api v0.32.1 // indirect + k8s.io/component-base v0.32.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index d25df5fb16f..1416f8d41f7 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -559,8 +559,8 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ= -github.com/grafana/grafana-app-sdk/logging v0.29.0 h1:mgbXaAf33aFwqwGVeaX30l8rkeAJH0iACgX5Rn6YkN4= -github.com/grafana/grafana-app-sdk/logging v0.29.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM= +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-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX/Gh3FZKBE= 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= @@ -1088,25 +1088,25 @@ go.opentelemetry.io/contrib/propagators/jaeger v1.33.0/go.mod h1:ku/EpGk44S5lyVM go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 h1:Co7WDtZosbvNcG4Nqbs3AEVuHNsN6EMc1/1uGKAvyJk= go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0/go.mod h1:IohbtCIY5Erb6wKnDddXOMNlG7GwyZnkrgcqjPmhpaA= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -1477,10 +1477,10 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -1512,8 +1512,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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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= @@ -1558,20 +1558,20 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= -k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.0 h1:VJ89ZvQZ8p1sLeiWdRJpRD6oLozNZD2+qVSLi+ft5Qs= -k8s.io/apiserver v0.32.0/go.mod h1:HFh+dM1/BE/Hm4bS4nTXHVfN6Z6tFIZPi649n83b4Ag= -k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= -k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= -k8s.io/component-base v0.32.0 h1:d6cWHZkCiiep41ObYQS6IcgzOUQUNpywm39KVYaUqzU= -k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= +k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= +k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.32.0 h1:jwOfunHIrcdYl5FRcA+uUKKtg6qiqoPCwmS2T3XTYL4= -k8s.io/kms v0.32.0/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM= +k8s.io/kms v0.32.1 h1:TW6cswRI/fawoQRFGWLmEceO37rZXupdoRdmO019jCc= +k8s.io/kms v0.32.1/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM= k8s.io/kube-aggregator v0.32.0 h1:5ZyMW3QwAbmkasQrROcpa5we3et938DQuyUYHeXSPao= k8s.io/kube-aggregator v0.32.0/go.mod h1:6OKivf6Ypx44qu2v1ZUMrxH8kRp/8LKFKeJU72J18lU= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 1f87f8bc70e..a955e2d2476 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -21,13 +21,13 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/prometheus/client_golang v1.20.5 github.com/stretchr/testify v1.10.0 - go.opentelemetry.io/otel v1.33.0 - go.opentelemetry.io/otel/trace v1.33.0 + go.opentelemetry.io/otel v1.34.0 + go.opentelemetry.io/otel/trace v1.34.0 gocloud.dev v0.40.0 golang.org/x/sync v0.10.0 google.golang.org/grpc v1.70.0 - google.golang.org/protobuf v1.36.1 - k8s.io/apimachinery v0.32.0 + google.golang.org/protobuf v1.36.3 + k8s.io/apimachinery v0.32.1 ) require ( @@ -117,7 +117,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect - github.com/grafana/grafana-app-sdk/logging v0.29.0 // indirect + 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/pkg/apiserver v0.0.0-20250121113133-e747350fee2d // indirect @@ -210,10 +210,10 @@ require ( go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/crypto v0.32.0 // indirect @@ -229,16 +229,16 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.216.0 // indirect google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // 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 gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.32.0 // indirect - k8s.io/apiserver v0.32.0 // indirect - k8s.io/component-base v0.32.0 // indirect + k8s.io/api v0.32.1 // indirect + k8s.io/apiserver v0.32.1 // indirect + k8s.io/component-base v0.32.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 0e30c13985c..4e45d508a62 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -413,8 +413,8 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ= -github.com/grafana/grafana-app-sdk/logging v0.29.0 h1:mgbXaAf33aFwqwGVeaX30l8rkeAJH0iACgX5Rn6YkN4= -github.com/grafana/grafana-app-sdk/logging v0.29.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM= +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-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX/Gh3FZKBE= 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= @@ -843,25 +843,25 @@ go.opentelemetry.io/contrib/propagators/jaeger v1.33.0/go.mod h1:ku/EpGk44S5lyVM go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 h1:Co7WDtZosbvNcG4Nqbs3AEVuHNsN6EMc1/1uGKAvyJk= go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0/go.mod h1:IohbtCIY5Erb6wKnDddXOMNlG7GwyZnkrgcqjPmhpaA= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -1058,10 +1058,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= -google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1084,8 +1084,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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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= @@ -1119,14 +1119,14 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= -k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.0 h1:VJ89ZvQZ8p1sLeiWdRJpRD6oLozNZD2+qVSLi+ft5Qs= -k8s.io/apiserver v0.32.0/go.mod h1:HFh+dM1/BE/Hm4bS4nTXHVfN6Z6tFIZPi649n83b4Ag= -k8s.io/component-base v0.32.0 h1:d6cWHZkCiiep41ObYQS6IcgzOUQUNpywm39KVYaUqzU= -k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= +k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= +k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= +k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= From 3589d9192dca9d29c3c35b53d258bd49a91496e0 Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Thu, 30 Jan 2025 16:00:23 +0100 Subject: [PATCH 237/894] =?UTF-8?q?chore:=20use=20IRM=20plugin=20ID=20inst?= =?UTF-8?q?ead=20of=20OnCall=20/=20Incident=20if=20it's=20present=E2=80=A6?= =?UTF-8?q?=20(#99742)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: use IRM plugin ID instead of OnCall / Incident if it's present in the stack * minor improvements * fix unit tests * Add IRM plugin discovery tests --------- Co-authored-by: Konrad Lalik --- .../alerting/unified/api/incidentsApi.ts | 4 +- .../alerting/unified/api/onCallApi.test.ts | 27 ++++ .../alerting/unified/api/onCallApi.ts | 6 +- .../bridges/DeclareIncidentButton.tsx | 22 +++- .../contact-points/useContactPoints.ts | 4 +- .../onCall/useOnCallIntegration.ts | 6 +- .../receivers/grafanaAppReceivers/types.ts | 2 +- .../useReceiversMetadata.ts | 12 +- .../alerting/unified/testSetup/plugins.ts | 115 +++++++++++++----- .../alerting/unified/types/pluginBridges.ts | 1 + .../alerting/unified/utils/config.test.ts | 55 ++++++++- .../features/alerting/unified/utils/config.ts | 14 +++ .../buildNewDashboardSaveModel.test.ts | 1 + .../DashExportModal/DashboardExporter.test.ts | 1 + .../plugins/components/AppRootPage.test.tsx | 1 + public/img/alerting/irm_logo.svg | 44 +++++++ 16 files changed, 261 insertions(+), 54 deletions(-) create mode 100644 public/app/features/alerting/unified/api/onCallApi.test.ts create mode 100644 public/img/alerting/irm_logo.svg diff --git a/public/app/features/alerting/unified/api/incidentsApi.ts b/public/app/features/alerting/unified/api/incidentsApi.ts index 3a449ca3fdb..737529c18d4 100644 --- a/public/app/features/alerting/unified/api/incidentsApi.ts +++ b/public/app/features/alerting/unified/api/incidentsApi.ts @@ -1,4 +1,4 @@ -import { SupportedPlugin } from '../types/pluginBridges'; +import { getIrmIfPresentOrIncidentPluginId } from '../utils/config'; import { alertingApi } from './alertingApi'; @@ -7,7 +7,7 @@ interface IncidentsPluginConfigDto { isIncidentCreated: boolean; } -const getProxyApiUrl = (path: string) => `/api/plugins/${SupportedPlugin.Incident}/resources${path}`; +const getProxyApiUrl = (path: string) => `/api/plugins/${getIrmIfPresentOrIncidentPluginId()}/resources${path}`; export const incidentsApi = alertingApi.injectEndpoints({ endpoints: (build) => ({ diff --git a/public/app/features/alerting/unified/api/onCallApi.test.ts b/public/app/features/alerting/unified/api/onCallApi.test.ts new file mode 100644 index 00000000000..a403f79b4a2 --- /dev/null +++ b/public/app/features/alerting/unified/api/onCallApi.test.ts @@ -0,0 +1,27 @@ +import { config } from '@grafana/runtime'; + +import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins'; +import { SupportedPlugin } from '../types/pluginBridges'; + +import { getProxyApiUrl } from './onCallApi'; + +describe('getProxyApiUrl', () => { + it('should return URL with IRM plugin ID when IRM plugin is present', () => { + config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }; + + expect(getProxyApiUrl('/alert_receive_channels/')).toBe( + '/api/plugins/grafana-irm-app/resources/alert_receive_channels/' + ); + }); + + it('should return URL with OnCall plugin ID when IRM plugin is not present', () => { + config.apps = { + [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), + [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), + }; + + expect(getProxyApiUrl('/alert_receive_channels/')).toBe( + '/api/plugins/grafana-oncall-app/resources/alert_receive_channels/' + ); + }); +}); diff --git a/public/app/features/alerting/unified/api/onCallApi.ts b/public/app/features/alerting/unified/api/onCallApi.ts index fdb08f9f991..0e35ae2f1b6 100644 --- a/public/app/features/alerting/unified/api/onCallApi.ts +++ b/public/app/features/alerting/unified/api/onCallApi.ts @@ -1,7 +1,7 @@ import { FetchError, isFetchError } from '@grafana/runtime'; import { GRAFANA_ONCALL_INTEGRATION_TYPE } from '../components/receivers/grafanaAppReceivers/onCall/onCall'; -import { SupportedPlugin } from '../types/pluginBridges'; +import { getIrmIfPresentOrOnCallPluginId } from '../utils/config'; import { alertingApi } from './alertingApi'; @@ -38,7 +38,9 @@ export interface OnCallConfigChecks { is_integration_chatops_connected: boolean; } -const getProxyApiUrl = (path: string) => `/api/plugins/${SupportedPlugin.OnCall}/resources${path}`; +export function getProxyApiUrl(path: string) { + return `/api/plugins/${getIrmIfPresentOrOnCallPluginId()}/resources${path}`; +} export const onCallApi = alertingApi.injectEndpoints({ endpoints: (build) => ({ diff --git a/public/app/features/alerting/unified/components/bridges/DeclareIncidentButton.tsx b/public/app/features/alerting/unified/components/bridges/DeclareIncidentButton.tsx index 1625cb449f7..b0c21ca66fc 100644 --- a/public/app/features/alerting/unified/components/bridges/DeclareIncidentButton.tsx +++ b/public/app/features/alerting/unified/components/bridges/DeclareIncidentButton.tsx @@ -1,7 +1,7 @@ import { Button, LinkButton, Menu, Tooltip } from '@grafana/ui'; import { usePluginBridge } from '../../hooks/usePluginBridge'; -import { SupportedPlugin } from '../../types/pluginBridges'; +import { getIrmIfPresentOrIncidentPluginId } from '../../utils/config'; import { createBridgeURL } from '../PluginBridge'; interface Props { @@ -10,10 +10,16 @@ interface Props { url?: string; } -export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: Props) => { - const bridgeURL = createBridgeURL(SupportedPlugin.Incident, '/incidents/declare', { title, severity, url }); +const pluginId = getIrmIfPresentOrIncidentPluginId(); - const { loading, installed, settings } = usePluginBridge(SupportedPlugin.Incident); +export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: Props) => { + const bridgeURL = createBridgeURL(pluginId, '/incidents/declare', { + title, + severity, + url, + }); + + const { loading, installed, settings } = usePluginBridge(pluginId); return ( <> @@ -39,9 +45,13 @@ export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: P }; export const DeclareIncidentMenuItem = ({ title = '', severity = '', url = '' }: Props) => { - const bridgeURL = createBridgeURL(SupportedPlugin.Incident, '/incidents/declare', { title, severity, url }); + const bridgeURL = createBridgeURL(pluginId, '/incidents/declare', { + title, + severity, + url, + }); - const { loading, installed, settings } = usePluginBridge(SupportedPlugin.Incident); + const { loading, installed, settings } = usePluginBridge(pluginId); return ( <> diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts b/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts index 314a64aa852..e1e9cc3f8e8 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts @@ -26,7 +26,7 @@ import { useAsync } from '../../hooks/useAsync'; import { usePluginBridge } from '../../hooks/usePluginBridge'; import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig'; import { addReceiverAction, deleteReceiverAction, updateReceiverAction } from '../../reducers/alertmanager/receivers'; -import { SupportedPlugin } from '../../types/pluginBridges'; +import { getIrmIfPresentOrOnCallPluginId } from '../../utils/config'; import { enhanceContactPointsWithMetadata } from './utils'; @@ -67,7 +67,7 @@ const defaultOptions = { * Otherwise, returns no data */ const useOnCallIntegrations = ({ skip }: Skippable = {}) => { - const { installed, loading } = usePluginBridge(SupportedPlugin.OnCall); + const { installed, loading } = usePluginBridge(getIrmIfPresentOrOnCallPluginId()); const oncallIntegrationsResponse = useGrafanaOnCallIntegrationsQuery(undefined, { skip: skip || !installed }); return useMemo(() => { diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.ts b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.ts index 27adfd1f370..1a22aa3de9b 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.ts +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.ts @@ -3,13 +3,13 @@ import { useCallback, useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; import { isFetchError } from '@grafana/runtime'; +import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config'; import { useAppNotification } from '../../../../../../../core/copy/appNotification'; import { Receiver } from '../../../../../../../plugins/datasource/alertmanager/types'; import { NotifierDTO } from '../../../../../../../types'; import { ONCALL_INTEGRATION_V2_FEATURE, onCallApi } from '../../../../api/onCallApi'; import { usePluginBridge } from '../../../../hooks/usePluginBridge'; -import { SupportedPlugin } from '../../../../types/pluginBridges'; import { option } from '../../../../utils/notifier-types'; import { GRAFANA_APP_RECEIVERS_SOURCE_IMAGE } from '../types'; @@ -41,7 +41,7 @@ function useOnCallPluginStatus() { installed: isOnCallEnabled, loading: isPluginBridgeLoading, error: pluginError, - } = usePluginBridge(SupportedPlugin.OnCall); + } = usePluginBridge(getIrmIfPresentOrOnCallPluginId()); const { data: onCallFeatures = [], @@ -240,7 +240,7 @@ export function useOnCallIntegration() { description: isOnCallEnabled ? 'Connect effortlessly to Grafana OnCall' : 'Enable Grafana OnCall plugin to use this integration', - iconUrl: GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[SupportedPlugin.OnCall], + iconUrl: GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[getIrmIfPresentOrOnCallPluginId()], }, extendOnCallNotifierFeatures, extendOnCallReceivers, diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts index d769ca4739e..5ac5aa95219 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts @@ -2,7 +2,7 @@ import { SupportedPlugin } from '../../../types/pluginBridges'; export const GRAFANA_APP_RECEIVERS_SOURCE_IMAGE: Record = { [SupportedPlugin.OnCall]: 'public/img/alerting/oncall_logo.svg', - + [SupportedPlugin.Irm]: 'public/img/alerting/irm_logo.svg', [SupportedPlugin.Incident]: '', [SupportedPlugin.MachineLearning]: '', [SupportedPlugin.Labels]: '', diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts index 463d092fcc9..404e86e24c7 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts @@ -1,6 +1,6 @@ import { GrafanaManagedReceiverConfig } from '../../../../../../plugins/datasource/alertmanager/types'; import { OnCallIntegrationDTO } from '../../../api/onCallApi'; -import { SupportedPlugin } from '../../../types/pluginBridges'; +import { getIrmIfPresentOrOnCallPluginId, getIsIrmPluginPresent } from '../../../utils/config'; import { createBridgeURL } from '../../PluginBridge'; import { GRAFANA_APP_RECEIVERS_SOURCE_IMAGE } from './types'; @@ -13,7 +13,7 @@ export interface ReceiverPluginMetadata { warning?: string; } -const onCallReceiverICon = GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[SupportedPlugin.OnCall]; +const onCallReceiverICon = GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[getIrmIfPresentOrOnCallPluginId()]; const onCallReceiverTitle = 'Grafana OnCall'; export const onCallReceiverMeta: ReceiverPluginMetadata = { @@ -26,6 +26,8 @@ export function getOnCallMetadata( receiver: GrafanaManagedReceiverConfig, hasAlertManagerConfigData = true ): ReceiverPluginMetadata { + const pluginName = getIsIrmPluginPresent() ? 'IRM' : 'OnCall'; + if (!hasAlertManagerConfigData) { return onCallReceiverMeta; } @@ -43,7 +45,7 @@ export function getOnCallMetadata( if (onCallIntegrations == null) { return { ...onCallReceiverMeta, - warning: 'Grafana OnCall is not installed or is disabled', + warning: `Grafana ${pluginName} is not installed or is disabled`, }; } @@ -55,8 +57,8 @@ export function getOnCallMetadata( ...onCallReceiverMeta, description: matchingOnCallIntegration?.display_name, externalUrl: matchingOnCallIntegration - ? createBridgeURL(SupportedPlugin.OnCall, `/integrations/${matchingOnCallIntegration.value}`) + ? createBridgeURL(getIrmIfPresentOrOnCallPluginId(), `/integrations/${matchingOnCallIntegration.value}`) : undefined, - warning: matchingOnCallIntegration ? undefined : 'OnCall Integration no longer exists', + warning: matchingOnCallIntegration ? undefined : `${pluginName} Integration no longer exists`, }; } diff --git a/public/app/features/alerting/unified/testSetup/plugins.ts b/public/app/features/alerting/unified/testSetup/plugins.ts index 93d92565750..dd3fb82ee76 100644 --- a/public/app/features/alerting/unified/testSetup/plugins.ts +++ b/public/app/features/alerting/unified/testSetup/plugins.ts @@ -1,5 +1,5 @@ -import { PluginMeta, PluginType } from '@grafana/data'; -import { setPluginComponentsHook, setPluginExtensionsHook } from '@grafana/runtime'; +import { PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data'; +import { AppPluginConfig, setPluginComponentsHook, setPluginExtensionsHook } from '@grafana/runtime'; import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; import { mockPluginLinkExtension } from '../mocks'; @@ -21,8 +21,8 @@ export function setupPluginsExtensionsHook() { })); } -export const plugins: PluginMeta[] = [ - { +export const pluginMeta = { + [SupportedPlugin.Slo]: { id: SupportedPlugin.Slo, name: 'SLO dashboard', type: PluginType.app, @@ -44,8 +44,28 @@ export const plugins: PluginMeta[] = [ }, module: 'public/plugins/grafana-slo-app/module.js', baseUrl: 'public/plugins/grafana-slo-app', - }, - { + } satisfies PluginMeta, + [SupportedPlugin.Irm]: { + id: SupportedPlugin.Irm, + name: 'Grafana IRM', + type: PluginType.app, + enabled: true, + info: { + author: { name: 'Grafana Labs', url: '' }, + description: 'Grafana IRM', + links: [], + logos: { + small: 'public/plugins/grafana-irm-app/img/logo.svg', + large: 'public/plugins/grafana-irm-app/img/logo.svg', + }, + screenshots: [], + version: 'local-dev', + updated: '2024-04-09', + }, + module: 'public/plugins/grafana-irm-app/module.js', + baseUrl: 'public/plugins/grafana-irm-app', + } satisfies PluginMeta, + [SupportedPlugin.Incident]: { id: SupportedPlugin.Incident, name: 'Incident management', type: PluginType.app, @@ -67,31 +87,8 @@ export const plugins: PluginMeta[] = [ }, module: 'public/plugins/grafana-incident-app/module.js', baseUrl: 'public/plugins/grafana-incident-app', - }, - { - id: 'grafana-asserts-app', - name: 'Asserts', - type: PluginType.app, - enabled: true, - info: { - author: { - name: 'Grafana Labs', - url: '', - }, - description: 'Asserts', - links: [], - logos: { - small: 'public/plugins/grafana-asserts-app/img/logo.svg', - large: 'public/plugins/grafana-asserts-app/img/logo.svg', - }, - screenshots: [], - version: 'local-dev', - updated: '2024-04-09', - }, - module: 'public/plugins/grafana-asserts-app/module.js', - baseUrl: 'public/plugins/grafana-asserts-app', - }, - { + } satisfies PluginMeta, + [SupportedPlugin.OnCall]: { id: SupportedPlugin.OnCall, name: 'OnCall', type: PluginType.app, @@ -113,5 +110,59 @@ export const plugins: PluginMeta[] = [ }, module: 'public/plugins/grafana-oncall-app/module.js', baseUrl: 'public/plugins/grafana-oncall-app', - }, + } satisfies PluginMeta, + ['grafana-asserts-app']: { + id: 'grafana-asserts-app', + name: 'Asserts', + type: PluginType.app, + enabled: true, + info: { + author: { + name: 'Grafana Labs', + url: '', + }, + description: 'Asserts', + links: [], + logos: { + small: 'public/plugins/grafana-asserts-app/img/logo.svg', + large: 'public/plugins/grafana-asserts-app/img/logo.svg', + }, + screenshots: [], + version: 'local-dev', + updated: '2024-04-09', + }, + module: 'public/plugins/grafana-asserts-app/module.js', + baseUrl: 'public/plugins/grafana-asserts-app', + } satisfies PluginMeta, +}; + +export const plugins: PluginMeta[] = [ + pluginMeta[SupportedPlugin.Slo], + pluginMeta[SupportedPlugin.Incident], + pluginMeta[SupportedPlugin.OnCall], + pluginMeta['grafana-asserts-app'], ]; + +export function pluginMetaToPluginConfig(pluginMeta: PluginMeta): AppPluginConfig { + return { + id: pluginMeta.id, + path: pluginMeta.baseUrl, + preload: true, + version: pluginMeta.info.version, + angular: { detected: false, hideDeprecation: false }, + loadingStrategy: PluginLoadingStrategy.script, + dependencies: { + plugins: [], + grafanaVersion: 'local-dev', + extensions: { + exposedComponents: [], + }, + }, + extensions: { + addedLinks: [], + addedComponents: [], + extensionPoints: [], + exposedComponents: [], + }, + }; +} diff --git a/public/app/features/alerting/unified/types/pluginBridges.ts b/public/app/features/alerting/unified/types/pluginBridges.ts index 926a75ff41d..a1d0b9b20cc 100644 --- a/public/app/features/alerting/unified/types/pluginBridges.ts +++ b/public/app/features/alerting/unified/types/pluginBridges.ts @@ -1,6 +1,7 @@ export enum SupportedPlugin { Incident = 'grafana-incident-app', OnCall = 'grafana-oncall-app', + Irm = 'grafana-irm-app', MachineLearning = 'grafana-ml-app', Labels = 'grafana-labels-app', Slo = 'grafana-slo-app', diff --git a/public/app/features/alerting/unified/utils/config.test.ts b/public/app/features/alerting/unified/utils/config.test.ts index cab4615eb7a..34a4734b55e 100644 --- a/public/app/features/alerting/unified/utils/config.test.ts +++ b/public/app/features/alerting/unified/utils/config.test.ts @@ -1,6 +1,14 @@ import { config } from '@grafana/runtime'; -import { checkEvaluationIntervalGlobalLimit } from './config'; +import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins'; +import { SupportedPlugin } from '../types/pluginBridges'; + +import { + checkEvaluationIntervalGlobalLimit, + getIrmIfPresentOrIncidentPluginId, + getIrmIfPresentOrOnCallPluginId, + getIsIrmPluginPresent, +} from './config'; describe('checkEvaluationIntervalGlobalLimit', () => { it('should NOT exceed limit if evaluate every is not valid duration', () => { @@ -51,3 +59,48 @@ describe('checkEvaluationIntervalGlobalLimit', () => { expect(exceedsLimit).toBe(false); }); }); + +describe('getIsIrmPluginPresent', () => { + it('should return true when IRM plugin is present in config.apps', () => { + config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }; + expect(getIsIrmPluginPresent()).toBe(true); + }); + + it('should return false when IRM plugin is not present in config.apps', () => { + config.apps = { + [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), + [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), + }; + expect(getIsIrmPluginPresent()).toBe(false); + }); +}); + +describe('getIrmIfPresentOrIncidentPluginId', () => { + it('should return IRM plugin ID when IRM plugin is present', () => { + config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }; + expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Irm); + }); + + it('should return Incident plugin ID when IRM plugin is not present', () => { + config.apps = { + [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), + [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), + }; + expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Incident); + }); +}); + +describe('getIrmIfPresentOrOnCallPluginId', () => { + it('should return IRM plugin ID when IRM plugin is present', () => { + config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }; + expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.Irm); + }); + + it('should return OnCall plugin ID when IRM plugin is not present', () => { + config.apps = { + [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), + [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), + }; + expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.OnCall); + }); +}); diff --git a/public/app/features/alerting/unified/utils/config.ts b/public/app/features/alerting/unified/utils/config.ts index 2183ebd5ce1..6a85614622a 100644 --- a/public/app/features/alerting/unified/utils/config.ts +++ b/public/app/features/alerting/unified/utils/config.ts @@ -1,6 +1,8 @@ import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { SupportedPlugin } from '../types/pluginBridges'; + import { isValidPrometheusDuration, safeParsePrometheusDuration } from './time'; export function getAllDataSources(): Array> { @@ -26,3 +28,15 @@ export function checkEvaluationIntervalGlobalLimit(alertGroupEvaluateEvery?: str return { globalLimit: evaluateEveryGlobalLimitMs, exceedsLimit }; } + +export function getIsIrmPluginPresent() { + return SupportedPlugin.Irm in config.apps; +} + +export function getIrmIfPresentOrIncidentPluginId() { + return getIsIrmPluginPresent() ? SupportedPlugin.Irm : SupportedPlugin.Incident; +} + +export function getIrmIfPresentOrOnCallPluginId() { + return getIsIrmPluginPresent() ? SupportedPlugin.Irm : SupportedPlugin.OnCall; +} diff --git a/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.test.ts index f425f2a7a5a..27b8e60c68e 100644 --- a/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.test.ts @@ -44,6 +44,7 @@ jest.mock('@grafana/runtime', () => ({ featureToggles: { newDashboardWithFiltersAndGroupBy: false, }, + apps: {}, bootData: { ...jest.requireActual('@grafana/runtime').config.bootData, user: { diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts index 15b958bc505..7c1256fb54f 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts @@ -36,6 +36,7 @@ jest.mock('@grafana/runtime', () => ({ config: { buildInfo: {}, panels: {}, + apps: {}, featureToggles: { newVariables: false, }, diff --git a/public/app/features/plugins/components/AppRootPage.test.tsx b/public/app/features/plugins/components/AppRootPage.test.tsx index d9b3bd9d77d..84736aed8de 100644 --- a/public/app/features/plugins/components/AppRootPage.test.tsx +++ b/public/app/features/plugins/components/AppRootPage.test.tsx @@ -32,6 +32,7 @@ jest.mock('@grafana/runtime', () => ({ featureToggles: { accessControlOnCall: true, }, + apps: {}, theme2: { breakpoints: { values: { diff --git a/public/img/alerting/irm_logo.svg b/public/img/alerting/irm_logo.svg new file mode 100644 index 00000000000..aa300c81f17 --- /dev/null +++ b/public/img/alerting/irm_logo.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 64e9c38b66eda0a14952fcd8127df6fd6d16a104 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:14:38 +0000 Subject: [PATCH 238/894] Tempo: Show consistently named links for external reference types (#99008) * Show consistently named links for external reference types * Update betterer --- .betterer.results | 4 +--- .../TraceView/components/types/trace.ts | 2 +- .../explore/TraceView/createSpanLink.tsx | 19 +++++++++++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.betterer.results b/.betterer.results index bc0dd082f2a..26eff410c4f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5021,9 +5021,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "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()", "4"] ], "public/app/features/explore/extensions/ConfirmNavigationModal.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], diff --git a/public/app/features/explore/TraceView/components/types/trace.ts b/public/app/features/explore/TraceView/components/types/trace.ts index 0c38b7ba584..3c7dc4733e5 100644 --- a/public/app/features/explore/TraceView/components/types/trace.ts +++ b/public/app/features/explore/TraceView/components/types/trace.ts @@ -29,7 +29,7 @@ export type TraceProcess = { }; export type TraceSpanReference = { - refType: 'CHILD_OF' | 'FOLLOWS_FROM'; + refType: 'CHILD_OF' | 'FOLLOWS_FROM' | 'EXTERNAL'; // eslint-disable-next-line no-use-before-define span?: TraceSpan | null | undefined; spanID: string; diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 43024dd7cd7..662876c3b21 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -29,6 +29,7 @@ import { ExploreFieldLinkModel, getFieldLinksForExplore, getVariableUsageInfo } import { SpanLinkDef, SpanLinkFunc, Trace, TraceSpan } from './components'; import { SpanLinkType } from './components/types/links'; +import { TraceSpanReference } from './components/types/trace'; /** * This is a factory for the link creator. It returns the function mainly so it can return undefined in which case @@ -323,11 +324,12 @@ function legacyCreateSpanLinkFactory( } const link = createFocusSpanLink(reference.traceID, reference.spanID); + const title = getReferenceTitle(reference); links!.push({ href: link.href, - title: reference.span ? reference.span.operationName : 'View linked span', - content: , + title, + content: , onClick: link.onClick, field: link.origin, type: SpanLinkType.Traces, @@ -338,11 +340,12 @@ function legacyCreateSpanLinkFactory( if (span.subsidiarilyReferencedBy && createFocusSpanLink) { for (const reference of span.subsidiarilyReferencedBy) { const link = createFocusSpanLink(reference.traceID, reference.spanID); + const title = getReferenceTitle(reference); links!.push({ href: link.href, - title: reference.span ? reference.span.operationName : 'View linked span', - content: , + title, + content: , onClick: link.onClick, field: link.origin, type: SpanLinkType.Traces, @@ -366,6 +369,14 @@ function legacyCreateSpanLinkFactory( }; } +const getReferenceTitle = (reference: TraceSpanReference) => { + let title = reference.span ? reference.span.operationName : 'View linked span'; + if (reference.refType === 'EXTERNAL') { + title = 'View linked span'; + } + return title; +}; + function getQueryForLoki( span: TraceSpan, options: TraceToLogsOptionsV2, From a066659e1122b6e23b1c9002b9b2b9acc254454a Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 30 Jan 2025 16:17:36 +0100 Subject: [PATCH 239/894] Frontend Sandbox: Add to statscollector (#99735) --- .../usagestats/statscollector/service.go | 14 +++++++++++++ .../usagestats/statscollector/service_test.go | 5 +++++ pkg/server/wireexts_oss.go | 3 +++ .../pluginsintegration/sandbox/sandbox.go | 21 +++++++++++++++++++ .../sandbox/sandbox_test.go | 19 +++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 pkg/services/pluginsintegration/sandbox/sandbox.go create mode 100644 pkg/services/pluginsintegration/sandbox/sandbox_test.go diff --git a/pkg/infra/usagestats/statscollector/service.go b/pkg/infra/usagestats/statscollector/service.go index 20b0d186f98..cb30bd7f030 100644 --- a/pkg/infra/usagestats/statscollector/service.go +++ b/pkg/infra/usagestats/statscollector/service.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" + "github.com/grafana/grafana/pkg/services/pluginsintegration/sandbox" "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/setting" ) @@ -39,6 +40,7 @@ type Service struct { features *featuremgmt.FeatureManager datasources datasources.DataSourceService httpClientProvider httpclient.Provider + sandbox sandbox.Sandbox log log.Logger @@ -58,6 +60,7 @@ func ProvideService( features *featuremgmt.FeatureManager, datasourceService datasources.DataSourceService, httpClientProvider httpclient.Provider, + sandbox sandbox.Sandbox, ) *Service { s := &Service{ cfg: cfg, @@ -69,6 +72,7 @@ func ProvideService( features: features, datasources: datasourceService, httpClientProvider: httpClientProvider, + sandbox: sandbox, startTime: time.Now(), log: log.New("infra.usagestats.collector"), @@ -146,6 +150,7 @@ func (s *Service) collectSystemStats(ctx context.Context) (map[string]any, error m["stats.plugins.apps.count"] = s.appCount(ctx) m["stats.plugins.panels.count"] = s.panelCount(ctx) m["stats.plugins.datasources.count"] = s.dataSourceCount(ctx) + m["stats.plugins.sandboxed_plugins.count"] = s.sandboxCount() m["stats.alerts.count"] = statsResult.Alerts m["stats.active_users.count"] = statsResult.ActiveUsers m["stats.active_admins.count"] = statsResult.ActiveAdmins @@ -361,3 +366,12 @@ func (s *Service) panelCount(ctx context.Context) int { func (s *Service) dataSourceCount(ctx context.Context) int { return len(s.plugins.Plugins(ctx, plugins.TypeDataSource)) } + +func (s *Service) sandboxCount() int { + ps, err := s.sandbox.Plugins() + if err != nil { + s.log.Error("Failed to get sandboxed plugin count", "error", err) + return 0 + } + return len(ps) +} diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index 0d5f955ce47..f8c4f2a5f7f 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" + "github.com/grafana/grafana/pkg/services/pluginsintegration/sandbox" "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/stats/statstest" "github.com/grafana/grafana/pkg/setting" @@ -148,6 +149,7 @@ func TestCollectingUsageStats(t *testing.T) { RemoteCacheOptions: &setting.RemoteCacheSettings{ Name: "database", }, + EnableFrontendSandboxForPlugins: []string{"grafana-worldmap-panel"}, }, sqlStore, statsService, withDatasources(mockDatasourceService{datasources: expectedDataSources})) @@ -178,6 +180,8 @@ func TestCollectingUsageStats(t *testing.T) { assert.EqualValues(t, 3, metrics["stats.correlations.count"]) assert.InDelta(t, int64(65), metrics["stats.uptime"], 6) + + assert.EqualValues(t, 1, metrics["stats.plugins.sandboxed_plugins.count"]) } func TestDatasourceStats(t *testing.T) { @@ -382,6 +386,7 @@ func createService(t testing.TB, cfg *setting.Cfg, store db.DB, statsService sta featuremgmt.WithManager("feature1", "feature2"), o.datasources, httpclient.NewProvider(sdkhttpclient.ProviderOptions{Middlewares: []sdkhttpclient.Middleware{}}), + sandbox.ProvideService(cfg), ) } diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index d8990795049..2ace4123b30 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -39,6 +39,7 @@ import ( "github.com/grafana/grafana/pkg/services/login/authinfoimpl" "github.com/grafana/grafana/pkg/services/pluginsintegration" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" + "github.com/grafana/grafana/pkg/services/pluginsintegration/sandbox" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/publicdashboards" publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api" @@ -113,6 +114,8 @@ var wireExtsBasicSet = wire.NewSet( search2.ProvideDashboardStats, wire.Bind(new(search2.DashboardStats), new(*search2.OssDashboardStats)), search2.ProvideDocumentBuilders, + sandbox.ProvideService, + wire.Bind(new(sandbox.Sandbox), new(*sandbox.Service)), ) var wireExtsSet = wire.NewSet( diff --git a/pkg/services/pluginsintegration/sandbox/sandbox.go b/pkg/services/pluginsintegration/sandbox/sandbox.go new file mode 100644 index 00000000000..4ec5ac27c69 --- /dev/null +++ b/pkg/services/pluginsintegration/sandbox/sandbox.go @@ -0,0 +1,21 @@ +package sandbox + +import "github.com/grafana/grafana/pkg/setting" + +type Sandbox interface { + Plugins() ([]string, error) +} + +type Service struct { + cfg *setting.Cfg +} + +func ProvideService(cfg *setting.Cfg) *Service { + return &Service{ + cfg: cfg, + } +} + +func (s *Service) Plugins() ([]string, error) { + return s.cfg.EnableFrontendSandboxForPlugins, nil +} diff --git a/pkg/services/pluginsintegration/sandbox/sandbox_test.go b/pkg/services/pluginsintegration/sandbox/sandbox_test.go new file mode 100644 index 00000000000..b318ae19978 --- /dev/null +++ b/pkg/services/pluginsintegration/sandbox/sandbox_test.go @@ -0,0 +1,19 @@ +package sandbox + +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" +) + +func TestService_Plugins(t *testing.T) { + cfg := &setting.Cfg{ + EnableFrontendSandboxForPlugins: []string{"plugin1", "plugin2"}, + } + service := ProvideService(cfg) + + plugins, err := service.Plugins() + assert.NoError(t, err) + assert.Equal(t, []string{"plugin1", "plugin2"}, plugins) +} From 9189feeaf5cc95a3255284fd599897046fbee527 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:46:12 +0000 Subject: [PATCH 240/894] Update scenes to v5.41.1 (#99811) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- yarn.lock | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 8cc4da3dbc7..331415bd2dd 100644 --- a/package.json +++ b/package.json @@ -273,8 +273,8 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "5.41.0", - "@grafana/scenes-react": "5.41.0", + "@grafana/scenes": "5.41.1", + "@grafana/scenes-react": "5.41.1", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/yarn.lock b/yarn.lock index d015ea5b6e8..a9d05122c22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3799,11 +3799,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:5.41.0": - version: 5.41.0 - resolution: "@grafana/scenes-react@npm:5.41.0" +"@grafana/scenes-react@npm:5.41.1": + version: 5.41.1 + resolution: "@grafana/scenes-react@npm:5.41.1" dependencies: - "@grafana/scenes": "npm:5.41.0" + "@grafana/scenes": "npm:5.41.1" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3814,13 +3814,13 @@ __metadata: "@grafana/ui": ^11.0.0 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/80d5b51a190ed962e17468c0646e9b48a52133c3af026f8bc599c5e8e1150bcd0210f7973dd13e28f96a300798a1c2a1ec41c1bcedc580d8da393591d1e69eb4 + checksum: 10/f196e5eba9cf1b2912f4292ca08a8abdeeac289fc851e2e88e15ecd48e02d059ac483d92e4f742a2e004f3c9834739c2d69e2f695a93518eddc4af093bddbf46 languageName: node linkType: hard -"@grafana/scenes@npm:5.41.0": - version: 5.41.0 - resolution: "@grafana/scenes@npm:5.41.0" +"@grafana/scenes@npm:5.41.1": + version: 5.41.1 + resolution: "@grafana/scenes@npm:5.41.1" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3837,7 +3837,7 @@ __metadata: "@grafana/ui": ">=10.4" react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/a77b57688b5ac518fa1f3842a871ad21e2daf840d670abda6b69facce74bb6761195c43f25507a3dff87c8551625a979c4240debe62832834473b67145b5d589 + checksum: 10/675fa3253924b313f66dafcae305e010f75b7d1191062bb8ba91080fabeca411ee546f24ba9dc4e9fa05abba6d4367a27c186b3467ce87fcd0b2bbaf5b2e6f7d languageName: node linkType: hard @@ -17794,8 +17794,8 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:5.41.0" - "@grafana/scenes-react": "npm:5.41.0" + "@grafana/scenes": "npm:5.41.1" + "@grafana/scenes-react": "npm:5.41.1" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0" From f92945ac35e84d7265b2464ef0c244f0c5f09181 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:49:28 +0100 Subject: [PATCH 241/894] Update @grafana/plugin-ui to v0.10.0 (treeshake-able version) (#99809) * Update @grafana/plugin-ui to v0.10.0 (treeshake-able version) * Update to 0.10.1 --- package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-sql/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/mssql/package.json | 2 +- .../app/plugins/datasource/mysql/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 78 +++++++++++++++---- 12 files changed, 74 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 331415bd2dd..0cb29eae330 100644 --- a/package.json +++ b/package.json @@ -269,7 +269,7 @@ "@grafana/lezer-logql": "0.2.7", "@grafana/monaco-logql": "^0.0.8", "@grafana/o11y-ds-frontend": "workspace:*", - "@grafana/plugin-ui": "0.9.6", + "@grafana/plugin-ui": "0.10.1", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 30d1371c9c4..9cb9025d02f 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -20,7 +20,7 @@ "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", "@grafana/e2e-selectors": "11.5.0-pre", - "@grafana/plugin-ui": "0.9.6", + "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 9df8ba063ed..48d778736d4 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -41,7 +41,7 @@ "@grafana/data": "11.5.0-pre", "@grafana/faro-web-sdk": "1.12.3", "@grafana/llm": "0.12.0", - "@grafana/plugin-ui": "0.9.6", + "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 87c21909f24..e868769944f 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -17,7 +17,7 @@ "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", "@grafana/e2e-selectors": "11.5.0-pre", - "@grafana/plugin-ui": "0.9.6", + "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", "@react-awesome-query-builder/ui": "6.6.4", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index dab73a2bc56..7d55c792de8 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", - "@grafana/plugin-ui": "0.9.6", + "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 8d6bbae331a..56fa6c5d118 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -7,7 +7,7 @@ "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", "@grafana/google-sdk": "0.1.2", - "@grafana/plugin-ui": "0.9.6", + "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index 2f85083d376..575d6fa8e5f 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", - "@grafana/plugin-ui": "0.9.6", + "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "11.5.0-pre", "@grafana/sql": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index b5dd82b92a7..caef3607014 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -8,7 +8,7 @@ "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", "@grafana/o11y-ds-frontend": "workspace:*", - "@grafana/plugin-ui": "0.9.6", + "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "workspace:*", "@grafana/ui": "workspace:*", "lodash": "4.17.21", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index 008012c3d27..37d3945b481 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", - "@grafana/plugin-ui": "0.9.6", + "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "11.5.0-pre", "@grafana/sql": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index 2cbed20190a..15eb6a8607b 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", - "@grafana/plugin-ui": "0.9.6", + "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "11.5.0-pre", "@grafana/sql": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 798087d01f5..e0d71055e2a 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -8,7 +8,7 @@ "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", "@grafana/o11y-ds-frontend": "workspace:*", - "@grafana/plugin-ui": "0.9.6", + "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "workspace:*", "@grafana/ui": "workspace:*", "lodash": "4.17.21", diff --git a/yarn.lock b/yarn.lock index a9d05122c22..b7e5a28f759 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2707,7 +2707,7 @@ __metadata: "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" "@grafana/plugin-configs": "npm:11.5.0-pre" - "@grafana/plugin-ui": "npm:0.9.6" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/schema": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" @@ -2751,7 +2751,7 @@ __metadata: "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" "@grafana/plugin-configs": "npm:11.5.0-pre" - "@grafana/plugin-ui": "npm:0.9.6" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/sql": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" @@ -2863,7 +2863,7 @@ __metadata: "@grafana/e2e-selectors": "workspace:*" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/plugin-configs": "workspace:*" - "@grafana/plugin-ui": "npm:0.9.6" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "workspace:*" "@grafana/ui": "workspace:*" "@testing-library/dom": "npm:10.4.0" @@ -2904,7 +2904,7 @@ __metadata: "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" "@grafana/plugin-configs": "npm:11.5.0-pre" - "@grafana/plugin-ui": "npm:0.9.6" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/sql": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" @@ -2935,7 +2935,7 @@ __metadata: "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" "@grafana/plugin-configs": "npm:11.5.0-pre" - "@grafana/plugin-ui": "npm:0.9.6" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/sql": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" @@ -2999,7 +2999,7 @@ __metadata: "@grafana/e2e-selectors": "npm:11.5.0-pre" "@grafana/google-sdk": "npm:0.1.2" "@grafana/plugin-configs": "npm:11.5.0-pre" - "@grafana/plugin-ui": "npm:0.9.6" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/schema": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" @@ -3105,7 +3105,7 @@ __metadata: "@grafana/e2e-selectors": "workspace:*" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/plugin-configs": "workspace:*" - "@grafana/plugin-ui": "npm:0.9.6" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "workspace:*" "@grafana/ui": "workspace:*" "@testing-library/dom": "npm:10.4.0" @@ -3497,7 +3497,7 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/plugin-ui": "npm:0.9.6" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/schema": "npm:11.5.0-pre" "@grafana/tsconfig": "npm:^2.0.0" @@ -3561,7 +3561,35 @@ __metadata: languageName: node linkType: hard -"@grafana/plugin-ui@npm:0.9.6, @grafana/plugin-ui@npm:^0.9.3": +"@grafana/plugin-ui@npm:0.10.1": + version: 0.10.1 + resolution: "@grafana/plugin-ui@npm:0.10.1" + dependencies: + "@emotion/css": "npm:^11.11.2" + "@hello-pangea/dnd": "npm:^17.0.0" + "@types/prismjs": "npm:^1.26.4" + lodash: "npm:^4.17.21" + prismjs: "npm:^1.29.0" + react-awesome-query-builder: "npm:^5.3.1" + react-calendar: "npm:^4.8.0" + react-popper-tooltip: "npm:^4.4.2" + react-use: "npm:^17.3.1" + react-virtualized-auto-sizer: "npm:^1.0.6" + sql-formatter-plus: "npm:^1.3.6" + uuid: "npm:^11.0.0" + peerDependencies: + "@grafana/data": ^10.4.0 || ^11.0.0 + "@grafana/e2e-selectors": ^10.4.0 || ^11.0.0 + "@grafana/runtime": ^10.4.0 || ^11.0.0 + "@grafana/ui": ^10.4.0 || ^11.0.0 + react: ^18.2.0 + react-dom: ^18.2.0 + rxjs: ^7.8.1 + checksum: 10/9b152c751b90ab414366d1f0ad6687862fcc1d72e4ecaab99f84d7a020ecbb31ce06267f621aaee70944037c3c3f01d9ff63040cd3964a6d957b9aab4ba114f4 + languageName: node + linkType: hard + +"@grafana/plugin-ui@npm:^0.9.3": version: 0.9.6 resolution: "@grafana/plugin-ui@npm:0.9.6" dependencies: @@ -3602,7 +3630,7 @@ __metadata: "@grafana/e2e-selectors": "npm:11.5.0-pre" "@grafana/faro-web-sdk": "npm:1.12.3" "@grafana/llm": "npm:0.12.0" - "@grafana/plugin-ui": "npm:0.9.6" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/schema": "npm:11.5.0-pre" "@grafana/tsconfig": "npm:^2.0.0" @@ -3875,7 +3903,7 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/plugin-ui": "npm:0.9.6" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/tsconfig": "npm:^2.0.0" "@grafana/ui": "npm:11.5.0-pre" @@ -9867,7 +9895,7 @@ __metadata: languageName: node linkType: hard -"@types/prismjs@npm:1.26.5": +"@types/prismjs@npm:1.26.5, @types/prismjs@npm:^1.26.4": version: 1.26.5 resolution: "@types/prismjs@npm:1.26.5" checksum: 10/617099479db9550119d0f84272dc79d64b2cf3e0d7a17167fe740d55fdf0f155697d935409464392d164e62080c2c88d649cf4bc4fdd30a87127337536657277 @@ -17790,7 +17818,7 @@ __metadata: "@grafana/monaco-logql": "npm:^0.0.8" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/plugin-e2e": "npm:1.17.0" - "@grafana/plugin-ui": "npm:0.9.6" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" @@ -25712,6 +25740,26 @@ __metadata: languageName: node linkType: hard +"react-calendar@npm:^4.8.0": + version: 4.8.0 + resolution: "react-calendar@npm:4.8.0" + dependencies: + "@wojtekmaj/date-utils": "npm:^1.1.3" + clsx: "npm:^2.0.0" + get-user-locale: "npm:^2.2.1" + prop-types: "npm:^15.6.0" + warning: "npm:^4.0.0" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/26a58d9bbe9b1bbe1201612628d0f6bdeec0355ed3730013d066b9e87adbbee4b6f3f1c83352978c0e2f317d8d32ac31ac2cbb3c6f83833bef0ba047c5bb6907 + languageName: node + linkType: hard + "react-calendar@npm:^5.1.0": version: 5.1.0 resolution: "react-calendar@npm:5.1.0" @@ -26566,7 +26614,7 @@ __metadata: languageName: node linkType: hard -"react-use@npm:17.6.0, react-use@npm:^17.4.0, react-use@npm:^17.4.2, react-use@npm:^17.5.0": +"react-use@npm:17.6.0, react-use@npm:^17.3.1, react-use@npm:^17.4.0, react-use@npm:^17.4.2, react-use@npm:^17.5.0": version: 17.6.0 resolution: "react-use@npm:17.6.0" dependencies: @@ -30792,7 +30840,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:11.0.5, uuid@npm:^11.0.2": +"uuid@npm:11.0.5, uuid@npm:^11.0.0, uuid@npm:^11.0.2": version: 11.0.5 resolution: "uuid@npm:11.0.5" bin: From 52aeae13424bded8830063b4232099f3a8e80450 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 30 Jan 2025 16:58:18 +0100 Subject: [PATCH 242/894] Prometheus: Implement dispose method (#99782) * implement dispose method * use s instead of i * add debug log --- pkg/promlib/library.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/promlib/library.go b/pkg/promlib/library.go index 111e0a51986..596a6f61c02 100644 --- a/pkg/promlib/library.go +++ b/pkg/promlib/library.go @@ -41,6 +41,14 @@ func NewService(httpClientProvider *sdkhttpclient.Provider, plog log.Logger, ext } } +// Dispose here tells plugin SDK that plugin wants to clean up resources when a new instance +// created. As soon as datasource settings change detected by SDK old datasource instance will +// be disposed and a new one will be created using NewSampleDatasource factory function. +func (s *Service) Dispose() { + // Clean up datasource instance resources. + s.logger.Debug("Disposing the instance...") +} + func newInstanceSettings(httpClientProvider *sdkhttpclient.Provider, log log.Logger, extendOptions ExtendOptions) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { // Creates a http roundTripper. From 3d19a778ba1205d7588d48ef7919f2877146db9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Irene=20Rodr=C3=ADguez?= Date: Thu, 30 Jan 2025 17:26:34 +0100 Subject: [PATCH 243/894] Remove old admonition (#99821) --- docs/sources/setup-grafana/image-rendering/_index.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/sources/setup-grafana/image-rendering/_index.md b/docs/sources/setup-grafana/image-rendering/_index.md index 639f00b6f79..1daecc33d2f 100644 --- a/docs/sources/setup-grafana/image-rendering/_index.md +++ b/docs/sources/setup-grafana/image-rendering/_index.md @@ -20,8 +20,6 @@ weight: 1000 Grafana supports automatic rendering of panels as PNG images. This allows Grafana to automatically generate images of your panels to include in alert notifications, [PDF export]({{< relref "../../dashboards/create-reports#export-dashboard-as-pdf" >}}), and [Reporting]({{< relref "../../dashboards/create-reports" >}}). PDF Export and Reporting are available only in [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise" >}}) and [Grafana Cloud](/docs/grafana-cloud/). -> **Note:** Image rendering of dashboards is not supported at this time. - While an image is being rendered, the PNG image is temporarily written to the file system. When the image is rendered, the PNG image is temporarily written to the `png` folder in the Grafana `data` folder. A background job runs every 10 minutes and removes temporary images. You can configure how long an image should be stored before being removed by configuring the [temp_data_lifetime]({{< relref "../configure-grafana#temp_data_lifetime" >}}) setting. From a95005eab5f9616a05abd72bf378e288f44e1a7e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 30 Jan 2025 17:42:48 +0100 Subject: [PATCH 244/894] Zanzana: Disable broken OpenFGA health check (#99818) * Zanzana: Disable broken OpenFGA health check * simplify return Co-authored-by: Gabriel MABILLE --------- Co-authored-by: Gabriel MABILLE --- pkg/services/authz/zanzana/server/server.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/services/authz/zanzana/server/server.go b/pkg/services/authz/zanzana/server/server.go index 6826b2b936f..891d13cb74b 100644 --- a/pkg/services/authz/zanzana/server/server.go +++ b/pkg/services/authz/zanzana/server/server.go @@ -10,6 +10,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel" + "google.golang.org/protobuf/types/known/wrapperspb" dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1" "github.com/grafana/grafana/pkg/infra/localcache" @@ -69,7 +70,12 @@ func NewServer(cfg setting.ZanzanaServerSettings, openfga OpenFGAServer, logger } func (s *Server) IsHealthy(ctx context.Context) (bool, error) { - return s.openfga.IsReady(ctx) + // FIXME: get back to openfga.IsReady() when issue is fixed + // https://github.com/openfga/openfga/issues/2251 + _, err := s.openfga.ListStores(ctx, &openfgav1.ListStoresRequest{ + PageSize: wrapperspb.Int32(1), + }) + return err == nil, nil } func (s *Server) getContextuals(ctx context.Context, subject string) (*openfgav1.ContextualTupleKeys, error) { From 415628a2c6d8330c7cbe5c03d136eabd349a4091 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 17:24:02 +0000 Subject: [PATCH 245/894] Update `make docs` procedure (#99789) Co-authored-by: grafanabot Co-authored-by: Jack Baldry --- docs/make-docs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/make-docs b/docs/make-docs index ba448ce0727..e8111479d7c 100755 --- a/docs/make-docs +++ b/docs/make-docs @@ -1,4 +1,6 @@ #!/bin/sh +# shellcheck disable=SC2034 +# # The source of this file is https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/make-docs. # # `make-docs` procedure changelog # @@ -6,6 +8,12 @@ # [Semantic versioning](https://semver.org/) is used to help the reader identify the significance of changes. # Changes are relevant to this script and the support docs.mk GNU Make interface. # +# ## 8.4.0 (2025-01-27) +# +# ### Fixed +# +# - Correct mount for the /docs/grafana-cloud/send-data/fleet-management/ project. +# # ## 8.3.0 (2024-12-27) # # ### Added @@ -304,6 +312,7 @@ PODMAN="$(if command -v podman >/dev/null 2>&1; then echo podman; else echo dock if ! command -v curl >/dev/null 2>&1; then if ! command -v wget >/dev/null 2>&1; then + # shellcheck disable=SC2016 errr 'either `curl` or `wget` must be installed for this script to work.' exit 1 @@ -311,6 +320,7 @@ if ! command -v curl >/dev/null 2>&1; then fi if ! command -v "${PODMAN}" >/dev/null 2>&1; then + # shellcheck disable=SC2016 errr 'either `podman` or `docker` must be installed for this script to work.' exit 1 @@ -357,6 +367,10 @@ EOF exit 1 fi +# The following variables comprise a pseudo associative array of project names to source repositories. +# You only need to set a SOURCES variable if the project name does not match the source repository name. +# You can get a key identifier using the `identifier` function. +# To look up the value of any pseudo associative array, use the `aget` function. SOURCES_as_code='as-code-docs' SOURCES_enterprise_metrics='backend-enterprise' SOURCES_enterprise_metrics_='backend-enterprise' @@ -366,11 +380,16 @@ SOURCES_grafana_cloud_alerting_and_irm_slo='slo' SOURCES_grafana_cloud_k6='k6-docs' SOURCES_grafana_cloud_data_configuration_integrations='cloud-onboarding' SOURCES_grafana_cloud_frontend_observability_faro_web_sdk='faro-web-sdk' +SOURCES_grafana_cloud_send_data_fleet_management='fleet-management' SOURCES_helm_charts_mimir_distributed='mimir' SOURCES_helm_charts_tempo_distributed='tempo' SOURCES_opentelemetry='opentelemetry-docs' SOURCES_resources='website' +# The following variables comprise a pseudo associative array of project names to versions. +# You only need to set a VERSIONS variable if it is not the default of 'latest'. +# You can get a key identifier using the `identifier` function. +# To look up the value of any pseudo associative array, use the `aget` function. VERSIONS_as_code='UNVERSIONED' VERSIONS_grafana_cloud='UNVERSIONED' VERSIONS_grafana_cloud_alerting_and_irm_machine_learning='UNVERSIONED' @@ -378,12 +397,17 @@ VERSIONS_grafana_cloud_alerting_and_irm_slo='UNVERSIONED' VERSIONS_grafana_cloud_k6='UNVERSIONED' VERSIONS_grafana_cloud_data_configuration_integrations='UNVERSIONED' VERSIONS_grafana_cloud_frontend_observability_faro_web_sdk='UNVERSIONED' +VERSIONS_grafana_cloud_send_data_fleet_management='UNVERSIONED' VERSIONS_opentelemetry='UNVERSIONED' VERSIONS_resources='UNVERSIONED' VERSIONS_technical_documentation='UNVERSIONED' VERSIONS_website='UNVERSIONED' VERSIONS_writers_toolkit='UNVERSIONED' +# The following variables comprise a pseudo associative array of project names to source repository paths. +# You only need to set a PATHS variable if it is not the default of 'docs/sources'. +# You can get a key identifier using the `identifier` function. +# To look up the value of any pseudo associative array, use the `aget` function. PATHS_grafana_cloud='content/docs/grafana-cloud' PATHS_helm_charts_mimir_distributed='docs/sources/helm-charts/mimir-distributed' PATHS_helm_charts_tempo_distributed='docs/sources/helm-charts/tempo-distributed' @@ -816,7 +840,9 @@ EOF case "${OUTPUT_FORMAT}" in human) if ! command -v jq >/dev/null 2>&1; then + # shellcheck disable=SC2016 errr '`jq` must be installed for the `doc-validator` target to work.' + # shellcheck disable=SC2016 note 'To install `jq`, refer to https://jqlang.github.io/jq/download/,' exit 1 From c8297599a9832a96796f93e5a82bf4531a171729 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 17:27:40 +0000 Subject: [PATCH 246/894] Update dependency rollup to v4.32.1 (#99820) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 158 +++++++++++++++++++++++++++--------------------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/yarn.lock b/yarn.lock index b7e5a28f759..c930abae96b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6871,135 +6871,135 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.28.1" +"@rollup/rollup-android-arm-eabi@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.32.1" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-android-arm64@npm:4.28.1" +"@rollup/rollup-android-arm64@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-android-arm64@npm:4.32.1" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-darwin-arm64@npm:4.28.1" +"@rollup/rollup-darwin-arm64@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.32.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-darwin-x64@npm:4.28.1" +"@rollup/rollup-darwin-x64@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.32.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.28.1" +"@rollup/rollup-freebsd-arm64@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.32.1" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-freebsd-x64@npm:4.28.1" +"@rollup/rollup-freebsd-x64@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-freebsd-x64@npm:4.32.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.28.1" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.32.1" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.28.1" +"@rollup/rollup-linux-arm-musleabihf@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.32.1" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.28.1" +"@rollup/rollup-linux-arm64-gnu@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.32.1" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.28.1" +"@rollup/rollup-linux-arm64-musl@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.32.1" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.28.1" +"@rollup/rollup-linux-loongarch64-gnu@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.32.1" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.28.1" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.32.1" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.28.1" +"@rollup/rollup-linux-riscv64-gnu@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.32.1" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.28.1" +"@rollup/rollup-linux-s390x-gnu@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.32.1" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.28.1" +"@rollup/rollup-linux-x64-gnu@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.32.1" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.28.1" +"@rollup/rollup-linux-x64-musl@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.32.1" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.28.1" +"@rollup/rollup-win32-arm64-msvc@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.32.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.28.1" +"@rollup/rollup-win32-ia32-msvc@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.32.1" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.28.1" +"@rollup/rollup-win32-x64-msvc@npm:4.32.1": + version: 4.32.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.32.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -27488,28 +27488,28 @@ __metadata: linkType: hard "rollup@npm:^4.22.4": - version: 4.28.1 - resolution: "rollup@npm:4.28.1" + version: 4.32.1 + resolution: "rollup@npm:4.32.1" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.28.1" - "@rollup/rollup-android-arm64": "npm:4.28.1" - "@rollup/rollup-darwin-arm64": "npm:4.28.1" - "@rollup/rollup-darwin-x64": "npm:4.28.1" - "@rollup/rollup-freebsd-arm64": "npm:4.28.1" - "@rollup/rollup-freebsd-x64": "npm:4.28.1" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.28.1" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.28.1" - "@rollup/rollup-linux-arm64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-arm64-musl": "npm:4.28.1" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.28.1" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-s390x-gnu": "npm:4.28.1" - "@rollup/rollup-linux-x64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-x64-musl": "npm:4.28.1" - "@rollup/rollup-win32-arm64-msvc": "npm:4.28.1" - "@rollup/rollup-win32-ia32-msvc": "npm:4.28.1" - "@rollup/rollup-win32-x64-msvc": "npm:4.28.1" + "@rollup/rollup-android-arm-eabi": "npm:4.32.1" + "@rollup/rollup-android-arm64": "npm:4.32.1" + "@rollup/rollup-darwin-arm64": "npm:4.32.1" + "@rollup/rollup-darwin-x64": "npm:4.32.1" + "@rollup/rollup-freebsd-arm64": "npm:4.32.1" + "@rollup/rollup-freebsd-x64": "npm:4.32.1" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.32.1" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.32.1" + "@rollup/rollup-linux-arm64-gnu": "npm:4.32.1" + "@rollup/rollup-linux-arm64-musl": "npm:4.32.1" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.32.1" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.32.1" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.32.1" + "@rollup/rollup-linux-s390x-gnu": "npm:4.32.1" + "@rollup/rollup-linux-x64-gnu": "npm:4.32.1" + "@rollup/rollup-linux-x64-musl": "npm:4.32.1" + "@rollup/rollup-win32-arm64-msvc": "npm:4.32.1" + "@rollup/rollup-win32-ia32-msvc": "npm:4.32.1" + "@rollup/rollup-win32-x64-msvc": "npm:4.32.1" "@types/estree": "npm:1.0.6" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -27555,7 +27555,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/4337898d07e646835b52494b43b4ccd6929da87af2b0febc05ab217fd2425cfda05af5efaea6037c1641c90d803eb5b3e491eefdd47b28fda85af4f46a0dad34 + checksum: 10/5a64860df9d0c1b88d142b8502cb2e858e8314025ed35c605c70dc5c7c099fcecc9340cac269412c9a8b53705b911f1454b01164d23400c7d84cafb241be255f languageName: node linkType: hard From 8e53e997a07f1f13fc7f567fc6b362fbf176e027 Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Thu, 30 Jan 2025 12:32:31 -0600 Subject: [PATCH 247/894] Docs: Moving migrate to cloud guide from website repo to OSS (#99829) --- .../administration/migration-guide/_index.md | 23 ++ .../cloud-migration-assistant.md | 195 +++++++++++ .../manually-migrate-to-grafana-cloud.md | 312 ++++++++++++++++++ 3 files changed, 530 insertions(+) create mode 100644 docs/sources/administration/migration-guide/_index.md create mode 100644 docs/sources/administration/migration-guide/cloud-migration-assistant.md create mode 100644 docs/sources/administration/migration-guide/manually-migrate-to-grafana-cloud.md diff --git a/docs/sources/administration/migration-guide/_index.md b/docs/sources/administration/migration-guide/_index.md new file mode 100644 index 00000000000..9fb1faa2ddf --- /dev/null +++ b/docs/sources/administration/migration-guide/_index.md @@ -0,0 +1,23 @@ +--- +aliases: + - /docs/grafana-cloud/account-management/e2c-guide/ + - /docs/grafana-cloud/account-management/migration-guide/ +description: Migrate from Grafana OSS/Enterprise to Grafana Cloud +keywords: + - Grafana Cloud + - Grafana Enterprise + - Grafana OSS +menuTitle: Migrate from Grafana OSS/Enterprise to Grafana Cloud +title: Migrate from Grafana OSS/Enterprise to Grafana Cloud +--- + +# Migrate from Grafana OSS/Enterprise to Grafana Cloud + +When you decide to migrate from your self-managed Grafana instance to Grafana Cloud, you can benefit from the convenience of a managed observability platform, additional cloud-only features, and robust security. There are a couple of key approaches to help you transition to Grafana Cloud. + +| Migration type | Tools used | Availability | Migratable resources | +| :------------- | :-------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Manual |
  • Command line utilities
  • The Grafana HTTP API
| Generally available in all versions of Grafana OSS/Enterprise | The entire Grafana instance | +| Automated | The Grafana Cloud Migration Assistant | Available in public preview from Grafana v11.2 using the `OnPremToCloudMigration` feature toggle. This toggle is enabled by default in Grafana v11.5 and later. |
  • Dashboards
  • Folders
  • Data sources
  • App Plugins
  • Panel Plugins
  • Library Panels
  • Grafana Alerting resources
| + +Our detailed [migration guide](https://www.grafana.com/docs/grafana-cloud/account-management/migration-guide/manually-migrate-to-grafana-cloud/) explains the key steps and scripts to manually migrate your resources to Grafana Cloud, covering a comprehensive set of resources in your Grafana instance. Alternatively, the [Grafana Cloud Migration Assistant](https://www.grafana.com/docs/grafana-cloud/account-management/migration-guide/cloud-migration-assistant/), available in public preview in Grafana v11.2 and later, automates the migration process across a broad range of Grafana resources. You can use the migration assistant to migrate a large proportion of your Grafana resources and then, if needed, leverage the migration guide to migrate the rest. diff --git a/docs/sources/administration/migration-guide/cloud-migration-assistant.md b/docs/sources/administration/migration-guide/cloud-migration-assistant.md new file mode 100644 index 00000000000..4ee56b63c8b --- /dev/null +++ b/docs/sources/administration/migration-guide/cloud-migration-assistant.md @@ -0,0 +1,195 @@ +--- +description: Migrate from Grafana OSS/Enterprise to Grafana Cloud using the Grafana Cloud Migration Assistant +keywords: + - Grafana Cloud + - Grafana Enterprise + - Grafana OSS +menuTitle: Migrate to Grafana Cloud using the Grafana Cloud Migration Assistant +title: Migrate from Grafana OSS/Enterprise to Grafana Cloud using the Grafana Cloud Migration Assistant +weight: 400 +--- + +# Grafana Cloud Migration Assistant + +The Grafana Cloud Migration Assistant is available in Grafana 11.2+ as a [public preview feature](https://grafana.com/docs/release-life-cycle/#public-preview) that automatically migrates resources from your Grafana OSS/Enterprise instance to Grafana Cloud. It provides the following functionalities: + +- Securely connect your self-managed instance to a Grafana Cloud instance. +- Seamlessly migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps. +- View the migration status of your resources in real-time. + +Some of the benefits of the migration assistant are: + +Ease of use +: Follow the steps provided by the UI to easily migrate all your resources to Grafana Cloud without using Grafana APIs or scripts. + +Security +: Encrypt and securely migrate your resources to your connected Grafana Cloud instance. + +Speed +: Migrate all of your resources in minutes and accelerate your transition to Grafana Cloud. + +## Supported resources + +The following resources are supported by the migration assistant: + +- Dashboards +- Folders +- Data sources +- App Plugins +- Panel Plugins +- Library Panels +- Grafana Alerting resources + +## Before you begin + +To use the Grafana migration assistant, you need: + +- Grafana v11.2 or above with the `onPremToCloudMigrations` feature toggle enabled. In Grafana 11.5, this is enabled by default. For more information on how to enable a feature toggle, refer to [Configure feature toggles](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/#configure-feature-toggles). +- A [Grafana Cloud Stack](https://grafana.com/docs/grafana-cloud/get-started/) you intend to migrate your resources to. +- [`Admin`](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/cloud-roles/) access to the Grafana Cloud Stack. To check your access level, go to `https://grafana.com/orgs//members`. +- [Grafana server administrator](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/#grafana-server-administrators) access to your existing Grafana OSS/Enterprise instance. To check your access level, go to `https:///admin/users`. +- Internet access from your existing Grafana OSS/Enterprise instance. + +## Access the migration assistant + +In Grafana OSS, access to the migration assistant is limited to the server administrator. + +In Grafana Enterprise, the server administrator has access to the migration assistant by default. It is also possible to grant access to other Admins using a role-based access control (RBAC) role that enables other admins on the Grafana instance to view, build snapshots, and upload resources to Grafana Cloud. + +### Grant access in Grafana Enterprise + +{{< admonition type="impotrtant">}} +You must [configure RBAC](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/configure-rbac/) before you can grant other administrators access to the Grafana Migration Assistant. +{{< /admonition >}} + +To grant other Admins access to the migration assistant in Grafana Enterprise: + +1. Sign in to Grafana as a server administrator. +1. Navigate to **Home** > **Administration** > **Users and access** > **Users** in the Grafana sidebar. +1. Click an Admin. +1. In the **Organizations** section, click **Change role**. +1. Select **Organization resource migrator** from the role selector menu under **Migration Assistant**. + + ![The Organization resource migrator role in the role picker](/media/docs/grafana-cloud/account-management/screenshot-grant-migration-assistant-access.png) + +1. Click **Apply**. + +## Use the migration assistant + +You can use the migration assistant to generate a migration token on your Grafana Cloud instance, use that token to connect your self-managed Grafana instance to your Grafana Cloud instance, build snapshots of your self-managed Grafana instance, and upload these snapshots to Grafana Cloud. + +### Generate a migration token on the destination cloud instance: + +1. Navigate to **Home** > **Administration** > **General** > **Migrate to Grafana Cloud** in the cloud instance where you intend to migrate your resources. +1. Click on the **Generate a migration token** button. + + ![The Generate a migration token button in the Migrate to Grafana Cloud page in the intended Grafana Cloud Stack](/media/docs/grafana-cloud/account-management/screenshot-generate-migration-token.png) + +1. Make a copy of the migration token by copying to clipboard. The token is required to authenticate your self-managed instance with the Grafana Cloud Stack. + +### Connect your self-managed Grafana instance to the Grafana Cloud Stack + +1. On your self-managed Grafana instance, navigate to **Home** > **Administration** > **General** > **Migrate to Grafana Cloud**. + +1. Click the **Migrate this instance to Cloud** button. + + ![The Migrate this instance to Cloud button in the Migrate to Grafana Cloud page on a self-managed Grafana instance](/media/docs/grafana-cloud/account-management/screenshot-migrate-to-cloud.png) + +1. Enter your token and click **Connect to this Stack**. + + ![The Migration token field and Connect to this stack button in the Connect to a cloud stack page in a self-managed Grafana instance](/media/docs/grafana-cloud/account-management/screenshot-connect-to-a-stack.png) + +### Build a snapshot + +After connecting to the cloud stack, this is the empty state of the migration assistant. You need to create a snapshot of the self-managed Grafana instance to upload it to the cloud stack. + +- Click **Build snapshot** + + ![The Build snapshot button on the Migrate to Grafana Cloud page in a self-managed Grafana instance](/media/docs/grafana-cloud/account-management/screenshot-build-a-snapshot.png) + +### Upload resources to the cloud + +After a snapshot is created, a list of resources appears with resource Type and Status populated with **Not yet uploaded**. + +![A list of resources with snapshots built but not yet uploaded to Grafana Cloud](/media/docs/grafana-cloud/account-management/screenshot-upload-snapshot.png) + +1. Click on **Upload snapshot** to copy the resources to the Grafana Cloud instance. This also updates statuses for the list of resources. The status changes to 'Uploaded to cloud' for resources successfully copied to the cloud. + + The Snapshot information also updates to inform the user of total resources, errors, and total number of successfully migrated resources. + + ![An updates list of resources with snapshots built after attempting to upload them to Grafana Cloud](/media/docs/grafana-cloud/account-management/screenshot-updated-snapshot-page.png) + +1. Use the assistant's real-time progress tracking to monitor the migration. + +1. Review error details for any issues that need manual resolution. + +## Snapshots created by the migration assistant + +The migration assistant currently supports a subset of all resources available in Grafana. Refer to [Supported Resources](https://wwww.grafana.com/docs/grafana-cloud/account-management/cloud-migration-assistant/#supported-resources) for more details. + +When you create a snapshot, the migration assistant makes a copy of all supported resources and saves them in the snapshot. The snapshot reflects the current state of the resources when the snapshot is built and is stored locally on your instance, ready to be uploaded in the last stage. It is currently not possible to select specific resources to include in the snapshot, such as only dashboards. All supported resources are included by default. + +Resources saved in the snapshot are strictly limited to the resources stored within an organization. This is important to note if there are multiple organizations used in your Grafana instance. If you want to migrate multiple organizations, refer to [Migrate multiple organizations](https://wwww.grafana.com/docs/grafana-cloud/account-management/cloud-migration-assistant/#migrate-multiple-organizations) for more information and guidance. + +## Resource migration details + +During a migration, resource UIDs are preserved, allowing you to correlate your local and cloud resources. If you perform the same migration multiple times, resources in your Grafana Cloud stack that were previously migrated are updated. The assistant never modifies your self-managed resources or cloud resources that didn't come from a snapshot. + +### Dashboards and folders + +Dashboard names and UIDs are preserved along with references to data sources. Folder hierarchy is also preserved, so you can find your dashboards and other resources saved in identical folder locations. + +### Data sources + +Your data sources, including credentials, are migrated securely and seamlessly to your Grafana Cloud instance, so you don't need to find and enter all your data source credentials again. + +### Plugins + +The migration assistant supports any plugins found in the plugins catalog. As long as the plugin is signed or is a core plugin built into Grafana, it is eligible for migration. Due to security reasons, unsigned plugins are not supported in Grafana Cloud. If you are using any unsigned private plugins, Grafana recommends you seek an alternative plugin for the catalog or work on a strategy to deprecate certain functionality from your self-managed instance. + +### Grafana Alerting resources + +The migration assistant can migrate the majority of Grafana Alerting resources to your Grafana Cloud instance. These include: + +- Alert rules +- Notifications +- Contact points +- Mute timings +- Notification policy tree +- Notification templates + +This is sufficient to have your Alerting configuration up and running in Grafana Cloud with minimal effort. + +Migration of Silences is not supported by the migration assistant and needs to be configured manually. Alert History is also not available for migration. + +Successfully migrating Alerting resources to your Grafana Cloud instance could result in 2 sets of notifications being generated; one from your OSS/Enterprise instance and another from the newly migrated alerts in your Grafana Cloud instance. To avoid double notifications, a new `alert_rules_state` configuration option in the `custom.ini` or `grafana.ini` file controls how Alert Rules are migrated to the Grafana Cloud instance and is set to `paused` by default so you can review and test your Alerting resources in your Grafana Cloud instance without duplicate notifications. + +The available options for `alert_rule_state` are: + +`paused` +: Creates all Alert rules in paused state on the Cloud instance. This is helpful to avoid double notifications. + +`unchanged` +: The Alert rules maintain their original state coming from the source instance. + +When you are ready to start using your alert rules and notifications from your Grafana Cloud instance, run the migration again with `alert_rules_state = unchanged`. + +### Resource permissions + +Because the migration assistant does not yet migrate teams or RBAC permissions, your resources are migrated with default permissions. Ensure that you reconfigure permissions in your cloud stack as needed following a migration. For more information, refer to [Grafana Cloud user roles and permissions](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/cloud-roles/). + +## Migrate multiple organizations + +If you are using the [organizations](https://grafana.com/docs/grafana/latest/administration/organization-management/#about-organizations) feature on your Grafana Instance and intend to migrate to Grafana Cloud, you need to plan this aspect of the migration carefully. + +The organizations feature is not supported in Grafana Cloud, but folders and RBAC can be used to protect and grant permissions to resources instead. The recommended path is to migrate multiple organizations to a single cloud stack. This is the simplest option and provides the best user experience. + +The migration assistant creates and uploads snapshots based on the resources within a specific organization. There is no option to migrate an entire Grafana instance with multiple organizations at once. You need to run the migration process for each organization you want to migrate. + +The Grafana server administrator is granted access to the migration assistant by default. The server administrator can perform the migration by switching organizations and running the migration assistant each time. The Grafana server administrator can also grant access to the migration assistant to organization administrators who are members using the RBAC **Migration Assistant:Organization resource migrator** role. This allows those organization administrators to run the migration process for their respective organizations. + +### Access Control and managing resources in the Cloud Instance + +The main driver for setting up organizations in the first place is resource isolation. In order to achieve this in Grafana Cloud, you can organize resources into folders and set up teams and permissions that correspond to your organizations. + +For more information about configuring teams and permissions, refer to [Configure Grafana Teams](https://grafana.com/docs/grafana/latest/administration/team-management/configure-grafana-teams/). diff --git a/docs/sources/administration/migration-guide/manually-migrate-to-grafana-cloud.md b/docs/sources/administration/migration-guide/manually-migrate-to-grafana-cloud.md new file mode 100644 index 00000000000..6049846c108 --- /dev/null +++ b/docs/sources/administration/migration-guide/manually-migrate-to-grafana-cloud.md @@ -0,0 +1,312 @@ +--- +description: Migrate from Grafana OSS/Enterprise to Grafana Cloud manually +keywords: + - Grafana Cloud + - Grafana Enterprise + - Grafana OSS +menuTitle: Manually migrate to Grafana Cloud +title: Migrate from Grafana OSS/Enterprise to Grafana Cloud manually +weight: 300 +--- + +# Migrate from Grafana OSS/Enterprise to Grafana Cloud manually + +This migration guide is designed to assist Grafana OSS/Enterprise users in seamlessly transitioning manually to Grafana Cloud. + +{{< admonition type="note" >}} +There isn't yet a standard method for importing existing data into Grafana Cloud from self-managed databases. +{{< /admonition >}} + +## Plan and perform a manual migration + +If you need to migrate resources beyond what is supported by the Grafana Cloud Migration Assistant, you can migrate them manually with this guide. Moving your team from Grafana OSS/Enterprise to Grafana Cloud manually involves some coordination and communication in addition to the technical migration in the following documentation. + +If you are an existing Grafana OSS/Enterprise customer, contact your account team at Grafana Labs to plan a transition period, arrange licenses, and learn how much your Grafana Cloud subscription costs in comparison to Grafana OSS/Enterprise. The account team can also offer specific guidance and arrange professional services to assist with your migration if needed. + +Evaluate Grafana Cloud's security and compliance policies at the [Grafana Labs Trust Center](https://trust.grafana.com/). + +You may choose to test Grafana Cloud for some time before migrating your entire organization. To do so, set up a “test” stack in Cloud and migrate resources there first. If you use Grafana Alerting, make sure to set up a different contact point so that alerts do not fire twice. + +When you decide to migrate, set aside a day of cutover during which users should not create new dashboards or alerts. Migrate any newly-created resources, turn on your production Alerting contact points and notification policies in Cloud and turn them off in Grafana OSS/Enterprise, and notify your users. You may also choose to redirect the Grafana OSS/Enterprise URL to your Grafana Cloud URL. + +| Component | Migration Effort | Notes | +| ------------ | ---------------- | -------------------------------------------------------------------------- | +| Folders | Low | | +| Dashboards | Low | Data source references might need to be renamed | +| Alerts | Medium | Data source based alerts might need to be adapted | +| Plugins | Medium | Depends on the feature set of the plugin | +| Data sources | High | If the data sources references any secrets, you need to provide them again | + +## Before you begin + +Ensure you have the following: + +- A [Grafana Cloud Stack](https://grafana.com/docs/grafana-cloud/get-started/) and access to a Linux Machine (or a working WSL2 installation) to run the code snippets in this guide. +- Administrator access to a Grafana Cloud stack. To check you access level, Go to `https://grafana.com/orgs//members` +- Administrator access to your existing Grafana OSS/Enterprise instance. To check your access level, Go to `https:///admin/users` +- Access to the credentials used to connect to your data sources. For example, API keys or usernames and passwords. Since this information is encrypted, it cannot be copied from one instance to the other. +- If some of your data sources are only available from inside your network, refer to the requirements for [Private Data Source Connect](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) +- For Plugins, Reports and Playlists only: The [curl](https://github.com/curl/curl) and [jq](https://jqlang.github.io/jq/download/) command line tools + +## Upgrade Grafana OSS/Enterprise to the latest version + +Grafana Cloud stacks generally run the latest version of Grafana. In order to avoid issues during migration, upgrade Grafana by following our guides [here](https://grafana.com/docs/grafana/latest/upgrade-guide/). + +## Migrate Grafana resources + +In this guide, the term **"resources"** refers to things you create in Grafana, like dashboards, folders, alerts, data sources, and permissions. + +The process of migration works by pulling the existing resources (like dashboards and folders) from the old Grafana instance, modifying them if necessary, and then pushing them to the new Grafana Cloud instance. + +In the provided code snippets throughout this migration guide, you need to substitute specific placeholders with your actual credentials and instance URLs. Make the following replacements before executing the scripts: + +- `$GRAFANA_SOURCE_TOKEN` with the access token from your Grafana OSS/Enterprise instance. +- `$GRAFANA_DEST_TOKEN` with the access token from your Grafana Cloud instance. +- `$GRAFANA_ONPREM_INSTANCE_URL` with the URL of your Grafana OSS/Enterprise instance. For example: `https://grafana.mydomain.com` +- `$GRAFANA_CLOUD_INSTANCE_URL` with the URL of your Grafana Cloud instance. For example: `https://myorganization.grafana.net` + +### Migrate Grafana Plugins + +Migration of plugins is the first step when transitioning from Grafana OSS/Enterprise to Grafana Cloud, given that plugins are integral components that influence the functionality and display of other Grafana resources, such as dashboards. + +1. To retrieve the Plugins installed in your Grafana OSS/Enterprise instance, issue an HTTP GET request to the `/api/plugins` endpoint. Use the following shell command: + + ```shell + response=$(curl -s -H "Accept: application/json" -H "Authorization: Bearer $GRAFANA_SOURCE_TOKEN" "${GRAFANA_ONPREM_INSTANCE_URL}/api/plugins") + + plugins=$(echo $response | jq '[.[] | select(.signatureType == "community" or (.signatureType != "internal" and .signatureType != "")) | {name: .id, version: .info.version}]') + + echo "$plugins" > plugins.json + ``` + + The command provided above will carry out an HTTP request to this endpoint and accomplish several tasks: + + - It issues a GET request to the `/api/plugins` endpoint of your Grafana OSS/Enterprise instance to retrieve a list of installed plugins. + - It filters out the list to only include community plugins and those signed by external parties. + - It extracts the plugin ID and version before storing them in a `plugins.json` file. + +1. To import the plugins in your Grafana Cloud Instance, execute the following command. This command constructs an HTTP POST request to `https://grafana.com/api/instances//plugins` + + ```shell + CLOUD_INSTANCE=$GRAFANA_CLOUD_INSTANCE_URL + + stack_slug="${CLOUD_INSTANCE#*//}" + stack_slug="${stack_slug%%.*}" + jq -c '.[]' plugins.json | while IFS= read -r plugin; do + name=$(echo "$plugin" | jq -r '.name') + version=$(echo "$plugin" | jq -r '.version') + echo "Adding plugin $name with version $version to stack $stack_slug" + response=$(curl -s -X POST "https://grafana.com/api/instances/$stack_slug/plugins" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d "{\"plugin\": \"$name\", \"version\": \"$version\"}") + echo "POST response for plugin $name version $version: $response" + done + ``` + + Replace `` with your Grafana Cloud Access Policy Token. To create a new one, refer to Grafana Cloud [access policies documentation](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) + + This script iterates through each plugin listed in the `plugins.json` file: + + - It constructs a POST request for each plugin to add it to the specified Grafana Cloud instance. + - It reports back the response for each POST request to give you confirmation or information about any issues that occurred. + +### Migrate resources that are already provisioned as-code + +If you already use tools like [Terraform](https://grafana.com/docs/grafana-cloud/developer-resources/infrastructure-as-code/terraform/), [Ansible](https://grafana.com/docs/grafana-cloud/developer-resources/infrastructure-as-code/ansible/), or [Grafana’s HTTP API](https://grafana.com/docs/grafana-cloud/developer-resources/api-reference/http-api/) to provision resources to Grafana, redirect those to the new Grafana Cloud instance by replacing the Grafana URL and credentials. + +### Migrate dashboards, folders, data sources, library panels, and alert rules using Grizzly + +Grizzly is a command line tool that streamlines working with Grafana resources. Use it to migrate most of the content in your Grafana instance. Follow these steps in your terminal to install Grizzly. If you need to change the os or the architecture, Refer to the Grizzly [releases](https://github.com/grafana/grizzly/releases) and use the binary according to your needs. + +```shell +# download the binary (adapt os and arch as needed) +$ curl -fSL -o "/usr/local/bin/grr" "https://github.com/grafana/grizzly/releases/download/v0.3.1/grr-linux-amd64" + +# make it executable +$ chmod a+x "/usr/local/bin/grr" + +# have fun :) +$ grr --help +``` + +First, create a new folder on your computer and navigate to it to keep your work organized. + +```shell +mkdir grafana-migration +cd grafana-migration +``` + +To give grizzly access to your Grafana OSS/Enterprise instance and the Grafana Cloud Instance, you need to create a [service account](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/service-accounts/) and a corresponding [access token](https://www.grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/service-accounts/#service-account-tokens) on each instance. You can use these tokens to authenticate requests to pull and push resources. Follow these steps on your Grafana OSS/Enterprise instance: + +- Navigate to the **Administration -> Users and access -> Service Accounts** Page within the Grafana OSS/Enterprise instance. +- Click on **Add Service Account** +- Give the Service account a descriptive name like “grizzly-migration” and apply the **Admin** role. +- After creating the account, click on **Add Service Account Token** +- Enter a name for the token +- Select **Set expiration date** and enter an expiry date for the token +- Click **Generate Token** and save this token in a password manager or other secure place. + +Complete the service account creation and token generation process for your Grafana Cloud Instance by following the same steps outlined above for your Grafana OSS/Enterprise instance. This ensures that Grizzly has the necessary access token for both platforms. + +Next, to tell grizzly which instances you’re going to work on, use the following commands: + +```shell +grr config create-context grafana-onprem +grr config use-context grafana-onprem +grr config set output-format json +grr config set grafana.url $GRAFANA_ENT_INSTANCE_URL +grr config set grafana.token $GRAFANA_SOURCE_TOKEN + +grr config create-context grafana-cloud +grr config use-context grafana-cloud +grr config set output-format json +grr config set grafana.url $GRAFANA_CLOUD_INSTANCE_URL +grr config set grafana.token $GRAFANA_DEST_TOKEN +``` + +Afterward, you will have two contexts set up; one for your local Grafana OSS/Enterprise installation and one for Grafana Cloud. The `grr config use-context` command allows you to switch between the two instances while using Grizzly. + +#### Export existing resources + +Switch to the `grafana-onprem` context and use the pull command to fetch the resources you want to migrate: + +```shell +grr config use-context grafana-onprem +grr pull . \ + -t 'Dashboard/*' \ + -t 'Datasource/*' \ + -t 'DashboardFolder/*' \ + -t 'LibraryElement/*' \ + -t 'AlertRuleGroup/*' \ + -t 'AlertContactPoint/*' \ + -t 'AlertNotificationPolicy/*' +``` + +This will fetch the specified resources from Grafana and store them in the current directory. + +#### Push the resources to your Grafana Cloud stack + +With everything in place, switch to the Grafana cloud context and use the following commands to apply the resources to the configured instance: + +```shell +grr config use-context grafana-cloud + +grr apply . -t 'DashboardFolder/*' +grr apply . -t 'LibraryElement/*' +grr apply . -t 'Datasource/*' +grr apply . -t 'Dashboard/*' +grr apply . -t 'AlertRuleGroup/*' +grr apply . -t 'AlertContactPoint/*' +grr apply . -t 'AlertNotificationPolicy/*' +``` + +#### Fill in data source credentials + +After migrating your data sources, you must fill in their credentials, like tokens, usernames, or passwords. For security reasons, grizzly cannot read encrypted data source credentials from the existing Grafana instance. + +To fill in the missing authentication information, go to the **Connections -> Datasources** page in your new Grafana Cloud instance and verify that credentials for all data sources are set. You can skip data sources starting with `grafanacloud` - These are managed by Grafana Cloud directly and provide access to Grafana Cloud databases. + +If one of your data sources can only be accessed from your internal network, take a look at the [Private Data Source Connect documentation](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/). + +After you have configured the data sources, all your dashboards should be available as they were before. + +##### (Optional) Configure Private Data Source Connect (PDC) + +This step only applies if you use Grafana OSS/Enterprise to access network-secured data sources. + +Some data sources, like Prometheus or SQL databases, live on private networks or behind fire wall rules that are not accessible by Grafana Cloud. If your Grafana OSS/Enterprise instance was hosted on the same network as your data source, you might find that Grafana Cloud cannot connect to all of the same data sources that Grafana OSS/Enterprise could access. + +To access these data sources from Grafana Cloud, follow our guide to [configure PDC in your network](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/), and then configure the applicable Grafana data sources to [connect using PDC](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-a-data-source-to-use-private-data-source-connect-pdc). Note that PDC is only needed for your network-secured data sources, not for data sources like Splunk or CloudWatch that are accessible over the public internet. + +For more information on how PDC works, see our [overview document]({{< relref "../connect-externally-hosted/private-data-source-connect" >}}). + +### Migrate reports and playlists using Grafana’s HTTP API + +Grizzly does not currently support Reports and Playlists as a resource, so you can perform this migration using Grafana’s HTTP API using the `curl` command. + +#### Reports (For Grafana Enterprise only) + +1. To export your Reports, you will need to invoke the `api/reports` endpoint of your Grafana OSS/Enterprise instance. The below shell command accomplishes this by using `curl` to send a request to the endpoint and then stores the retrieved report configuration into a file named `reports.json`. + + ```shell + curl ${GRAFANA_ONPREM_INSTANCE_URL}/api/reports -H "Authorization: Bearer $GRAFANA_SOURCE_TOKEN" > reports.json + ``` + +2. To upload the configuration data you have saved in the `reports.json` file to your new Grafana Cloud instance, run the below command. The command will take the local file `reports.json` and push its contents to the `api/reports` endpoint of your Grafana Cloud instance. + + ```shell + jq -M -r -c '.[]' < reports.json | while read -r json; do curl -XPOST ${GRAFANA_CLOUD_INSTANCE_URL}/api/reports -H"Authorization: Bearer $GRAFANA_DEST_TOKEN" -d"$json" -H 'Content-Type: application/json'; done + ``` + +#### Playlists + +1. To retrieve the Playlists from your Grafana OSS/Enterprise instance, issue an HTTP GET request to the `/api/playlists` endpoint. Use the following shell command: + + ```shell + mkdir playlists + curl "${GRAFANA_ONPREM_INSTANCE_URL}/api/playlists" \ + -H "Authorization: Bearer $GRAFANA_SOURCE_TOKEN" \ + | jq -M -r -c '.[] | .uid' \ + | while read -r uid; do \ + curl "${GRAFANA_ONPREM_INSTANCE_URL}/api/playlists/$uid" \ + -H "Authorization: Bearer $GRAFANA_SOURCE_TOKEN" \ + > playlists/$uid.json; \ + done + ``` + + The command provided above will carry out an HTTP request to this endpoint and accomplish several tasks: + + - It fetches an array of all the playlists available in the Grafana OSS/Enterprise instance. + - It then iterates through each playlist to obtain the complete set of details. + - Finally, it stores each playlist's specification as separate JSON files within a directory named `playlists` + +2. To import the playlists, execute the following command. This command constructs an HTTP POST request targeting the `/api/playlists` endpoint of your Grafana Cloud Instance. + + ```shell + for playlist in playlists/*; do + curl -XPOST "${GRAFANA_CLOUD_INSTANCE_URL}/api/playlists" \ + -H "Authorization: Bearer $GRAFANA_DEST_TOKEN" \ + -H "Content-Type: application/json" \ + -d $playlist > /dev/null; + done + ``` + +### Migrate single sign-on configuration + +Grafana Cloud stacks support all of the same authentication and authorization options as Grafana OSS/Enterprise, except for [anonymous authentication](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/anonymous-auth/) and use of the [Auth proxy](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/auth-proxy/). However, single sign-on settings cannot be exported and imported like dashboards, alerts, and other resources. + +To set up SAML authentication from scratch using Grafana’s UI or API, follow [these instructions](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/saml-ui/) to Configure SAML authentication in Grafana. + +LDAP and OIDC/OAuth2 can only be configured in Grafana Cloud by the Grafana Labs support team. Follow [these instructions](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/) to request SSO configuration from the support team. + +### Migrate custom Grafana configuration + +You may have customized the [configuration](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/) of your Grafana OSS/Enterprise instance, for example with feature toggles, custom auth, or embedding options. Since Grafana configuration is stored in environment variables or the filesystem where Grafana runs, Grafana Cloud users do not have access to it. However, you can open a support ticket to ask a Grafana Labs support engineer for customizations. + +The following customizations are available via support: + +- Enabling [feature toggles](http://www.grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles). +- [Single sign-on and team sync using SAML, LDAP, or OAuth](http://www.grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication). +- Enable [embedding Grafana dashboards in other applications](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#allow_embedding) for Grafana Cloud contracted customers. +- [Audit logging](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/audit-grafana/) ([Usage insights logs and dashboards](https://grafana.com/docs/grafana-cloud/account-management/usage-insights/) are available in Grafana Cloud Pro and Advanced by default). + +Note that the following custom configurations are not supported in Grafana Cloud: + +- [Anonymous user access](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/anonymous-auth/). +- [Auth proxy](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/auth-proxy/). +- [Third-party database encryption](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-database-encryption/) and the [Hashicorp Vault](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-hashicorp-key-vault/) integration. +- Running self-signed plugins, like custom-built data sources or visualizations. For more information on plugin signing, refer to our [developer documentation](https://grafana.com/developers/plugin-tools/publish-a-plugin/sign-a-plugin). + +If you have a custom configuration in Grafana OSS/Enterprise that is not listed here, reach out to our support team to find out whether they can help you set it up. + +## Next steps + +After you have successfully migrated resources and configuration from Grafana OSS/Enterprise, consider the following steps to enhance your monitoring experience: + +- **Get started with Grafana Cloud**: learn more about the functionality available in Grafana Cloud, which is not available in the open source or Enterprise editions. Read more in [Get started with Grafana Cloud](https://grafana.com/docs/grafana-cloud/get-started/) +- **AWS PrivateLink for Grafana Cloud**: securely transmit telemetry data from your AWS Virtual Private Cloud (VPC) to Grafana Cloud, entirely within the AWS network. + Learn how to set this up with [AWS PrivateLink Integration](https://grafana.com/docs/grafana-cloud/send-data/aws-privatelink/). +- **Azure PrivateLink for Grafana Cloud**, securely transmit telemetry from your Azure Virtual Network to Grafana Cloud while staying on the Azure network, and avoid exposing your traffic to the public internet. + Learn how to set this up with [AWS PrivateLink Integration](https://grafana.com/docs/grafana-cloud/send-data/azure-privatelink/). +- **[Grafana Integrations](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/integrations/)**: ready-made integrations to make monitoring your infrastructure and applications more straightforward. From ce38eb339819453f71fb1815b5cf1ef233373781 Mon Sep 17 00:00:00 2001 From: Scott Lepper Date: Thu, 30 Jan 2025 14:22:22 -0500 Subject: [PATCH 248/894] [search] fix dashboard list default sort (#99813) [search] fix dashboard list default sort --- pkg/registry/apis/dashboard/search.go | 10 +++ pkg/registry/apis/dashboard/search_test.go | 84 +++++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 7f866943cbe..fc170a36783 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "slices" + "sort" "strconv" "strings" @@ -198,6 +199,7 @@ func (s *SearchHandler) DoSortable(w http.ResponseWriter, r *http.Request) { const rootFolder = "general" +//nolint:gocyclo func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { ctx, span := s.tracer.Start(r.Context(), "dashboard.search") defer span.End() @@ -346,6 +348,14 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { return } + if parsedResults != nil && len(searchRequest.SortBy) == 0 { + // default sort by resource descending ( folders then dashboards ) then title + sort.Slice(parsedResults.Hits, func(i, j int) bool { + return parsedResults.Hits[i].Resource > parsedResults.Hits[j].Resource || + (parsedResults.Hits[i].Resource == parsedResults.Hits[j].Resource && strings.ToLower(parsedResults.Hits[i].Title) < strings.ToLower(parsedResults.Hits[j].Title)) + }) + } + s.write(w, parsedResults) } diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index fbd3a209ff5..83e46a6e6c2 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -2,15 +2,19 @@ package dashboard import ( "context" + "encoding/json" "fmt" "net/http/httptest" "testing" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/grpc" ) @@ -177,7 +181,7 @@ func TestSearchFallback(t *testing.T) { } */ -func TestSearchHandlerFields(t *testing.T) { +func TestSearchHandler(t *testing.T) { // Create a mock client mockClient := &MockClient{} @@ -238,6 +242,33 @@ func TestSearchHandlerFields(t *testing.T) { t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) } }) + + t.Run("Sort - default sort by resource then title", func(t *testing.T) { + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + + resp := rr.Result() + defer func() { + if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } + }() + + p := &v0alpha1.SearchResults{} + err := json.NewDecoder(resp.Body).Decode(p) + require.NoError(t, err) + assert.Equal(t, len(mockResults), len(p.Hits)) + assert.Equal(t, mockResults[3].Value, p.Hits[0].Title) + assert.Equal(t, mockResults[1].Value, p.Hits[3].Title) + }) } // MockClient implements the ResourceIndexClient interface for testing @@ -248,10 +279,59 @@ type MockClient struct { LastSearchRequest *resource.ResourceSearchRequest } +type MockResult struct { + Name string + Resource string + Value string +} + +var mockResults = []MockResult{ + { + Name: "d1", + Resource: "dashboard", + Value: "Dashboard 1", + }, + { + Name: "d2", + Resource: "dashboard", + Value: "Dashboard 2", + }, + { + Name: "f2", + Resource: "folder", + Value: "Folder 2", + }, + { + Name: "f1", + Resource: "folder", + Value: "Folder 1", + }, +} + func (m *MockClient) Search(ctx context.Context, in *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) { m.LastSearchRequest = in - return &resource.ResourceSearchResponse{}, nil + rows := make([]*resource.ResourceTableRow, len(mockResults)) + for i, r := range mockResults { + rows[i] = &resource.ResourceTableRow{ + Key: &resource.ResourceKey{ + Name: r.Name, + Resource: r.Resource, + }, + Cells: [][]byte{ + []byte(r.Value), + }, + } + } + + return &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + {Name: resource.SEARCH_FIELD_TITLE}, + }, + Rows: rows, + }, + }, nil } func (m *MockClient) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) { From 8ce8c1635fc5822faed1fbae67ee0f6671c225eb Mon Sep 17 00:00:00 2001 From: beejeebus Date: Thu, 30 Jan 2025 15:36:45 -0500 Subject: [PATCH 249/894] Escape database names in MSSQL datasource (#99754) Valid MSSQL database names can contain characters like `-`, which need to be escaped when used in queries. This PR wraps database names in `[]`, and fixes Grafana issue #58757. --- .../datasource/mssql/MSSqlMetaQuery.test.ts | 10 ++++++++++ .../plugins/datasource/mssql/MSSqlMetaQuery.ts | 2 +- .../plugins/datasource/mssql/sqlUtil.test.ts | 18 ++++++++++++++++++ public/app/plugins/datasource/mssql/sqlUtil.ts | 2 +- 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 public/app/plugins/datasource/mssql/MSSqlMetaQuery.test.ts create mode 100644 public/app/plugins/datasource/mssql/sqlUtil.test.ts diff --git a/public/app/plugins/datasource/mssql/MSSqlMetaQuery.test.ts b/public/app/plugins/datasource/mssql/MSSqlMetaQuery.test.ts new file mode 100644 index 00000000000..b479a7dc629 --- /dev/null +++ b/public/app/plugins/datasource/mssql/MSSqlMetaQuery.test.ts @@ -0,0 +1,10 @@ +import { getSchema } from './MSSqlMetaQuery'; + +describe('getSchema', () => { + const database = 'foo'; + const table = 'bar'; + const schema = getSchema(database, table); + it('should escapte database names', () => { + expect(schema).toContain(`USE [${database}]`); + }); +}); diff --git a/public/app/plugins/datasource/mssql/MSSqlMetaQuery.ts b/public/app/plugins/datasource/mssql/MSSqlMetaQuery.ts index 070be7252c8..fca04eae97a 100644 --- a/public/app/plugins/datasource/mssql/MSSqlMetaQuery.ts +++ b/public/app/plugins/datasource/mssql/MSSqlMetaQuery.ts @@ -10,7 +10,7 @@ export function getSchemaAndName(database?: string) { export function getSchema(database?: string, table?: string) { return ` - USE ${database} + USE [${database}] SELECT COLUMN_NAME as 'column',DATA_TYPE as 'type' FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='${table}';`; } diff --git a/public/app/plugins/datasource/mssql/sqlUtil.test.ts b/public/app/plugins/datasource/mssql/sqlUtil.test.ts new file mode 100644 index 00000000000..807615a9d1c --- /dev/null +++ b/public/app/plugins/datasource/mssql/sqlUtil.test.ts @@ -0,0 +1,18 @@ +import { SQLQuery, QueryEditorExpressionType } from '@grafana/sql'; + +import { toRawSql } from './sqlUtil'; + +describe('toRawSql should escape database names', () => { + const query: SQLQuery = { + dataset: 'foo', + sql: { + columns: [{ name: 'a', alias: 'lol', type: QueryEditorExpressionType.Function }], + }, + refId: 'lolsob', + table: 'table', + }; + const queryString = toRawSql(query); + it('should escapte database names', () => { + expect(queryString).toContain(`FROM [${query.dataset}].${query.table}`); + }); +}); diff --git a/public/app/plugins/datasource/mssql/sqlUtil.ts b/public/app/plugins/datasource/mssql/sqlUtil.ts index ba7347e77ae..92b35a70411 100644 --- a/public/app/plugins/datasource/mssql/sqlUtil.ts +++ b/public/app/plugins/datasource/mssql/sqlUtil.ts @@ -89,7 +89,7 @@ export function toRawSql({ sql, dataset, table }: SQLQuery): string { rawQuery += createSelectClause(sql.columns, sql.limit); if (dataset && table) { - rawQuery += `FROM ${dataset}.${table} `; + rawQuery += `FROM [${dataset}].${table} `; } if (sql.whereString) { From e3d9b6cadfd508c2805bb417d11e24919bebc817 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 31 Jan 2025 01:19:09 +0300 Subject: [PATCH 250/894] K8s/Unstructured: Avoid panic in DeepCopy (#99840) --- .../apis/common/v0alpha1/unstructured.go | 54 ++++++++++++++++++- .../apis/common/v0alpha1/unstructured_test.go | 44 +++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go diff --git a/pkg/apimachinery/apis/common/v0alpha1/unstructured.go b/pkg/apimachinery/apis/common/v0alpha1/unstructured.go index e779c5fafd3..17417fd85fa 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/unstructured.go +++ b/pkg/apimachinery/apis/common/v0alpha1/unstructured.go @@ -2,6 +2,7 @@ package v0alpha1 import ( "encoding/json" + "reflect" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" runtime "k8s.io/apimachinery/pkg/runtime" @@ -63,7 +64,7 @@ func (u *Unstructured) DeepCopy() *Unstructured { } out := new(Unstructured) *out = *u - out.Object = runtime.DeepCopyJSON(u.Object) + out.Object = deepCopyJSONValue(u.Object).(map[string]interface{}) return out } @@ -72,6 +73,57 @@ func (u *Unstructured) DeepCopyInto(out *Unstructured) { *out = *clone } +// Copied from: +// +// runtime.DeepCopyJSON(u.Object) +// +// BUT this avoids panic on int +func deepCopyJSONValue(x interface{}) interface{} { + switch x := x.(type) { + case map[string]interface{}: + if x == nil { + // Typed nil - an interface{} that contains a type map[string]interface{} with a value of nil + return x + } + clone := make(map[string]interface{}, len(x)) + for k, v := range x { + clone[k] = deepCopyJSONValue(v) + } + return clone + case []interface{}: + if x == nil { + // Typed nil - an interface{} that contains a type []interface{} with a value of nil + return x + } + clone := make([]interface{}, len(x)) + for i, v := range x { + clone[i] = deepCopyJSONValue(v) + } + return clone + case string, int64, bool, float64, nil, json.Number: + return x + + // Keep more numbers + case int, int8, int16, int32, float32, uint, uint16, uint32, uint64, uint8: + return x + + case runtime.Object: + return x.DeepCopyObject() + + default: + // fallback to reflection + val := reflect.ValueOf(x).Elem() + cpy := reflect.New(val.Type()) + cpy.Elem().Set(val) + + // Using the , for the type conversion ensures that it doesn't panic if it can't be converted + if obj, ok := cpy.Interface().(runtime.Object); ok { + return obj + } + return x + } +} + func (u *Unstructured) Set(field string, value interface{}) { if u.Object == nil { u.Object = make(map[string]interface{}) diff --git a/pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go b/pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go new file mode 100644 index 00000000000..1698bd081c0 --- /dev/null +++ b/pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go @@ -0,0 +1,44 @@ +package v0alpha1 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestDeepCopyJSON(t *testing.T) { + obj := &Unstructured{ + Object: map[string]interface{}{ + "int": int(2), + "int16": int16(2), + "int32": int32(2), + "uint64": uint64(2), + "ref": &ObjectReference{ + Resource: "x", + }, + "array": []any{ + int(1), int64(2), "hello", + }, + "string": "hello", + "bool": true, + "map": map[string]any{ + "x": &ObjectReference{ + Resource: "x", + }, + }, + "object": &v1.APIGroup{ + Name: "HELLO", + }, + }, + } + before, err := json.Marshal(obj) + require.NoError(t, err) + + clone := obj.DeepCopy() + after, err := json.Marshal(clone) + require.NoError(t, err) + + require.JSONEq(t, string(before), string(after)) +} From 58df80e542201b91e480c3425ecce6dcd792c1d3 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Thu, 30 Jan 2025 18:29:51 -0600 Subject: [PATCH 251/894] Unified Storage: Fix panic from log (#99850) evt.Object can be nil, so use the key instead --- pkg/storage/unified/resource/search.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index c7aa0c5d03a..6d03f97e6f2 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -461,7 +461,7 @@ func (s *searchSupport) handleEvent(ctx context.Context, evt *WrittenEvent) { latencySeconds := float64(time.Now().UnixMicro()-evt.ResourceVersion) / 1e6 span.AddEvent("index latency", trace.WithAttributes(attribute.Float64("latency_seconds", latencySeconds))) if latencySeconds > 5 { - s.log.Debug("high index latency object details", "resource", evt.Key.Resource, "latency_seconds", latencySeconds, "name", evt.Object.GetName(), "namespace", evt.Object.GetNamespace(), "uid", evt.Object.GetUID()) + s.log.Debug("high index latency object details", "resource", evt.Key.Resource, "latency_seconds", latencySeconds, "name", evt.Key.Name, "namespace", evt.Key.Namespace) s.log.Warn("high index latency", "latency", latencySeconds) } if IndexMetrics != nil { From bda4deb20c449692ca3d1a8f9066f614c7f69416 Mon Sep 17 00:00:00 2001 From: Alexa V <239999+axelavargas@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:30:39 +0100 Subject: [PATCH 252/894] Dashboards - Schema V2 - Implement move dashboards using v2 api (#99664) * Dashboards - Schema V2 - Implement move dashboards using v2 api * Fix root folder uid not being picked by FolderPicker --- .../api/browseDashboardsAPI.ts | 27 +++++++++++++------ public/app/features/dashboard/api/v2.ts | 7 +++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 27b46514d70..bbe79f9cdb3 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -263,16 +263,27 @@ export const browseDashboardsAPI = createApi({ // Move all the dashboards sequentially // TODO error handling here for (const dashboardUID of selectedDashboards) { - const fullDash: DashboardDTO = await getDashboardAPI().getDashboardDTO(dashboardUID); + if (config.featureToggles.useV2DashboardsAPI) { + const fullDash = await getDashboardAPI('v2').getDashboardDTO(dashboardUID); - await getDashboardAPI().saveDashboard({ - dashboard: fullDash.dashboard, - folderUid: destinationUID, - overwrite: false, - message: '', - }); + await getDashboardAPI('v2').saveDashboard({ + dashboard: fullDash.spec, + folderUid: destinationUID, + overwrite: false, + message: '', + k8s: fullDash.metadata, + }); + } else { + const fullDash: DashboardDTO = await getDashboardAPI().getDashboardDTO(dashboardUID); + + await getDashboardAPI().saveDashboard({ + dashboard: fullDash.dashboard, + folderUid: destinationUID, + overwrite: false, + message: '', + }); + } } - return { data: undefined }; }, onQueryStarted: ({ destinationUID, selectedItems }, { queryFulfilled, dispatch }) => { diff --git a/public/app/features/dashboard/api/v2.ts b/public/app/features/dashboard/api/v2.ts index 3a283071091..ec4fa199386 100644 --- a/public/app/features/dashboard/api/v2.ts +++ b/public/app/features/dashboard/api/v2.ts @@ -56,6 +56,11 @@ export class K8sDashboardV2API } catch (e) { throw new Error('Failed to load folder'); } + } else if (result.metadata.annotations && !result.metadata.annotations[AnnoKeyFolder]) { + // Set AnnoKeyFolder to empty string for top-level dashboards + // This ensures NestedFolderPicker correctly identifies it as being in the "Dashboard" root folder + // AnnoKeyFolder undefined -> top-level dashboard -> empty string + result.metadata.annotations[AnnoKeyFolder] = ''; } // Depending on the ui components readiness, we might need to convert the response to v1 @@ -117,6 +122,8 @@ export class K8sDashboardV2API } if (obj.metadata.name) { + // remove resource version when updating + delete obj.metadata.resourceVersion; return this.client.update(obj).then((v) => this.asSaveDashboardResponseDTO(v)); } return await this.client.create(obj).then((v) => this.asSaveDashboardResponseDTO(v)); From bfdd00665bdf6469b55af20fcdb6dac3ea6000b3 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 31 Jan 2025 10:36:56 +0100 Subject: [PATCH 253/894] Schema V2: Support v2 custom home dashboards (#99748) --- .../DashboardScenePageStateManager.test.ts | 135 +++++++++++++++++- .../pages/DashboardScenePageStateManager.ts | 65 +++++---- .../dashboard/api/ResponseTransformers.ts | 7 +- .../features/dashboard/state/initDashboard.ts | 6 +- public/app/types/dashboard.ts | 9 +- 5 files changed, 186 insertions(+), 36 deletions(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 64d850e9047..d885e17fdda 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -154,6 +154,22 @@ describe('DashboardScenePageStateManager v1', () => { status: 500, }); }); + + it('should throw when v2 custom home dashboard is provided', async () => { + setBackendSrv({ + get: () => Promise.resolve({ dashboard: customHomeDashboardV2Spec, meta: {} }), + } as unknown as BackendSrv); + + const loader = new DashboardScenePageStateManager({}); + await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home }); + + expect(loader.state.dashboard).toBeUndefined(); + expect(loader.state.loadError).toEqual({ + message: 'v2 dashboard spec is not supported. Enable useV2DashboardsAPI feature toggle', + messageId: undefined, + status: undefined, + }); + }); }); describe('New dashboards', () => { @@ -423,13 +439,12 @@ describe('DashboardScenePageStateManager v2', () => { }); describe('Home dashboard', () => { - // TODO: Unskip when redirect is implemented in v2 API - it.skip('should handle home dashboard redirect', async () => { + it('should handle home dashboard redirect', async () => { setBackendSrv({ get: () => Promise.resolve({ redirectUri: '/d/asd' }), } as unknown as BackendSrv); - const loader = new DashboardScenePageStateManager({}); + const loader = new DashboardScenePageStateManagerV2({}); await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home }); expect(loader.state.dashboard).toBeUndefined(); @@ -455,6 +470,45 @@ describe('DashboardScenePageStateManager v2', () => { status: 500, }); }); + + it('should not transform v2 custom home dashboard spec', async () => { + setBackendSrv({ + get: () => + Promise.resolve({ + dashboard: customHomeDashboardV2Spec, + meta: { + canSave: false, + canEdit: true, + canAdmin: false, + canStar: false, + canDelete: false, + slug: '', + url: '', + expires: '0001-01-01T00:00:00Z', + created: '0001-01-01T00:00:00Z', + updated: '0001-01-01T00:00:00Z', + updatedBy: '', + createdBy: '', + version: 0, + hasAcl: false, + isFolder: false, + folderId: 0, + folderUid: '', + folderTitle: 'General', + folderUrl: '', + provisioned: false, + provisionedExternalId: '', + annotationsPermissions: null, + }, + }), + } as unknown as BackendSrv); + + const loader = new DashboardScenePageStateManagerV2({}); + await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home }); + + expect(loader.state.dashboard?.getInitialSaveModel()).toEqual(customHomeDashboardV2Spec); + expect(loader.state.loadError).toBeUndefined(); + }); }); describe('New dashboards', () => { @@ -593,3 +647,78 @@ describe('DashboardScenePageStateManager v2', () => { }); }); }); + +const customHomeDashboardV2Spec = { + title: 'Home Dashboard v2 schema', + cursorSync: 'Off', + preload: false, + editable: true, + links: [], + tags: [], + timeSettings: { + timezone: 'browser', + from: 'now-6h', + to: 'now', + autoRefresh: '', + autoRefreshIntervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], + quickRanges: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'], + hideTimepicker: false, + weekStart: '', + fiscalYearStartMonth: 0, + }, + variables: [], + elements: { + text_panel: { + kind: 'Panel', + spec: { + id: 0, + title: 'Welcome', + description: 'Welcome to the home dashboard!', + links: [], + data: { + kind: 'QueryGroup', + spec: { + queries: [], + transformations: [], + queryOptions: {}, + }, + }, + vizConfig: { + kind: 'text', + spec: { + pluginVersion: '', + options: { + mode: 'markdown', + content: '# Welcome to the home dashboard!\n\n## Example of v2 schema home dashboard', + }, + fieldConfig: { + defaults: {}, + overrides: [], + }, + }, + }, + }, + }, + }, + annotations: [], + layout: { + kind: 'GridLayout', + spec: { + items: [ + { + kind: 'GridLayoutItem', + spec: { + x: 6, + y: 0, + width: 12, + height: 6, + element: { + kind: 'ElementReference', + name: 'text_panel', + }, + }, + }, + ], + }, + }, +}; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 8989681da8d..161ee48f10c 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -10,11 +10,18 @@ import { startMeasure, stopMeasure } from 'app/core/utils/metrics'; import { AnnoKeyFolder } from 'app/features/apiserver/types'; import { ResponseTransformers } from 'app/features/dashboard/api/ResponseTransformers'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; +import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; import { dashboardLoaderSrv, DashboardLoaderSrvV2 } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor'; import { trackDashboardSceneLoaded } from 'app/features/dashboard/utils/tracking'; -import { DashboardDTO, DashboardRoutes } from 'app/types'; +import { + DashboardDataDTO, + DashboardDTO, + DashboardRoutes, + HomeDashboardRedirectDTO, + isRedirectResponse, +} from 'app/types'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardScene } from '../scene/DashboardScene'; @@ -68,6 +75,10 @@ export interface LoadDashboardOptions { }; } +export type HomeDashboardDTO = DashboardDTO & { + dashboard: DashboardDataDTO | DashboardV2Spec; +}; + interface DashboardScenePageStateManagerLike { fetchDashboard(options: LoadDashboardOptions): Promise; getDashboardFromCache(cacheKey: string): T | null; @@ -167,6 +178,11 @@ abstract class DashboardScenePageStateManagerBase private async loadScene(options: LoadDashboardOptions): Promise { this.setState({ dashboard: undefined, isLoading: true }); const rsp = await this.fetchDashboard(options); + + if (!rsp) { + return null; + } + return this.transformResponseToScene(rsp, options); } @@ -235,12 +251,6 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag return scene; } - if (rsp?.redirectUri) { - const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri); - locationService.replace(newUrl); - return null; - } - throw new Error('Dashboard not found'); } @@ -271,7 +281,7 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag } } - let rsp: DashboardDTO; + let rsp: DashboardDTO | HomeDashboardRedirectDTO; try { switch (route) { @@ -280,10 +290,16 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag break; case DashboardRoutes.Home: - rsp = await getBackendSrv().get('/api/dashboards/home'); + rsp = await getBackendSrv().get('/api/dashboards/home'); - if (rsp.redirectUri) { - return rsp; + if (isRedirectResponse(rsp)) { + const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri); + locationService.replace(newUrl); + return null; + } + + if (isDashboardV2Spec(rsp.dashboard)) { + throw new Error('v2 dashboard spec is not supported. Enable useV2DashboardsAPI feature toggle'); } if (rsp?.meta) { @@ -453,13 +469,6 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan return scene; } - // TOD)[schema v2]: Figure out redirect utl - // if (rsp?.redirectUri) { - // const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri); - // locationService.replace(newUrl); - // return null; - // } - throw new Error('Dashboard not found'); } @@ -487,17 +496,25 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan rsp = await buildNewDashboardSaveModelV2(urlFolderUid); break; case DashboardRoutes.Home: - // throw new Error('Method not implemented.'); - const dto = await getBackendSrv().get('/api/dashboards/home'); + const dto = await getBackendSrv().get('/api/dashboards/home'); + + if (isRedirectResponse(dto)) { + const newUrl = locationUtil.stripBaseFromUrl(dto.redirectUri); + locationService.replace(newUrl); + return null; + } + rsp = ResponseTransformers.ensureV2Response(dto); + + // if custom home dashboard is v2 spec already, ignore the spec transformation + if (isDashboardV2Spec(dto.dashboard)) { + rsp.spec = dto.dashboard; + } + rsp.access.canSave = false; rsp.access.canShare = false; rsp.access.canStar = false; - // if (rsp.redirectUri) { - // return rsp; - // } - break; case DashboardRoutes.Public: { return await this.dashboardLoader.loadDashboard('public', '', uid); diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 1831507291b..535265f606d 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -103,13 +103,8 @@ export function ensureV2Response( if (isDashboardResource(dto)) { accessMeta = dto.access; annotationsMeta = { - [AnnoKeyCreatedBy]: dto.metadata.annotations?.[AnnoKeyCreatedBy], - [AnnoKeyUpdatedBy]: dto.metadata.annotations?.[AnnoKeyUpdatedBy], - [AnnoKeyUpdatedTimestamp]: dto.metadata.annotations?.[AnnoKeyUpdatedTimestamp], - [AnnoKeyFolder]: dto.metadata.annotations?.[AnnoKeyFolder], - [AnnoKeySlug]: dto.metadata.annotations?.[AnnoKeySlug], + ...dto.metadata.annotations, [AnnoKeyDashboardGnetId]: dashboard.gnetId ?? undefined, - [AnnoKeyDashboardIsSnapshot]: dto.metadata.annotations?.[AnnoKeyDashboardIsSnapshot], }; creationTimestamp = dto.metadata.creationTimestamp; labelsMeta = { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 4ee7261cf02..23b36244519 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -24,6 +24,8 @@ import { DashboardDTO, DashboardInitPhase, DashboardRoutes, + HomeDashboardRedirectDTO, + isRedirectResponse, StoreState, ThunkDispatch, ThunkResult, @@ -69,10 +71,10 @@ async function fetchDashboard( } // load home dash - const dashDTO: DashboardDTO = await backendSrv.get('/api/dashboards/home'); + const dashDTO = await backendSrv.get('/api/dashboards/home'); // if user specified a custom home dashboard redirect to that - if (dashDTO.redirectUri) { + if (isRedirectResponse(dashDTO)) { const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri); locationService.replace(newUrl); return null; diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index fb4eb3fd962..51478ac3829 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -3,8 +3,11 @@ import { Dashboard, DataSourceRef } from '@grafana/schema'; import { ObjectMeta } from 'app/features/apiserver/types'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; +export interface HomeDashboardRedirectDTO { + redirectUri: string; +} + export interface DashboardDTO { - redirectUri?: string; dashboard: DashboardDataDTO; meta: DashboardMeta; } @@ -140,3 +143,7 @@ export interface DashboardState { } export const DASHBOARD_FROM_LS_KEY = 'DASHBOARD_FROM_LS_KEY'; + +export function isRedirectResponse(dto: DashboardDTO | HomeDashboardRedirectDTO): dto is HomeDashboardRedirectDTO { + return 'redirectUri' in dto; +} From 3b231c433f1f82556cad70debf34ec63106374d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 09:37:24 +0000 Subject: [PATCH 254/894] Update dependency semver to v7.7.0 (#99837) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- public/app/plugins/datasource/tempo/package.json | 2 +- yarn.lock | 16 ++++++++-------- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 0cb29eae330..d887106e6ca 100644 --- a/package.json +++ b/package.json @@ -398,7 +398,7 @@ "reselect": "5.1.1", "rxjs": "7.8.1", "selecto": "1.26.3", - "semver": "7.6.3", + "semver": "7.7.0", "slate": "0.47.9", "slate-plain-serializer": "0.7.13", "slate-react": "0.22.10", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 5eaf8277ad1..0c797232400 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -51,7 +51,7 @@ }, "dependencies": { "@grafana/tsconfig": "^2.0.0", - "semver": "7.6.3", + "semver": "7.7.0", "tslib": "2.8.1", "typescript": "5.7.3" } diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 48d778736d4..a27c1776ec7 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -70,7 +70,7 @@ "react-use": "17.6.0", "react-window": "1.8.11", "rxjs": "7.8.1", - "semver": "7.6.3", + "semver": "7.7.0", "tslib": "2.8.1", "uuid": "11.0.5", "whatwg-fetch": "3.6.20" diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 32e5bd74287..af785f67a57 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -32,7 +32,7 @@ "react-select": "5.10.0", "react-use": "17.6.0", "rxjs": "7.8.1", - "semver": "7.6.3", + "semver": "7.7.0", "stream-browserify": "3.0.0", "string_decoder": "1.3.0", "tslib": "2.8.1", diff --git a/yarn.lock b/yarn.lock index c930abae96b..0d52aee116a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3083,7 +3083,7 @@ __metadata: react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.1" - semver: "npm:7.6.3" + semver: "npm:7.7.0" stream-browserify: "npm:3.0.0" string_decoder: "npm:1.3.0" ts-node: "npm:10.9.2" @@ -3272,7 +3272,7 @@ __metadata: rollup-plugin-dts: "npm:^6.1.1" rollup-plugin-esbuild: "npm:6.1.1" rollup-plugin-node-externals: "npm:^8.0.0" - semver: "npm:7.6.3" + semver: "npm:7.7.0" tslib: "npm:2.8.1" typescript: "npm:5.7.3" languageName: unknown @@ -3713,7 +3713,7 @@ __metadata: rxjs: "npm:7.8.1" sass: "npm:1.83.4" sass-loader: "npm:16.0.4" - semver: "npm:7.6.3" + semver: "npm:7.7.0" style-loader: "npm:4.0.0" testing-library-selector: "npm:0.3.1" ts-node: "npm:10.9.2" @@ -18095,7 +18095,7 @@ __metadata: sass: "npm:1.83.4" sass-loader: "npm:16.0.4" selecto: "npm:1.26.3" - semver: "npm:7.6.3" + semver: "npm:7.7.0" slate: "npm:0.47.9" slate-plain-serializer: "npm:0.7.13" slate-react: "npm:0.22.10" @@ -27855,12 +27855,12 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.6.3, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3": - version: 7.6.3 - resolution: "semver@npm:7.6.3" +"semver@npm:7.7.0, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3": + version: 7.7.0 + resolution: "semver@npm:7.7.0" bin: semver: bin/semver.js - checksum: 10/36b1fbe1a2b6f873559cd57b238f1094a053dbfd997ceeb8757d79d1d2089c56d1321b9f1069ce263dc64cfa922fa1d2ad566b39426fe1ac6c723c1487589e10 + checksum: 10/5d615860a54ff563955c451e467bff3aaf74c8d060489f936f25551d5ca05f5ac683eb46c9ed7ade082e1e53b313f205ed9c5df0b25ebb3517ec25c79e1f0d9c languageName: node linkType: hard From ec836f2760bc3b0ff421ec6ab9e5a0e96d48a35f Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Fri, 31 Jan 2025 10:39:08 +0100 Subject: [PATCH 255/894] Chore: Code cleaning with unit tests in promlib (#99542) * update request tests * remove unused functions * add unit tests * remove commented code * fix unit tests --- pkg/promlib/library.go | 17 --- pkg/promlib/querydata/framing_bench_test.go | 4 +- pkg/promlib/querydata/framing_test.go | 2 +- pkg/promlib/querydata/request_test.go | 96 +++++++++++------ pkg/promlib/resource/resource.go | 9 -- pkg/promlib/resource/resource_test.go | 109 ++++++++++++++++++++ 6 files changed, 177 insertions(+), 60 deletions(-) create mode 100644 pkg/promlib/resource/resource_test.go diff --git a/pkg/promlib/library.go b/pkg/promlib/library.go index 596a6f61c02..cbee4a19ffc 100644 --- a/pkg/promlib/library.go +++ b/pkg/promlib/library.go @@ -2,7 +2,6 @@ package promlib import ( "context" - "errors" "fmt" "strings" @@ -11,7 +10,6 @@ import ( sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" "github.com/grafana/grafana/pkg/promlib/client" "github.com/grafana/grafana/pkg/promlib/instrumentation" @@ -138,18 +136,3 @@ func (s *Service) getInstance(ctx context.Context, pluginCtx backend.PluginConte in := i.(instance) return &in, nil } - -// IsAPIError returns whether err is or wraps a Prometheus error. -func IsAPIError(err error) bool { - // Check if the right error type is in err's chain. - var e *apiv1.Error - return errors.As(err, &e) -} - -func ConvertAPIError(err error) error { - var e *apiv1.Error - if errors.As(err, &e) { - return fmt.Errorf("%s: %s", e.Msg, e.Detail) - } - return err -} diff --git a/pkg/promlib/querydata/framing_bench_test.go b/pkg/promlib/querydata/framing_bench_test.go index b769df2b47d..411c8644e71 100644 --- a/pkg/promlib/querydata/framing_bench_test.go +++ b/pkg/promlib/querydata/framing_bench_test.go @@ -43,7 +43,7 @@ func BenchmarkExemplarJson(b *testing.B) { StatusCode: 200, Body: io.NopCloser(bytes.NewReader(responseBytes)), } - tCtx.httpProvider.setResponse(&res) + tCtx.httpProvider.setResponse(&res, &res) resp, err := tCtx.queryData.Execute(context.Background(), query) require.NoError(b, err) for _, r := range resp.Responses { @@ -74,7 +74,7 @@ func BenchmarkRangeJson(b *testing.B) { StatusCode: 200, Body: io.NopCloser(bytes.NewReader(body)), } - tCtx.httpProvider.setResponse(&res) + tCtx.httpProvider.setResponse(&res, &res) r, err = tCtx.queryData.Execute(context.Background(), q) require.NoError(b, err) } diff --git a/pkg/promlib/querydata/framing_test.go b/pkg/promlib/querydata/framing_test.go index 3668438ded4..3fa48bca92b 100644 --- a/pkg/promlib/querydata/framing_test.go +++ b/pkg/promlib/querydata/framing_test.go @@ -149,6 +149,6 @@ func runQuery(response []byte, q *backend.QueryDataRequest) (*backend.QueryDataR StatusCode: 200, Body: io.NopCloser(bytes.NewReader(response)), } - tCtx.httpProvider.setResponse(res) + tCtx.httpProvider.setResponse(res, res) return tCtx.queryData.Execute(context.Background(), q) } diff --git a/pkg/promlib/querydata/request_test.go b/pkg/promlib/querydata/request_test.go index 0977b9036d3..bfbde8dfb96 100644 --- a/pkg/promlib/querydata/request_test.go +++ b/pkg/promlib/querydata/request_test.go @@ -27,7 +27,6 @@ import ( func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { t.Run("exemplars response should be sampled and parsed normally", func(t *testing.T) { - t.Skip() exemplars := []apiv1.ExemplarQueryResult{ { SeriesLabels: p.LabelSet{ @@ -60,6 +59,22 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { }, } + values := []p.SamplePair{ + {Value: 1, Timestamp: 1000}, + {Value: 4, Timestamp: 4000}, + {Value: 6, Timestamp: 7000}, + {Value: 8, Timestamp: 1100}, + } + rangeResult := queryResult{ + Type: p.ValMatrix, + Result: p.Matrix{ + &p.SampleStream{ + Metric: p.Metric{"app": "Application", "tag2": "tag2"}, + Values: values, + }, + }, + } + tctx, err := setup() require.NoError(t, err) @@ -76,20 +91,20 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { RefID: "A", JSON: b, } - res, err := execute(tctx, query, exemplars) + res, err := execute(tctx, query, exemplars, rangeResult) require.NoError(t, err) // Test fields - require.Len(t, res, 1) - // require.Equal(t, res[0].Name, "exemplar") + require.Len(t, res, 2) + require.Equal(t, res[0].Name, "exemplar") require.Equal(t, res[0].Fields[0].Name, "Time") require.Equal(t, res[0].Fields[1].Name, "Value") require.Len(t, res[0].Fields, 6) // Test correct values (sampled to 2) - require.Equal(t, res[0].Fields[1].Len(), 2) + require.Equal(t, res[0].Fields[1].Len(), 4) require.Equal(t, res[0].Fields[1].At(0), 0.009545445) - require.Equal(t, res[0].Fields[1].At(1), 0.003535405) + require.Equal(t, res[0].Fields[1].At(3), 0.003535405) }) t.Run("matrix response should be parsed normally", func(t *testing.T) { @@ -128,7 +143,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, result) + res, err := execute(tctx, query, result, nil) require.NoError(t, err) require.Len(t, res, 1) @@ -177,7 +192,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, result) + res, err := execute(tctx, query, result, nil) require.NoError(t, err) require.Len(t, res, 1) @@ -222,7 +237,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, result) + res, err := execute(tctx, query, result, nil) require.NoError(t, err) require.Len(t, res, 1) @@ -266,7 +281,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, result) + res, err := execute(tctx, query, result, nil) require.NoError(t, err) require.Equal(t, `{app="Application"}`, res[0].Fields[1].Config.DisplayNameFromDS) @@ -298,7 +313,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, qr) + res, err := execute(tctx, query, qr, nil) require.NoError(t, err) require.Len(t, res, 1) @@ -317,7 +332,6 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { }) t.Run("scalar response should be parsed normally", func(t *testing.T) { - t.Skip("TODO: implement scalar responses") qr := queryResult{ Type: p.ValScalar, Result: &p.Scalar{ @@ -339,14 +353,15 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } tctx, err := setup() require.NoError(t, err) - res, err := execute(tctx, query, qr) + res, err := execute(tctx, query, qr, nil) require.NoError(t, err) require.Len(t, res, 1) require.Len(t, res[0].Fields, 2) require.Len(t, res[0].Fields[0].Labels, 0) require.Equal(t, res[0].Fields[0].Name, "Time") - require.Equal(t, "1", res[0].Fields[1].Name) + require.Equal(t, "Value", res[0].Fields[1].Name) + require.Equal(t, float64(1), res[0].Fields[1].At(0)) // Ensure the timestamps are UTC zoned testValue := res[0].Fields[0].At(0) @@ -360,22 +375,29 @@ type queryResult struct { Result any `json:"result"` } -func executeWithHeaders(tctx *testContext, query backend.DataQuery, qr any, headers map[string]string) (data.Frames, error) { +func executeWithHeaders(tctx *testContext, query backend.DataQuery, rqr any, eqr any, headers map[string]string) (data.Frames, error) { req := backend.QueryDataRequest{ Queries: []backend.DataQuery{query}, Headers: headers, } - promRes, err := toAPIResponse(qr) - defer func() { - if err := promRes.Body.Close(); err != nil { - fmt.Println(fmt.Errorf("response body close error: %v", err)) - } - }() + rangeRes, err := toAPIResponse(rqr) if err != nil { return nil, err } - tctx.httpProvider.setResponse(promRes) + exemplarRes, err := toAPIResponse(eqr) + if err != nil { + return nil, err + } + defer func() { + if err := rangeRes.Body.Close(); err != nil { + fmt.Println(fmt.Errorf("rangeRes body close error: %v", err)) + } + if err := exemplarRes.Body.Close(); err != nil { + fmt.Println(fmt.Errorf("exemplarRes body close error: %v", err)) + } + }() + tctx.httpProvider.setResponse(rangeRes, exemplarRes) res, err := tctx.queryData.Execute(context.Background(), &req) if err != nil { @@ -385,8 +407,8 @@ func executeWithHeaders(tctx *testContext, query backend.DataQuery, qr any, head return res.Responses[req.Queries[0].RefID].Frames, nil } -func execute(tctx *testContext, query backend.DataQuery, qr any) (data.Frames, error) { - return executeWithHeaders(tctx, query, qr, map[string]string{}) +func execute(tctx *testContext, query backend.DataQuery, rqr any, eqr any) (data.Frames, error) { + return executeWithHeaders(tctx, query, rqr, eqr, map[string]string{}) } type apiResponse struct { @@ -426,7 +448,11 @@ func setup() (*testContext, error) { opts: httpclient.Options{ Timeouts: &httpclient.DefaultTimeoutOptions, }, - res: &http.Response{ + rangeRes: &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader([]byte(`{}`))), + }, + exemplarRes: &http.Response{ StatusCode: 200, Body: io.NopCloser(bytes.NewReader([]byte(`{}`))), }, @@ -456,9 +482,10 @@ func setup() (*testContext, error) { type fakeHttpClientProvider struct { httpclient.Provider - opts httpclient.Options - req *http.Request - res *http.Response + opts httpclient.Options + req *http.Request + rangeRes *http.Response + exemplarRes *http.Response } func (p *fakeHttpClientProvider) New(opts ...httpclient.Options) (*http.Client, error) { @@ -476,11 +503,18 @@ func (p *fakeHttpClientProvider) GetTransport(opts ...httpclient.Options) (http. return http.DefaultTransport, nil } -func (p *fakeHttpClientProvider) setResponse(res *http.Response) { - p.res = res +func (p *fakeHttpClientProvider) setResponse(rangeRes *http.Response, exemplarRes *http.Response) { + p.rangeRes = rangeRes + p.exemplarRes = exemplarRes } func (p *fakeHttpClientProvider) RoundTrip(req *http.Request) (*http.Response, error) { p.req = req - return p.res, nil + switch req.URL.Path { + case "/api/v1/query_range", "/api/v1/query": + return p.rangeRes, nil + case "/api/v1/query_exemplars": + return p.exemplarRes, nil + } + return nil, fmt.Errorf("no such path: %s", req.URL.Path) } diff --git a/pkg/promlib/resource/resource.go b/pkg/promlib/resource/resource.go index 56988d10805..0673a5e0b41 100644 --- a/pkg/promlib/resource/resource.go +++ b/pkg/promlib/resource/resource.go @@ -83,15 +83,6 @@ func (r *Resource) Execute(ctx context.Context, req *backend.CallResourceRequest return callResponse, err } -func (r *Resource) DetectVersion(ctx context.Context, req *backend.CallResourceRequest) (*backend.CallResourceResponse, error) { - newReq := &backend.CallResourceRequest{ - PluginContext: req.PluginContext, - Path: "/api/v1/status/buildinfo", - } - - return r.Execute(ctx, newReq) -} - func getSelectors(expr string) ([]string, error) { parsed, err := parser.ParseExpr(expr) if err != nil { diff --git a/pkg/promlib/resource/resource_test.go b/pkg/promlib/resource/resource_test.go new file mode 100644 index 00000000000..a9b7ed21b60 --- /dev/null +++ b/pkg/promlib/resource/resource_test.go @@ -0,0 +1,109 @@ +package resource_test + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/promlib/resource" +) + +type mockRoundTripper struct { + Response *http.Response + Err error +} + +func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return m.Response, m.Err +} + +func setup() (*http.Client, backend.DataSourceInstanceSettings, log.Logger) { + // Mock HTTP Response + mockResponse := &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader([]byte(`{"message": "success"}`))), + Header: make(http.Header), + } + + // Create a mock RoundTripper + mockTransport := &mockRoundTripper{ + Response: mockResponse, + } + + // Create a mock HTTP client using the mock RoundTripper + mockClient := &http.Client{ + Transport: mockTransport, + } + + settings := backend.DataSourceInstanceSettings{ + ID: 1, + URL: "http://mock-server", + JSONData: []byte(`{}`), + } + + logger := log.DefaultLogger + + return mockClient, settings, logger +} + +func TestNewResource(t *testing.T) { + mockClient, settings, logger := setup() + res, err := resource.New(mockClient, settings, logger) + require.NoError(t, err) + assert.NotNil(t, res) +} + +func TestResource_Execute(t *testing.T) { + mockClient, settings, logger := setup() + res, err := resource.New(mockClient, settings, logger) + require.NoError(t, err) + + req := &backend.CallResourceRequest{ + URL: "/test", + } + ctx := context.Background() + + resp, err := res.Execute(ctx, req) + require.NoError(t, err) + assert.NotNil(t, resp) +} + +func TestResource_GetSuggestions(t *testing.T) { + mockClient, _, logger := setup() + settings := backend.DataSourceInstanceSettings{ + ID: 1, + URL: "http://localhost:9090", + JSONData: []byte(`{"httpMethod": "GET"}`), + } + + res, err := resource.New(mockClient, settings, logger) + require.NoError(t, err) + + suggestionReq := resource.SuggestionRequest{ + LabelName: "instance", + Queries: []string{"up"}, + Start: "1609459200", + End: "1609462800", + Limit: 10, + } + + body, err := json.Marshal(suggestionReq) + require.NoError(t, err) + + req := &backend.CallResourceRequest{ + Body: body, + } + ctx := context.Background() + + resp, err := res.GetSuggestions(ctx, req) + require.NoError(t, err) + assert.NotNil(t, resp) +} From 1458f5d0fb7bd9afd4d8d3db4cd9010a4c7db974 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Fri, 31 Jan 2025 10:39:22 +0100 Subject: [PATCH 256/894] Prometheus: Use the timerange in languageProvider when it's not provided (#99699) use the timerange in languageProvider --- .../grafana-prometheus/src/datasource.test.ts | 17 +++++++++++++++++ packages/grafana-prometheus/src/datasource.ts | 6 ++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/grafana-prometheus/src/datasource.test.ts b/packages/grafana-prometheus/src/datasource.test.ts index 54cdf88f8c6..1e11c3b54d8 100644 --- a/packages/grafana-prometheus/src/datasource.test.ts +++ b/packages/grafana-prometheus/src/datasource.test.ts @@ -1029,6 +1029,23 @@ describe('PrometheusDatasource', () => { expect(interval).toEqual({ text: '15s', value: '15s' }); expect(intervalMs).toEqual({ text: 15000, value: 15000 }); }); + + it('should use the default time range when no range provided in options', () => { + const prometheusDatasource = new PrometheusDatasource( + { ...instanceSettings, jsonData: { ...instanceSettings.jsonData, cacheLevel: PrometheusCacheLevel.None } }, + templateSrvStub + ); + const query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))'; + prometheusDatasource.metricFindQuery(query); + + // Last 6h + const range = replaceMock.mock.calls[1][1].__range; + const rangeMs = replaceMock.mock.calls[1][1].__range_ms; + const rangeS = replaceMock.mock.calls[1][1].__range_s; + expect(range).toEqual({ text: '21600s', value: '21600s' }); + expect(rangeMs).toEqual({ text: 21600000, value: 21600000 }); + expect(rangeS).toEqual({ text: 21600, value: 21600 }); + }); }); }); diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index a54475cebdd..cb4c80f0afd 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -456,13 +456,15 @@ export class PrometheusDatasource return Promise.resolve([]); } + const timeRange = options?.range ?? this.languageProvider.timeRange ?? getDefaultTimeRange(); + const scopedVars = { ...this.getIntervalVars(), - ...this.getRangeScopedVars(options?.range ?? getDefaultTimeRange()), + ...this.getRangeScopedVars(timeRange), }; const interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr); const metricFindQuery = new PrometheusMetricFindQuery(this, interpolated); - return metricFindQuery.process(options?.range ?? getDefaultTimeRange()); + return metricFindQuery.process(timeRange); } getIntervalVars() { From 7190bfb0ca675fc1b3b5d7ddf7e0ee9d1c9ca3d7 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:53:13 +0100 Subject: [PATCH 257/894] DashboardVariables: Use Combobox behind feature flag (#98261) * Add feature toggle * Use feature toggle * Remove usage of renderWithCombobox --- .../configure-grafana/feature-toggles/index.md | 1 + .../grafana-data/src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 +++++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++++ pkg/services/featuremgmt/toggles_gen.json | 13 +++++++++++++ 6 files changed, 27 insertions(+) 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 fecc19478b7..8dae19485ea 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -229,6 +229,7 @@ Experimental features might be changed or removed without prior notice. | `k8SFolderCounts` | Enable folder's api server counts | | `k8SFolderMove` | Enable folder's api server move | | `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams | +| `templateVariablesUsesCombobox` | Use new combobox component for template variables | | `queryLibraryDashboards` | Enables Query Library feature in Dashboards | | `grafanaAdvisor` | Enables Advisor app | | `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 6ab637d841e..f983b75f5fe 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -248,6 +248,7 @@ export interface FeatureToggles { improvedExternalSessionHandlingSAML?: boolean; teamHttpHeadersMimir?: boolean; ABTestFeatureToggleA?: boolean; + templateVariablesUsesCombobox?: boolean; ABTestFeatureToggleB?: boolean; queryLibraryDashboards?: boolean; grafanaAdvisor?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index d37b417b34e..340946621be 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1722,6 +1722,13 @@ var ( Expression: "false", HideFromDocs: true, }, + { + Name: "templateVariablesUsesCombobox", + Description: "Use new combobox component for template variables", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendPlatformSquad, + FrontendOnly: true, + }, { Name: "ABTestFeatureToggleB", Description: "Test feature toggle to see how cohorts could be set up AB testing", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 6ae52ca70d5..965d4b1fbe5 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -229,6 +229,7 @@ k8SFolderMove,experimental,@grafana/search-and-storage,false,false,false improvedExternalSessionHandlingSAML,preview,@grafana/identity-access-team,false,false,false teamHttpHeadersMimir,experimental,@grafana/identity-access-team,false,false,false ABTestFeatureToggleA,experimental,@grafana/sharing-squad,false,false,false +templateVariablesUsesCombobox,experimental,@grafana/grafana-frontend-platform,false,false,true ABTestFeatureToggleB,experimental,@grafana/sharing-squad,false,false,false queryLibraryDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false grafanaAdvisor,experimental,@grafana/plugins-platform-backend,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 0a6e5c4d0b4..64e4b7b2fc3 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -927,6 +927,10 @@ const ( // Test feature toggle to see how cohorts could be set up AB testing FlagABTestFeatureToggleA = "ABTestFeatureToggleA" + // FlagTemplateVariablesUsesCombobox + // Use new combobox component for template variables + FlagTemplateVariablesUsesCombobox = "templateVariablesUsesCombobox" + // FlagABTestFeatureToggleB // Test feature toggle to see how cohorts could be set up AB testing FlagABTestFeatureToggleB = "ABTestFeatureToggleB" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 1e79164d138..5c3dcb7040e 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3716,6 +3716,19 @@ "codeowner": "@grafana/identity-access-team" } }, + { + "metadata": { + "name": "templateVariablesUsesCombobox", + "resourceVersion": "1738141787383", + "creationTimestamp": "2025-01-29T09:09:47Z" + }, + "spec": { + "description": "Use new combobox component for template variables", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true + } + }, { "metadata": { "name": "timeRangeProvider", From a2b1a85dc4169e02087ec1a49af1e104c92d3f41 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:06:38 +0100 Subject: [PATCH 258/894] Update dependency @types/diff to v7.0.1 (#99864) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0d52aee116a..b2f46f4eff3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9374,9 +9374,9 @@ __metadata: linkType: hard "@types/diff@npm:^7.0.0": - version: 7.0.0 - resolution: "@types/diff@npm:7.0.0" - checksum: 10/11464fe70a81a0d02fdaad4ff4b0a0a5ac71825fa32dde9a98a1a82b1cac50dda302fb1621bea71db6c254e00ce002ea14543cc3502ac44f012598ca2c0fbcae + version: 7.0.1 + resolution: "@types/diff@npm:7.0.1" + checksum: 10/ef8c5fe0ea56737e8967c3db5e78518134f704e8e2971cb4ba6f2ed46ed5e13b4bbe41a6b2b78122b8f47c618ac25550f85a681633636a3a020369042523295d languageName: node linkType: hard From d062453c499fcb4874b8ee3884e53ec1c9bc2228 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Fri, 31 Jan 2025 11:46:38 +0100 Subject: [PATCH 259/894] Prometheus: Don't show expand rules warning for unique rule (#99540) * fix recording rule query hints * don't show the hint twice in code editor * provide testing rules --- devenv/docker/blocks/prometheus/recording.yml | 22 +++ .../src/query_hints.test.ts | 157 ++++++++++++++++++ .../grafana-prometheus/src/query_hints.ts | 11 +- .../components/PromQueryCodeEditor.tsx | 2 - 4 files changed, 189 insertions(+), 3 deletions(-) diff --git a/devenv/docker/blocks/prometheus/recording.yml b/devenv/docker/blocks/prometheus/recording.yml index f45cdbeddf2..2b8ae3e477a 100644 --- a/devenv/docker/blocks/prometheus/recording.yml +++ b/devenv/docker/blocks/prometheus/recording.yml @@ -1,4 +1,18 @@ groups: + - name: TESTING_RULES + rules: + - record: test:rate5m + expr: rate(ismail_prometheus_tsdb_reloads_total{job="prometheus"}[5m]) + labels: + identifier: special_idx + - record: agent:test:rate5m + expr: rate(prometheus_tsdb_reloads_total{job="prometheus"}[5m]) + labels: + identifier: special_idx + - record: agent:test:rate5m_agg_ + expr: rate(prometheus_tsdb_reloads_total_agg_{job="aggregated"}[5m]) + labels: + identifier: special_idx - name: RECORDING_RULES rules: - record: instance_path:requests:rate5m @@ -9,6 +23,14 @@ groups: expr: rate(prometheus_tsdb_reloads_failures_total{job="prometheus"}[5m]) - record: instance_path:reloads:rate5m expr: rate(prometheus_tsdb_reloads_total{job="prometheus"}[5m]) + - record: test:rate5m + expr: rate(prometheus_tsdb_reloads_total{job="prometheus"}[5m]) + - record: test:rate5m_agg_ + expr: rate(prometheus_tsdb_reloads_total_agg_{job="aggregated"}[5m]) + - record: agent:test:rate5m + expr: rate(prometheus_tsdb_reloads_total{job="prometheus"}[5m]) + - record: agent:test:rate5m_agg_ + expr: rate(prometheus_tsdb_reloads_total_agg_{job="aggregated"}[5m]) - record: instance_path:request_failures_per_requests:ratio_rate5m expr: |2 instance_path:reloads_failures:rate5m{job="prometheus"} diff --git a/packages/grafana-prometheus/src/query_hints.test.ts b/packages/grafana-prometheus/src/query_hints.test.ts index 5aabdbe7bd4..af2e9eee01a 100644 --- a/packages/grafana-prometheus/src/query_hints.test.ts +++ b/packages/grafana-prometheus/src/query_hints.test.ts @@ -8,6 +8,7 @@ import { getQueryHints, getQueryLabelsForRuleName, getRecordingRuleIdentifierIdx, + isRuleInQuery, SUM_HINT_THRESHOLD_COUNT, } from './query_hints'; import { buildVisualQueryFromString } from './querybuilder/parsing'; @@ -365,6 +366,92 @@ describe('getExpandRulesHints', () => { }, ]); }); + + it('should return expand rule hint, when given query include a non-unique rule name', () => { + const extractedMapping: RuleQueryMapping = { + 'duration:p95': [ + { + query: 'expanded_duration_p95{}', + labels: {}, + }, + { + query: 'expanded_duration_p95_aggregated{}', + labels: { + span_name: '__aggregated__', + }, + }, + ], + 'duration:p95:upper_threshold': [ + { + query: 'expanded_duration_p95_upper_threshold{}', + labels: {}, + }, + ], + }; + const query = 'sum(rate(duration:p95:upper_threshold{label="foo"}[5m])) by(bar)'; + const hints = getExpandRulesHints(query, extractedMapping); + expect(hints).toEqual([ + { + type: 'EXPAND_RULES', + label: 'Query contains recording rules.', + fix: { + label: 'Expand rules', + action: { + type: 'EXPAND_RULES', + query, + options: { + 'duration:p95:upper_threshold': { + expandedQuery: 'expanded_duration_p95_upper_threshold{}', + }, + }, + }, + }, + }, + ]); + }); + + it('should return expand rule hint, when given query include a non-unique rule name - second case', () => { + const extractedMapping: RuleQueryMapping = { + 'duration:p95': [ + { + query: 'expanded_duration_p95{}', + labels: {}, + }, + { + query: 'expanded_duration_p95_aggregated{}', + labels: { + span_name: '__aggregated__', + }, + }, + ], + 'upper_threshold:duration:p95': [ + { + query: 'expanded_duration_p95_upper_threshold{}', + labels: {}, + }, + ], + }; + const query = 'sum(rate(upper_threshold:duration:p95{label="foo"}[5m])) by(bar)'; + const hints = getExpandRulesHints(query, extractedMapping); + expect(hints).toEqual([ + { + type: 'EXPAND_RULES', + label: 'Query contains recording rules.', + fix: { + label: 'Expand rules', + action: { + type: 'EXPAND_RULES', + query, + options: { + 'upper_threshold:duration:p95': { + expandedQuery: 'expanded_duration_p95_upper_threshold{}', + }, + }, + }, + }, + }, + ]); + }); }); describe('getRecordingRuleIdentifierIdx', () => { @@ -505,3 +592,73 @@ describe('getQueryLabelsForRuleName', () => { expect(result).toEqual(expected); }); }); + +describe('ruleInQuery', () => { + it('should return true when ruleName is present in the query', () => { + expect(isRuleInQuery('rate(http_requests_total{job="api"}[5m])', 'http_requests_total')).toBe(true); + }); + + it('should return false when ruleName is not present in the query', () => { + expect(isRuleInQuery('rate(cpu_usage{instance="localhost"}[5m])', 'http_requests_total')).toBe(false); + }); + + it('should return true for ruleName at the start of the query', () => { + expect(isRuleInQuery('http_requests_total{job="api"}', 'http_requests_total')).toBe(true); + }); + + it('should return true for ruleName at the end of the query', () => { + expect(isRuleInQuery('sum by (instance) (http_requests_total)', 'http_requests_total')).toBe(true); + expect(isRuleInQuery('sum(http_requests_total)', 'http_requests_total')).toBe(true); + expect(isRuleInQuery('http_requests_total', 'http_requests_total')).toBe(true); + }); + + it('should return true when ruleName is followed by spaces', () => { + expect(isRuleInQuery('http_requests_total { job="api" }', 'http_requests_total')).toBe(true); + }); + + it('should return false when ruleName is a substring of another metric', () => { + expect(isRuleInQuery('rate(http_requests_total_new{job="api"}[5m])', 'http_requests_total')).toBe(false); + }); + + it('should return false for escaped ruleName usage', () => { + expect(isRuleInQuery('rate(\"http_requests_total\"{job="api"}[5m])', 'http_requests_total')).toBe(false); + }); + + it('should return false when query is empty', () => { + expect(isRuleInQuery('', 'http_requests_total')).toBe(false); + }); + + it('should return false when ruleName is an empty string', () => { + expect(isRuleInQuery('rate(http_requests_total{job="api"}[5m])', '')).toBe(false); + }); + + it('should return false when both query and ruleName are empty', () => { + expect(isRuleInQuery('', '')).toBe(false); + }); + + it('should return true when used with binary operations', () => { + expect(isRuleInQuery('rate(http_requests_total{job="api"}[5m]) + my:rule', 'my:rule')).toBe(true); + expect(isRuleInQuery('rate(http_requests_total{job="api"}[5m])+my:rule', 'my:rule')).toBe(true); + }); + + it('should return true when ruleName is inside nested functions', () => { + expect(isRuleInQuery('sum(rate(http_requests_total[5m]))', 'http_requests_total')).toBe(true); + }); + + it('should return false when ruleName is part of a label value', () => { + expect(isRuleInQuery('http_requests_total_bytes{rule="http_requests_total"}', 'http_requests_total')).toBe(false); + }); + + it('should return true when ruleName contains special characters like colon', () => { + expect(isRuleInQuery('rate(my_namespace:http_requests_total[5m])', 'my_namespace:http_requests_total')).toBe(true); + }); + + it('should return false when ruleName appears within string literals', () => { + expect( + isRuleInQuery( + 'label_replace(http_requests_total, "label", "value", "instance", "http_requests_total")', + 'http_requests_total' + ) + ).toBe(false); + }); +}); diff --git a/packages/grafana-prometheus/src/query_hints.ts b/packages/grafana-prometheus/src/query_hints.ts index 3fcdd66e0d2..73c3e90960e 100644 --- a/packages/grafana-prometheus/src/query_hints.ts +++ b/packages/grafana-prometheus/src/query_hints.ts @@ -175,10 +175,19 @@ export function getInitHints(datasource: PrometheusDatasource): QueryHint[] { return hints; } +export function isRuleInQuery(query: string, ruleName: string) { + if (!query || !ruleName) { + return false; + } + + const getRuleRegex = new RegExp(`(? { - if (query.search(ruleName) === -1) { + if (!isRuleInQuery(query, ruleName)) { return acc; } diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx index 9ad1b409456..f6ea9df0e1c 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx @@ -7,7 +7,6 @@ import { useStyles2 } from '@grafana/ui'; import { PromQueryField } from '../../components/PromQueryField'; import { PromQueryEditorProps } from '../../components/types'; -import { QueryEditorHints } from '../shared/QueryEditorHints'; import { PromQueryBuilderExplained } from './PromQueryBuilderExplained'; @@ -35,7 +34,6 @@ export function PromQueryCodeEditor(props: PromQueryCodeEditorProps) { app={app} /> {showExplain && } -
); } From 71e78b49ab2eedcbac661a5ff63a70ead743d902 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:51:39 +0000 Subject: [PATCH 260/894] Update dependency stylelint to v16.14.1 (#99873) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index d887106e6ca..2761f67cf16 100644 --- a/package.json +++ b/package.json @@ -230,7 +230,7 @@ "sass-loader": "16.0.4", "smtp-tester": "^2.1.0", "style-loader": "4.0.0", - "stylelint": "16.13.2", + "stylelint": "16.14.1", "stylelint-config-sass-guidelines": "12.1.0", "terser-webpack-plugin": "5.3.11", "testing-library-selector": "0.3.1", diff --git a/yarn.lock b/yarn.lock index b2f46f4eff3..f4491e0a17d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18101,7 +18101,7 @@ __metadata: slate-react: "npm:0.22.10" smtp-tester: "npm:^2.1.0" style-loader: "npm:4.0.0" - stylelint: "npm:16.13.2" + stylelint: "npm:16.14.1" stylelint-config-sass-guidelines: "npm:12.1.0" swagger-ui-react: "npm:5.18.3" symbol-observable: "npm:4.0.0" @@ -18949,7 +18949,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^7.0.1": +"ignore@npm:^7.0.3": version: 7.0.3 resolution: "ignore@npm:7.0.3" checksum: 10/ce5e812af3acd6607a3fe0a9f9b5f01d53f009a5ace8cbf5b6491d05a481b55d65186e6a7eaa13126e93f15276bcf3d1e8d6ff3ce5549c312f9bb313fff33365 @@ -24754,7 +24754,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.5.1, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.4.49": +"postcss@npm:8.5.1, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.5.1": version: 8.5.1 resolution: "postcss@npm:8.5.1" dependencies: @@ -29320,9 +29320,9 @@ __metadata: languageName: node linkType: hard -"stylelint@npm:16.13.2, stylelint@npm:^16.8.2": - version: 16.13.2 - resolution: "stylelint@npm:16.13.2" +"stylelint@npm:16.14.1, stylelint@npm:^16.8.2": + version: 16.14.1 + resolution: "stylelint@npm:16.14.1" dependencies: "@csstools/css-parser-algorithms": "npm:^3.0.4" "@csstools/css-tokenizer": "npm:^3.0.3" @@ -29342,7 +29342,7 @@ __metadata: globby: "npm:^11.1.0" globjoin: "npm:^0.1.4" html-tags: "npm:^3.3.1" - ignore: "npm:^7.0.1" + ignore: "npm:^7.0.3" imurmurhash: "npm:^0.1.4" is-plain-object: "npm:^5.0.0" known-css-properties: "npm:^0.35.0" @@ -29351,7 +29351,7 @@ __metadata: micromatch: "npm:^4.0.8" normalize-path: "npm:^3.0.0" picocolors: "npm:^1.1.1" - postcss: "npm:^8.4.49" + postcss: "npm:^8.5.1" postcss-resolve-nested-selector: "npm:^0.1.6" postcss-safe-parser: "npm:^7.0.1" postcss-selector-parser: "npm:^7.0.0" @@ -29364,7 +29364,7 @@ __metadata: write-file-atomic: "npm:^5.0.1" bin: stylelint: bin/stylelint.mjs - checksum: 10/98385b53d3c822b3b764fe8ff2f7212717127ab40ca9fd34a83bc6e27b5240d4ea02f959e01d4eaf91c87480a0c787b07b837883d4b3ec44133cc7ca03c79b47 + checksum: 10/2c0c95e5ee77d946efdbe58573da75ff4b12bc89c220e78b69f0c2f94251f5e5a1096f5ab583f28f56ae14f298b1335e212d52fe066719d63b8a41d6cc8083b6 languageName: node linkType: hard From 3372720a529d63cca3112bd3ae4005cc01f3c48b Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:54:28 +0100 Subject: [PATCH 261/894] Tempo, Pyroscope and tracing: Replace deprecated `@grafana/experimental` with `@grafana/plugin-ui` (#99671) * Tempo: Replace deprecated @grafana/experimental with @grafana/plugin-ui * Add also pyroscope and tracing features * align version of package with main --- .../SpanFilters/SpanFiltersTags.tsx | 2 +- .../components/settings/SpanBarSettings.tsx | 2 +- .../ConfigEditor.tsx | 2 +- .../tempo/SearchTraceQLEditor/GroupByField.tsx | 2 +- .../tempo/SearchTraceQLEditor/TagsInput.tsx | 2 +- .../tempo/configuration/ConfigEditor.tsx | 16 ++++++++-------- .../tempo/configuration/StreamingSection.tsx | 2 +- .../tempo/configuration/TagLimitSettings.tsx | 2 +- public/app/plugins/datasource/tempo/package.json | 2 +- .../tempo/traceql/TempoQueryBuilderOptions.tsx | 2 +- yarn.lock | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx index 0aabff724a4..10fa8befd62 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useMount } from 'react-use'; import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { Input, Select, Stack, useStyles2 } from '@grafana/ui'; import { randomId, SearchProps, Tag } from '../../../useSearch'; diff --git a/public/app/features/explore/TraceView/components/settings/SpanBarSettings.tsx b/public/app/features/explore/TraceView/components/settings/SpanBarSettings.tsx index a612dee380d..11710cef528 100644 --- a/public/app/features/explore/TraceView/components/settings/SpanBarSettings.tsx +++ b/public/app/features/explore/TraceView/components/settings/SpanBarSettings.tsx @@ -7,7 +7,7 @@ import { toOption, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/plugin-ui'; import { InlineField, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui'; export interface SpanBarOptions { diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/ConfigEditor.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/ConfigEditor.tsx index af6467d7896..3b2e3cbb5d0 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/ConfigEditor.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/ConfigEditor.tsx @@ -9,7 +9,7 @@ import { ConnectionSettings, DataSourceDescription, convertLegacyAuthProps, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { Divider, diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx index 6e7ace1ca18..14134fec7ec 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { Alert, HorizontalGroup, InputActionMeta, Select, useStyles2 } from '@grafana/ui'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx index d5f69c33064..d9ecd049504 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; -import { AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton } from '@grafana/plugin-ui'; import { FetchError } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx index 7602cea1487..941bcaadceb 100644 --- a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx @@ -1,6 +1,13 @@ import { css } from '@emotion/css'; import { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data'; +import { + NodeGraphSection, + SpanBarSection, + TraceToLogsSection, + TraceToMetricsSection, + TraceToProfilesSection, +} from '@grafana/o11y-ds-frontend'; import { AdvancedHttpSettings, Auth, @@ -10,14 +17,7 @@ import { ConnectionSettings, convertLegacyAuthProps, DataSourceDescription, -} from '@grafana/experimental'; -import { - NodeGraphSection, - SpanBarSection, - TraceToLogsSection, - TraceToMetricsSection, - TraceToProfilesSection, -} from '@grafana/o11y-ds-frontend'; +} from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { SecureSocksProxySettings, useStyles2, Divider, Stack } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx b/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx index bba18945603..e3d63e1ae26 100644 --- a/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx +++ b/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx @@ -7,7 +7,7 @@ import { GrafanaTheme2, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { ConfigSection } from '@grafana/experimental'; +import { ConfigSection } from '@grafana/plugin-ui'; import { InlineFieldRow, InlineField, InlineSwitch, Alert, Stack, useStyles2 } from '@grafana/ui'; import { FeatureName, featuresToTempoVersion } from '../datasource'; diff --git a/public/app/plugins/datasource/tempo/configuration/TagLimitSettings.tsx b/public/app/plugins/datasource/tempo/configuration/TagLimitSettings.tsx index edda2b69462..33768a47b9b 100644 --- a/public/app/plugins/datasource/tempo/configuration/TagLimitSettings.tsx +++ b/public/app/plugins/datasource/tempo/configuration/TagLimitSettings.tsx @@ -6,7 +6,7 @@ import { GrafanaTheme2, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/experimental'; +import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/plugin-ui'; import { InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui'; export interface TagLimitOptions extends DataSourceJsonData { diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index af785f67a57..970b6048c84 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -7,11 +7,11 @@ "@emotion/css": "11.13.5", "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", - "@grafana/experimental": "2.1.6", "@grafana/lezer-logql": "0.2.6", "@grafana/lezer-traceql": "0.0.20", "@grafana/monaco-logql": "^0.0.8", "@grafana/o11y-ds-frontend": "workspace:*", + "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "workspace:*", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx index 5834a153594..a6d0ae61fcd 100644 --- a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { EditorField, EditorRow } from '@grafana/experimental'; +import { EditorField, EditorRow } from '@grafana/plugin-ui'; import { AutoSizeInput, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { QueryOptionGroup } from '../_importedDependencies/datasources/prometheus/QueryOptionGroup'; diff --git a/yarn.lock b/yarn.lock index f4491e0a17d..6786d1ee0b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3043,12 +3043,12 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "workspace:*" "@grafana/e2e-selectors": "workspace:*" - "@grafana/experimental": "npm:2.1.6" "@grafana/lezer-logql": "npm:0.2.6" "@grafana/lezer-traceql": "npm:0.0.20" "@grafana/monaco-logql": "npm:^0.0.8" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/plugin-configs": "npm:11.5.0-pre" + "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "workspace:*" "@grafana/schema": "workspace:*" "@grafana/ui": "workspace:*" From abc76f8aad6e2d55b34ed135d0afaa8d573d4582 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 31 Jan 2025 11:03:13 +0000 Subject: [PATCH 262/894] Chore: Change jsonMarkup.js to esm (#99269) --- .../components/TraceTimelineViewer/SpanDetail/jsonMarkup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js index 2dcbb1ee895..7db0889e1e6 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js @@ -67,7 +67,7 @@ function escape(str) { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } -module.exports = function (doc, styleFile) { +export default function jsonMarkup(doc, styleFile) { let indent = ''; const style = Stylize(styleFile); @@ -130,4 +130,4 @@ module.exports = function (doc, styleFile) { } return '
' + visit(doc) + '
'; -}; +} From 39f212a9652e2bb1c4a0dcd105612179d64438a2 Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 31 Jan 2025 12:43:02 +0100 Subject: [PATCH 263/894] Alerting: Call RLock() before reading sendAlertsTo map (#99812) * Alerting: Call RLock() before reading sendAlertsTo map * defer unlocking * drive-tru fix for another lock * less time holding the lock in SyncAndApplyConfigFromDatabase --- pkg/services/ngalert/sender/router.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pkg/services/ngalert/sender/router.go b/pkg/services/ngalert/sender/router.go index cfb474f63be..637a3c77f21 100644 --- a/pkg/services/ngalert/sender/router.go +++ b/pkg/services/ngalert/sender/router.go @@ -90,9 +90,11 @@ func (d *AlertsRouter) SyncAndApplyConfigFromDatabase(ctx context.Context) error d.logger.Debug("Attempting to sync admin configs", "count", len(cfgs)) disableExternal := d.featureManager.IsEnabled(ctx, featuremgmt.FlagAlertingDisableSendAlertsExternal) - orgsFound := make(map[int64]struct{}, len(cfgs)) + + // We're holding this lock either until we return an error or right before we stop the senders. d.adminConfigMtx.Lock() + for _, cfg := range cfgs { _, isDisabledOrg := d.disabledOrgs[cfg.OrgID] if isDisabledOrg { @@ -167,6 +169,7 @@ func (d *AlertsRouter) SyncAndApplyConfigFromDatabase(ctx context.Context) error senderLogger := log.New("ngalert.sender.external-alertmanager") s, err := NewExternalAlertmanagerSender(senderLogger, prometheus.NewRegistry()) if err != nil { + d.adminConfigMtx.Unlock() return err } d.externalAlertmanagers[cfg.OrgID] = s @@ -190,9 +193,9 @@ func (d *AlertsRouter) SyncAndApplyConfigFromDatabase(ctx context.Context) error delete(d.externalAlertmanagersCfgHash, orgID) } } - d.adminConfigMtx.Unlock() - // We can now stop these external Alertmanagers w/o having to hold a lock. + // We can now stop these senders w/o having to hold a lock. + d.adminConfigMtx.Unlock() for orgID, s := range sendersToStop { d.logger.Info("Stopping sender", "org", orgID) s.Stop() @@ -318,8 +321,10 @@ func (d *AlertsRouter) Send(ctx context.Context, key models.AlertRuleKey, alerts } // Send alerts to local notifier if they need to be handled internally // or if no external AMs have been discovered yet. + d.adminConfigMtx.RLock() + defer d.adminConfigMtx.RUnlock() var localNotifierExist, externalNotifierExist bool - if d.sendAlertsTo[key.OrgID] == models.ExternalAlertmanagers && len(d.AlertmanagersFor(key.OrgID)) > 0 { + if d.sendAlertsTo[key.OrgID] == models.ExternalAlertmanagers && len(d.alertmanagersFor(key.OrgID)) > 0 { logger.Debug("All alerts for the given org should be routed to external notifiers only. skipping the internal notifier.") } else { logger.Info("Sending alerts to local notifier", "count", len(alerts.PostableAlerts)) @@ -340,8 +345,6 @@ func (d *AlertsRouter) Send(ctx context.Context, key models.AlertRuleKey, alerts // Send alerts to external Alertmanager(s) if we have a sender for this organization // and alerts are not being handled just internally. - d.adminConfigMtx.RLock() - defer d.adminConfigMtx.RUnlock() s, ok := d.externalAlertmanagers[key.OrgID] if ok && d.sendAlertsTo[key.OrgID] != models.InternalAlertmanager { logger.Info("Sending alerts to external notifier", "count", len(alerts.PostableAlerts)) @@ -358,6 +361,10 @@ func (d *AlertsRouter) Send(ctx context.Context, key models.AlertRuleKey, alerts func (d *AlertsRouter) AlertmanagersFor(orgID int64) []*url.URL { d.adminConfigMtx.RLock() defer d.adminConfigMtx.RUnlock() + return d.alertmanagersFor(orgID) +} + +func (d *AlertsRouter) alertmanagersFor(orgID int64) []*url.URL { s, ok := d.externalAlertmanagers[orgID] if !ok { return []*url.URL{} From 95a19c21dfd7734e8642804c25be1f55fe7798db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:45:03 +0000 Subject: [PATCH 264/894] Update dependency @grafana/plugin-e2e to v1.17.1 (#99876) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 2761f67cf16..26bc506bbf6 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "@emotion/eslint-plugin": "11.12.0", "@grafana/eslint-config": "8.0.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", - "@grafana/plugin-e2e": "1.17.0", + "@grafana/plugin-e2e": "1.17.1", "@grafana/tsconfig": "^2.0.0", "@manypkg/get-packages": "^2.2.0", "@playwright/test": "1.50.0", diff --git a/yarn.lock b/yarn.lock index 6786d1ee0b2..832270a9a6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3547,17 +3547,17 @@ __metadata: languageName: unknown linkType: soft -"@grafana/plugin-e2e@npm:1.17.0": - version: 1.17.0 - resolution: "@grafana/plugin-e2e@npm:1.17.0" +"@grafana/plugin-e2e@npm:1.17.1": + version: 1.17.1 + resolution: "@grafana/plugin-e2e@npm:1.17.1" dependencies: - "@grafana/e2e-selectors": "npm:^11.5.0-220285" + "@grafana/e2e-selectors": "npm:^11.5.0-221187" semver: "npm:^7.5.4" uuid: "npm:^11.0.2" yaml: "npm:^2.3.4" peerDependencies: "@playwright/test": ^1.41.2 - checksum: 10/d24465857228cb9588777f92bd7bee8b5b7042a9375cb20a31b04e9af75b307248d3f17bd421eff0640ff9592612ef66e16f1e461af57ca7c2d680052fc02348 + checksum: 10/d98b06fa04f59e07a8385fd81e8a7b456332d431aa529a0eb748087524f8b7b1bee867e616e01e7d1842b9e3c8dd508e8122fd5fb4d5a0a25162a4657b3dd4c0 languageName: node linkType: hard @@ -17817,7 +17817,7 @@ __metadata: "@grafana/lezer-logql": "npm:0.2.7" "@grafana/monaco-logql": "npm:^0.0.8" "@grafana/o11y-ds-frontend": "workspace:*" - "@grafana/plugin-e2e": "npm:1.17.0" + "@grafana/plugin-e2e": "npm:1.17.1" "@grafana/plugin-ui": "npm:0.10.1" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" From 111f9732421eeaf70e52c6573178e73c60265f47 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:05:46 +0200 Subject: [PATCH 265/894] Update dependency react-zoom-pan-pinch to v3.7.0 (#99879) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 832270a9a6e..b2e5da33ae7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26697,12 +26697,12 @@ __metadata: linkType: hard "react-zoom-pan-pinch@npm:^3.3.0": - version: 3.6.1 - resolution: "react-zoom-pan-pinch@npm:3.6.1" + version: 3.7.0 + resolution: "react-zoom-pan-pinch@npm:3.7.0" peerDependencies: react: "*" react-dom: "*" - checksum: 10/9146aa5c427dd6d0c8a4ebe3db0c720718eef6262d1b4b36033ee433bc76a9c84e30ca91311211ab95446305d3e2813d9abc576d093efbf5562be984431896cb + checksum: 10/5ae7f1ffea86fd19ae57f7b4c6818b282e13e00523200d23759db95a1518333044017583c2af3c179b2634c88c76162aa0ce8de79cd26359f07339415842ba34 languageName: node linkType: hard From f51eacef9a7b293aa4b6d7a67a0e74601ea10506 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 31 Jan 2025 14:25:16 +0200 Subject: [PATCH 266/894] RTK APIs: Extract base query logic (#99800) * RTK APIs: Extract base query function * Add error handling * Add return type * Use createBaseQuery in browseDashboards * Support custom manageError * Export getConfigError * Remove redundant type * data -> body --- public/app/api/createBaseQuery.ts | 43 ++++++++++++++++ .../api/browseDashboardsAPI.ts | 49 +++++-------------- .../dashboard/api/publicDashboardApi.ts | 40 ++++----------- .../QueryLibrary/QueryTemplatesList.tsx | 2 +- .../features/migrate-to-cloud/api/baseAPI.ts | 35 ++----------- .../features/migrate-to-cloud/api/index.ts | 7 ++- .../features/preferences/api/user/baseAPI.ts | 33 ++----------- .../app/features/query-library/api/factory.ts | 6 ++- .../app/features/query-library/api/query.ts | 31 ------------ 9 files changed, 79 insertions(+), 167 deletions(-) create mode 100644 public/app/api/createBaseQuery.ts diff --git a/public/app/api/createBaseQuery.ts b/public/app/api/createBaseQuery.ts new file mode 100644 index 00000000000..29327703bf9 --- /dev/null +++ b/public/app/api/createBaseQuery.ts @@ -0,0 +1,43 @@ +import { BaseQueryFn } from '@reduxjs/toolkit/query'; +import { lastValueFrom } from 'rxjs'; + +import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime'; + +interface RequestOptions extends BackendSrvRequest { + manageError?: (err: unknown) => { error: unknown }; + body?: BackendSrvRequest['data']; +} + +export function createBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn { + async function backendSrvBaseQuery(requestOptions: RequestOptions) { + try { + const { data: responseData, ...meta } = await lastValueFrom( + getBackendSrv().fetch({ + ...requestOptions, + url: baseURL + requestOptions.url, + showErrorAlert: requestOptions.showErrorAlert ?? false, + data: requestOptions.body, + }) + ); + return { data: responseData, meta }; + } catch (error) { + if (requestOptions.manageError) { + return requestOptions.manageError(error); + } else { + return handleRequestError(error); + } + } + } + + return backendSrvBaseQuery; +} + +export function handleRequestError(error: unknown) { + if (isFetchError(error)) { + return { error: new Error(error.data.message) }; + } else if (error instanceof Error) { + return { error }; + } else { + return { error: new Error('Unknown error') }; + } +} diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index bbe79f9cdb3..e14cae0ef31 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -1,10 +1,10 @@ -import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react'; -import { lastValueFrom } from 'rxjs'; +import { createApi } from '@reduxjs/toolkit/query/react'; import { AppEvents, isTruthy, locationUtil } from '@grafana/data'; -import { BackendSrvRequest, config, getBackendSrv, locationService } from '@grafana/runtime'; +import { config, getBackendSrv, locationService } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; +import { createBaseQuery, handleRequestError } from 'app/api/createBaseQuery'; import appEvents from 'app/core/app_events'; import { contextSrv } from 'app/core/core'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; @@ -28,11 +28,6 @@ import { DashboardTreeSelection } from '../types'; import { PAGE_SIZE } from './services'; -interface RequestOptions extends BackendSrvRequest { - manageError?: (err: unknown) => { error: unknown }; - showErrorAlert?: boolean; -} - interface DeleteItemsArgs { selectedItems: Omit; } @@ -64,28 +59,6 @@ interface HardDeleteDashboardArgs { dashboardUID: string; } -function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn { - async function backendSrvBaseQuery(requestOptions: RequestOptions) { - // Suppress error pop-up for root (aka 'general') folder - const isGeneralFolder = requestOptions.url === `/folders/general`; - requestOptions = isGeneralFolder ? { ...requestOptions, showErrorAlert: false } : requestOptions; - - try { - const { data: responseData, ...meta } = await lastValueFrom( - getBackendSrv().fetch({ - ...requestOptions, - url: baseURL + requestOptions.url, - }) - ); - return { data: responseData, meta }; - } catch (error) { - return requestOptions.manageError ? requestOptions.manageError(error) : { error }; - } - } - - return backendSrvBaseQuery; -} - export interface ListFolderQueryArgs { page: number; parentUid: string | undefined; @@ -96,7 +69,7 @@ export interface ListFolderQueryArgs { export const browseDashboardsAPI = createApi({ tagTypes: ['getFolder'], reducerPath: 'browseDashboardsAPI', - baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }), + baseQuery: createBaseQuery({ baseURL: '/api' }), endpoints: (builder) => ({ listFolders: builder.query({ providesTags: (result) => result?.map((folder) => ({ type: 'getFolder', id: folder.uid })) ?? [], @@ -117,7 +90,7 @@ export const browseDashboardsAPI = createApi({ query: ({ title, parentUid }) => ({ method: 'POST', url: '/folders', - data: { + body: { title, parentUid, }, @@ -144,7 +117,7 @@ export const browseDashboardsAPI = createApi({ query: ({ uid, title, version }) => ({ method: 'PUT', url: `/folders/${uid}`, - data: { + body: { title, version, }, @@ -167,7 +140,7 @@ export const browseDashboardsAPI = createApi({ query: ({ folder, destinationUID }) => ({ url: `/folders/${folder.uid}/move`, method: 'POST', - data: { parentUID: destinationUID }, + body: { parentUID: destinationUID }, }), onQueryStarted: ({ folder, destinationUID }, { queryFulfilled, dispatch }) => { const { parentUid } = folder; @@ -256,7 +229,7 @@ export const browseDashboardsAPI = createApi({ await baseQuery({ url: `/folders/${folderUID}/move`, method: 'POST', - data: { parentUID: destinationUID }, + body: { parentUID: destinationUID }, }); } @@ -363,7 +336,7 @@ export const browseDashboardsAPI = createApi({ } throw new Error('Invalid dashboard version'); } catch (error) { - return { error }; + return handleRequestError(error); } }, @@ -385,7 +358,7 @@ export const browseDashboardsAPI = createApi({ query: ({ dashboard, overwrite, inputs, folderUid }) => ({ method: 'POST', url: '/dashboards/import', - data: { + body: { dashboard, overwrite, inputs, @@ -410,7 +383,7 @@ export const browseDashboardsAPI = createApi({ restoreDashboard: builder.mutation({ query: ({ dashboardUID, targetFolderUID }) => ({ url: `/dashboards/uid/${dashboardUID}/trash`, - data: { + body: { folderUid: targetFolderUID, }, method: 'PATCH', diff --git a/public/app/features/dashboard/api/publicDashboardApi.ts b/public/app/features/dashboard/api/publicDashboardApi.ts index 063b55e4d79..40bda98f4c5 100644 --- a/public/app/features/dashboard/api/publicDashboardApi.ts +++ b/public/app/features/dashboard/api/publicDashboardApi.ts @@ -1,7 +1,7 @@ -import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react'; -import { lastValueFrom } from 'rxjs'; +import { createApi } from '@reduxjs/toolkit/query/react'; -import { BackendSrvRequest, config, FetchError, getBackendSrv, isFetchError } from '@grafana/runtime/src'; +import { config, FetchError, isFetchError } from '@grafana/runtime/src'; +import { createBaseQuery } from 'app/api/createBaseQuery'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification'; import { t } from 'app/core/internationalization'; @@ -19,39 +19,17 @@ import { PublicDashboardListWithPaginationResponse, } from 'app/features/manage-dashboards/types'; -type ReqOptions = { - manageError?: (err: unknown) => { error: unknown }; - showErrorAlert?: boolean; -}; - function isFetchBaseQueryError(error: unknown): error is { error: FetchError } { return typeof error === 'object' && error != null && 'error' in error; } -const backendSrvBaseQuery = - ({ baseUrl }: { baseUrl: string }): BaseQueryFn => - async (requestOptions) => { - try { - const { data: responseData, ...meta } = await lastValueFrom( - getBackendSrv().fetch({ - ...requestOptions, - url: baseUrl + requestOptions.url, - showErrorAlert: requestOptions.showErrorAlert, - }) - ); - return { data: responseData, meta }; - } catch (error) { - return requestOptions.manageError ? requestOptions.manageError(error) : { error }; - } - }; - -const getConfigError = (err: unknown) => ({ +export const getConfigError = (err: unknown) => ({ error: isFetchError(err) && err.data.messageId !== 'publicdashboards.notFound' ? err : null, }); export const publicDashboardApi = createApi({ reducerPath: 'publicDashboardApi', - baseQuery: backendSrvBaseQuery({ baseUrl: '/api' }), + baseQuery: createBaseQuery({ baseURL: '/api' }), tagTypes: ['PublicDashboard', 'AuditTablePublicDashboard', 'UsersWithActiveSessions', 'ActiveUserDashboards'], refetchOnMountOrArgChange: true, endpoints: (builder) => ({ @@ -81,7 +59,7 @@ export const publicDashboardApi = createApi({ return { url: `/dashboards/uid/${dashUid}/public-dashboards`, method: 'POST', - data: params.payload, + body: params.payload, }; }, async onQueryStarted({ dashboard, payload: { share } }, { dispatch, queryFulfilled }) { @@ -121,7 +99,7 @@ export const publicDashboardApi = createApi({ return { url: `/dashboards/uid/${dashUid}/public-dashboards/${payload.uid}`, method: 'PATCH', - data: payload, + body: payload, }; }, async onQueryStarted({ dashboard }, { dispatch, queryFulfilled }) { @@ -153,7 +131,7 @@ export const publicDashboardApi = createApi({ return { url: `/dashboards/uid/${dashUid}/public-dashboards/${payload.uid}`, method: 'PATCH', - data: payload, + body: payload, }; }, async onQueryStarted({ dashboard, payload: { isEnabled } }, { dispatch, queryFulfilled }) { @@ -193,7 +171,7 @@ export const publicDashboardApi = createApi({ return { url: `/dashboards/uid/${dashUid}/public-dashboards/${payload.uid}`, method: 'PATCH', - data: payload, + body: payload, }; }, async onQueryStarted({ dashboard, payload: { share } }, { dispatch, queryFulfilled }) { diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx index 54dc5951b88..076e1664cb4 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx @@ -53,7 +53,7 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) { return uniqBy(loadQueryMetadataResult.value, 'datasourceName').map((row) => row.datasourceName); }, [loadQueryMetadataResult.value]); - if (error) { + if (error instanceof Error) { return ( {error.message} diff --git a/public/app/features/migrate-to-cloud/api/baseAPI.ts b/public/app/features/migrate-to-cloud/api/baseAPI.ts index 6a140906631..60c18dfd3e3 100644 --- a/public/app/features/migrate-to-cloud/api/baseAPI.ts +++ b/public/app/features/migrate-to-cloud/api/baseAPI.ts @@ -1,38 +1,9 @@ -import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react'; -import { lastValueFrom } from 'rxjs'; +import { createApi } from '@reduxjs/toolkit/query/react'; -import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime'; - -interface RequestOptions extends BackendSrvRequest { - manageError?: (err: unknown) => { error: unknown }; - showErrorAlert?: boolean; - - // rtk codegen sets this - body?: BackendSrvRequest['data']; -} - -function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn { - async function backendSrvBaseQuery(requestOptions: RequestOptions) { - try { - const { data: responseData, ...meta } = await lastValueFrom( - getBackendSrv().fetch({ - ...requestOptions, - url: baseURL + requestOptions.url, - showErrorAlert: false, - data: requestOptions.body, - }) - ); - return { data: responseData, meta }; - } catch (error) { - return requestOptions.manageError ? requestOptions.manageError(error) : { error }; - } - } - - return backendSrvBaseQuery; -} +import { createBaseQuery } from 'app/api/createBaseQuery'; export const baseAPI = createApi({ reducerPath: 'migrateToCloudGeneratedAPI', - baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }), + baseQuery: createBaseQuery({ baseURL: '/api' }), endpoints: () => ({}), }); diff --git a/public/app/features/migrate-to-cloud/api/index.ts b/public/app/features/migrate-to-cloud/api/index.ts index 60a2d7a7b87..0e5c1d8b453 100644 --- a/public/app/features/migrate-to-cloud/api/index.ts +++ b/public/app/features/migrate-to-cloud/api/index.ts @@ -1,11 +1,14 @@ -export * from './endpoints.gen'; import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/query'; import { getLocalPlugins } from 'app/features/plugins/admin/api'; import { LocalPlugin } from 'app/features/plugins/admin/types'; +import { handleRequestError } from '../../../api/createBaseQuery'; + import { generatedAPI } from './endpoints.gen'; +export * from './endpoints.gen'; + export const cloudMigrationAPI = generatedAPI .injectEndpoints({ endpoints: (build) => ({ @@ -16,7 +19,7 @@ export const cloudMigrationAPI = generatedAPI const list = await getLocalPlugins(); return { data: list }; } catch (error) { - return { error: error }; + return handleRequestError(error); } }, }), diff --git a/public/app/features/preferences/api/user/baseAPI.ts b/public/app/features/preferences/api/user/baseAPI.ts index b1b75e7b036..33316419f6e 100644 --- a/public/app/features/preferences/api/user/baseAPI.ts +++ b/public/app/features/preferences/api/user/baseAPI.ts @@ -1,36 +1,9 @@ -import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react'; -import { lastValueFrom } from 'rxjs'; +import { createApi } from '@reduxjs/toolkit/query/react'; -import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime'; - -interface RequestOptions extends BackendSrvRequest { - manageError?: (err: unknown) => { error: unknown }; - showErrorAlert?: boolean; - body?: BackendSrvRequest['data']; -} - -function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn { - async function backendSrvBaseQuery(requestOptions: RequestOptions) { - try { - const { data: responseData, ...meta } = await lastValueFrom( - getBackendSrv().fetch({ - ...requestOptions, - url: baseURL + requestOptions.url, - showErrorAlert: requestOptions.showErrorAlert, - data: requestOptions.body, - }) - ); - return { data: responseData, meta }; - } catch (error) { - return requestOptions.manageError ? requestOptions.manageError(error) : { error }; - } - } - - return backendSrvBaseQuery; -} +import { createBaseQuery } from 'app/api/createBaseQuery'; export const baseAPI = createApi({ reducerPath: 'userPreferencesAPI', - baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }), + baseQuery: createBaseQuery({ baseURL: '/api' }), endpoints: () => ({}), }); diff --git a/public/app/features/query-library/api/factory.ts b/public/app/features/query-library/api/factory.ts index 00e6115db7d..d03074092d6 100644 --- a/public/app/features/query-library/api/factory.ts +++ b/public/app/features/query-library/api/factory.ts @@ -1,12 +1,14 @@ import { createApi } from '@reduxjs/toolkit/query/react'; -import { baseQuery } from './query'; +import { createBaseQuery } from '../../../api/createBaseQuery'; + +import { BASE_URL } from './query'; // Currently, we are loading all query templates // Organizations can have maximum of 1000 query templates export const QUERY_LIBRARY_GET_LIMIT = 1000; export const queryLibraryApi = createApi({ - baseQuery, + baseQuery: createBaseQuery({ baseURL: BASE_URL }), endpoints: () => ({}), }); diff --git a/public/app/features/query-library/api/query.ts b/public/app/features/query-library/api/query.ts index 610c1556387..d0cc6db0209 100644 --- a/public/app/features/query-library/api/query.ts +++ b/public/app/features/query-library/api/query.ts @@ -1,8 +1,3 @@ -import { BaseQueryFn } from '@reduxjs/toolkit/query/react'; -import { lastValueFrom } from 'rxjs'; - -import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime/src/services/backendSrv'; - import { getAPINamespace } from '../../../api/utils'; /** @@ -23,29 +18,3 @@ export enum QueryTemplateKinds { * @alpha */ export const BASE_URL = `/apis/${API_VERSION}/namespaces/${getAPINamespace()}`; - -interface QueryLibraryBackendRequest extends BackendSrvRequest { - body?: BackendSrvRequest['data']; -} - -export const baseQuery: BaseQueryFn = async (requestOptions) => { - try { - const responseObservable = getBackendSrv().fetch({ - url: `${BASE_URL}/${requestOptions.url ?? ''}`, - showErrorAlert: true, - method: requestOptions.method || 'GET', - data: requestOptions.body, - params: requestOptions.params, - headers: { ...requestOptions.headers }, - }); - return await lastValueFrom(responseObservable); - } catch (error) { - if (isFetchError(error)) { - return { error: new Error(error.data.message) }; - } else if (error instanceof Error) { - return { error }; - } else { - return { error: new Error('Unknown error') }; - } - } -}; From 05f039d5ab65176175267927e3fe5abee1c9816c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:49:10 +0000 Subject: [PATCH 267/894] Update dependency type-fest to v4.33.0 (#99883) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b2e5da33ae7..ed053bc653d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30361,9 +30361,9 @@ __metadata: linkType: hard "type-fest@npm:^4.18.2, type-fest@npm:^4.26.1": - version: 4.30.2 - resolution: "type-fest@npm:4.30.2" - checksum: 10/c5168b159c366e4fd5b74c7f7b786bed9248c03f67e6e07d52dd5d51354447468fa7c92b9f2142c7fe9279814031f783959370242c3520de848931b65ddb48bb + version: 4.33.0 + resolution: "type-fest@npm:4.33.0" + checksum: 10/0d179e66fa765bd0a25a785b12dc797f90f2f92bdb8c9c8a789f3fd8e5a4492444e7ef83551b3b8463aeab24fd6195761e26b03174722de636b4b75aa5726fb7 languageName: node linkType: hard From 74b04a237dc0d17997569637f0106b43b298560e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:07:05 +0200 Subject: [PATCH 268/894] Update dependency yaml to v2.7.0 (#99887) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index ed053bc653d..2c6b0370c1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31925,11 +31925,11 @@ __metadata: linkType: hard "yaml@npm:^2.0.0, yaml@npm:^2.3.4": - version: 2.6.1 - resolution: "yaml@npm:2.6.1" + version: 2.7.0 + resolution: "yaml@npm:2.7.0" bin: yaml: bin.mjs - checksum: 10/cf412f03a33886db0a3aac70bb4165588f4c5b3c6f8fc91520b71491e5537800b6c2c73ed52015617f6e191eb4644c73c92973960a1999779c62a200ee4c231d + checksum: 10/c8c314c62fbd49244a6a51b06482f6d495b37ab10fa685fcafa1bbaae7841b7233ee7d12cab087bcca5a0b28adc92868b6e437322276430c28d00f1c1732eeec languageName: node linkType: hard From 9ae5552cab9749c1372002fe3bd67cb88507d4c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 13:30:24 +0000 Subject: [PATCH 269/894] Update testing-library monorepo (#99889) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +- packages/grafana-flamegraph/package.json | 4 +- .../grafana-o11y-ds-frontend/package.json | 4 +- packages/grafana-prometheus/package.json | 4 +- packages/grafana-runtime/package.json | 4 +- packages/grafana-sql/package.json | 4 +- packages/grafana-ui/package.json | 4 +- .../datasource/azuremonitor/package.json | 4 +- .../datasource/cloud-monitoring/package.json | 4 +- .../package.json | 4 +- .../grafana-pyroscope-datasource/package.json | 4 +- .../grafana-testdata-datasource/package.json | 4 +- .../plugins/datasource/jaeger/package.json | 4 +- .../app/plugins/datasource/mssql/package.json | 4 +- .../app/plugins/datasource/mysql/package.json | 4 +- .../app/plugins/datasource/parca/package.json | 4 +- .../app/plugins/datasource/tempo/package.json | 4 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 86 +++++++++---------- 19 files changed, 78 insertions(+), 78 deletions(-) diff --git a/package.json b/package.json index 26bc506bbf6..ff42cadd8ae 100644 --- a/package.json +++ b/package.json @@ -96,8 +96,8 @@ "@swc/helpers": "0.5.15", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/angular": "1.8.9", "@types/angular-route": "1.7.6", "@types/babel__core": "^7", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index e48e6113c6b..fd9c092b5e7 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -63,8 +63,8 @@ "@rollup/plugin-node-resolve": "16.0.0", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.17.15", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 9cb9025d02f..903b3d61b26 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -33,8 +33,8 @@ "@grafana/tsconfig": "^2.0.0", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/jest": "^29.5.4", "@types/node": "22.12.0", "@types/react": "18.3.18", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index a27c1776ec7..964f0ba834c 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -85,8 +85,8 @@ "@swc/helpers": "0.5.15", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/d3": "7.4.3", "@types/debounce-promise": "3.1.9", "@types/eslint": "9.6.1", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 56876aab254..21c58965847 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -53,8 +53,8 @@ "@rollup/plugin-node-resolve": "16.0.0", "@rollup/plugin-terser": "0.4.4", "@testing-library/dom": "10.4.0", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/angular": "1.8.9", "@types/history": "4.7.11", "@types/jest": "29.5.14", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index e868769944f..b737832e038 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -37,8 +37,8 @@ "@grafana/tsconfig": "^2.0.0", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/jest": "^29.5.4", "@types/lodash": "4.17.15", "@types/node": "22.12.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index a48a1deb146..7892f942bee 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -136,8 +136,8 @@ "@storybook/theming": "^8.4.2", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/chance": "1.1.6", "@types/common-tags": "^1.8.0", "@types/d3": "7.4.3", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 7d55c792de8..b94e6cbfb9a 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -29,8 +29,8 @@ "@grafana/plugin-configs": "11.5.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", "@types/node": "22.12.0", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 56fa6c5d118..49699a2341c 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -30,8 +30,8 @@ "@grafana/plugin-configs": "11.5.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/debounce-promise": "3.1.9", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index 575d6fa8e5f..9b348a697bd 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -19,8 +19,8 @@ "@grafana/e2e-selectors": "11.5.0-pre", "@grafana/plugin-configs": "11.5.0-pre", "@testing-library/dom": "10.4.0", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", "@types/node": "22.12.0", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index ca1682f1758..1ad424096a3 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -23,8 +23,8 @@ "@grafana/plugin-configs": "11.5.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", "@types/node": "22.12.0", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 10ee41620e5..2f33cb2eb8e 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -25,8 +25,8 @@ "@grafana/e2e-selectors": "11.5.0-pre", "@grafana/plugin-configs": "11.5.0-pre", "@testing-library/dom": "10.4.0", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/d3-random": "^3.0.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index caef3607014..2ae92fa4a79 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -26,8 +26,8 @@ "@grafana/plugin-configs": "workspace:*", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", "@types/logfmt": "^1.2.3", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index 37d3945b481..477be490956 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -19,8 +19,8 @@ "@grafana/e2e-selectors": "11.5.0-pre", "@grafana/plugin-configs": "11.5.0-pre", "@testing-library/dom": "10.4.0", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", "@types/node": "22.12.0", diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index 15eb6a8607b..86ad07e26c2 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -19,8 +19,8 @@ "@grafana/e2e-selectors": "11.5.0-pre", "@grafana/plugin-configs": "11.5.0-pre", "@testing-library/dom": "10.4.0", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", "@types/node": "22.12.0", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index cf4dcec1268..4c32c0650ee 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -20,8 +20,8 @@ "devDependencies": { "@grafana/plugin-configs": "11.5.0-pre", "@testing-library/dom": "10.4.0", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/lodash": "4.17.15", "@types/node": "22.12.0", "@types/react": "18.3.18", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 970b6048c84..036930b25f1 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -42,8 +42,8 @@ "@grafana/plugin-configs": "11.5.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", - "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", + "@testing-library/react": "16.2.0", + "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", "@types/node": "22.12.0", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index e0d71055e2a..cb874a51024 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -23,7 +23,7 @@ "@grafana/plugin-configs": "workspace:*", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", - "@testing-library/react": "16.1.0", + "@testing-library/react": "16.2.0", "@types/jest": "29.5.14", "@types/lodash": "4.17.15", "@types/node": "22.12.0", diff --git a/yarn.lock b/yarn.lock index 2c6b0370c1e..84a0986b627 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2714,8 +2714,8 @@ __metadata: "@kusto/monaco-kusto": "npm:^10.0.0" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.12.0" @@ -2756,8 +2756,8 @@ __metadata: "@grafana/sql": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" "@testing-library/dom": "npm:10.4.0" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.12.0" @@ -2786,8 +2786,8 @@ __metadata: "@grafana/ui": "npm:11.5.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.12.0" @@ -2827,8 +2827,8 @@ __metadata: "@grafana/schema": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" "@testing-library/dom": "npm:10.4.0" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" @@ -2868,8 +2868,8 @@ __metadata: "@grafana/ui": "workspace:*" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" "@types/logfmt": "npm:^1.2.3" @@ -2909,8 +2909,8 @@ __metadata: "@grafana/sql": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" "@testing-library/dom": "npm:10.4.0" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.12.0" @@ -2940,8 +2940,8 @@ __metadata: "@grafana/sql": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" "@testing-library/dom": "npm:10.4.0" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.12.0" @@ -2969,8 +2969,8 @@ __metadata: "@grafana/schema": "npm:11.5.0-pre" "@grafana/ui": "npm:11.5.0-pre" "@testing-library/dom": "npm:10.4.0" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" @@ -3005,8 +3005,8 @@ __metadata: "@grafana/ui": "npm:11.5.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" @@ -3059,8 +3059,8 @@ __metadata: "@opentelemetry/semantic-conventions": "npm:1.28.0" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.12.0" @@ -3110,7 +3110,7 @@ __metadata: "@grafana/ui": "workspace:*" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" - "@testing-library/react": "npm:16.1.0" + "@testing-library/react": "npm:16.2.0" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.12.0" @@ -3394,8 +3394,8 @@ __metadata: "@rollup/plugin-node-resolve": "npm:16.0.0" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:^6.1.2" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.15" @@ -3504,8 +3504,8 @@ __metadata: "@grafana/ui": "npm:11.5.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:^6.1.2" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:^29.5.4" "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" @@ -3648,8 +3648,8 @@ __metadata: "@swc/helpers": "npm:0.5.15" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/d3": "npm:7.4.3" "@types/debounce-promise": "npm:3.1.9" "@types/eslint": "npm:9.6.1" @@ -3742,8 +3742,8 @@ __metadata: "@rollup/plugin-node-resolve": "npm:16.0.0" "@rollup/plugin-terser": "npm:0.4.4" "@testing-library/dom": "npm:10.4.0" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/angular": "npm:1.8.9" "@types/history": "npm:4.7.11" "@types/jest": "npm:29.5.14" @@ -3910,8 +3910,8 @@ __metadata: "@react-awesome-query-builder/ui": "npm:6.6.4" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:^6.1.2" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.15" "@types/node": "npm:22.12.0" @@ -4067,8 +4067,8 @@ __metadata: "@tanstack/react-virtual": "npm:^3.5.1" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/chance": "npm:1.1.6" "@types/common-tags": "npm:^1.8.0" "@types/d3": "npm:7.4.3" @@ -8796,9 +8796,9 @@ __metadata: languageName: node linkType: hard -"@testing-library/react@npm:16.1.0": - version: 16.1.0 - resolution: "@testing-library/react@npm:16.1.0" +"@testing-library/react@npm:16.2.0": + version: 16.2.0 + resolution: "@testing-library/react@npm:16.2.0" dependencies: "@babel/runtime": "npm:^7.12.5" peerDependencies: @@ -8812,16 +8812,16 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10/2a20e0dbfadbc93d45a84e82281ed47deed54a6a5fc1461a523172d7fbc0481e8502cf98a2080f38aba94290b3d745671a1c9e320e6f76ad6afcca67c580b963 + checksum: 10/cf10bfa9a363384e6861417696fff4a464a64f98ec6f0bb7f1fa7cbb550d075d23a2f6a943b7df85dded7bde3234f6ea6b6e36f95211f4544b846ea72c288289 languageName: node linkType: hard -"@testing-library/user-event@npm:14.5.2": - version: 14.5.2 - resolution: "@testing-library/user-event@npm:14.5.2" +"@testing-library/user-event@npm:14.6.1": + version: 14.6.1 + resolution: "@testing-library/user-event@npm:14.6.1" peerDependencies: "@testing-library/dom": ">=7.21.4" - checksum: 10/49821459d81c6bc435d97128d6386ca24f1e4b3ba8e46cb5a96fe3643efa6e002d88c1b02b7f2ec58da593e805c59b78d7fdf0db565c1f02ba782f63ee984040 + checksum: 10/34b74fff56a0447731a94b40d4cf246deb8dbc1c1e3aec93acd1c3377a760bb062e979f1572bb34ec164ad28ee2a391744b42d0d6d6cc16c4ce527e5e09610e1 languageName: node linkType: hard @@ -17864,8 +17864,8 @@ __metadata: "@swc/helpers": "npm:0.5.15" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" - "@testing-library/react": "npm:16.1.0" - "@testing-library/user-event": "npm:14.5.2" + "@testing-library/react": "npm:16.2.0" + "@testing-library/user-event": "npm:14.6.1" "@types/angular": "npm:1.8.9" "@types/angular-route": "npm:1.7.6" "@types/babel__core": "npm:^7" From d699f023c22ec1d01556e35b76b37f636910a69e Mon Sep 17 00:00:00 2001 From: Leonor Oliveira <9090754+leonorfmartins@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:36:20 +0100 Subject: [PATCH 270/894] Return max depth folder reached instead of a generic error (#99804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Return max depth folder reached instead of a generic error * Unit test error function * Lint * Update pkg/api/apierrors/folder.go Co-authored-by: Jean-Philippe Quéméner --------- Co-authored-by: Jean-Philippe Quéméner --- pkg/api/apierrors/folder.go | 6 +++ pkg/api/apierrors/folder_test.go | 83 ++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 pkg/api/apierrors/folder_test.go diff --git a/pkg/api/apierrors/folder.go b/pkg/api/apierrors/folder.go index 8006001356c..531636bc9c9 100644 --- a/pkg/api/apierrors/folder.go +++ b/pkg/api/apierrors/folder.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/util" ) @@ -43,6 +44,11 @@ func ToFolderErrorResponse(err error) response.Response { return response.JSON(http.StatusPreconditionFailed, util.DynMap{"status": "version-mismatch", "message": dashboards.ErrFolderVersionMismatch.Error()}) } + // folder errors are wrapped in an error util, so this is the only way of comparing errors + if err.Error() == folder.ErrMaximumDepthReached.Error() { + return response.JSON(http.StatusBadRequest, util.DynMap{"messageId": "folder.maximum-depth-reached", "message": "Maximum nested folder depth reached"}) + } + return response.ErrOrFallback(http.StatusInternalServerError, "Folder API error", err) } diff --git a/pkg/api/apierrors/folder_test.go b/pkg/api/apierrors/folder_test.go new file mode 100644 index 00000000000..e3d70bded31 --- /dev/null +++ b/pkg/api/apierrors/folder_test.go @@ -0,0 +1,83 @@ +package apierrors + +import ( + "errors" + "net/http" + "testing" + + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestToFolderErrorResponse(t *testing.T) { + tests := []struct { + name string + input error + want response.Response + }{ + { + name: "dashboard error", + input: dashboards.DashboardErr{StatusCode: 400, Reason: "Dashboard Error", Status: "error"}, + want: response.Error(400, "Dashboard Error", dashboards.DashboardErr{StatusCode: 400, Reason: "Dashboard Error", Status: "error"}), + }, + { + name: "folder title empty", + input: dashboards.ErrFolderTitleEmpty, + want: response.Error(400, "folder title cannot be empty", nil), + }, + { + name: "dashboard type mismatch", + input: dashboards.ErrDashboardTypeMismatch, + want: response.Error(400, "Dashboard cannot be changed to a folder", dashboards.ErrDashboardTypeMismatch), + }, + { + name: "dashboard invalid uid", + input: dashboards.ErrDashboardInvalidUid, + want: response.Error(400, "uid contains illegal characters", dashboards.ErrDashboardInvalidUid), + }, + { + name: "dashboard uid too long", + input: dashboards.ErrDashboardUidTooLong, + want: response.Error(400, "uid too long, max 40 characters", dashboards.ErrDashboardUidTooLong), + }, + { + name: "folder access denied", + input: dashboards.ErrFolderAccessDenied, + want: response.Error(http.StatusForbidden, "Access denied", dashboards.ErrFolderAccessDenied), + }, + { + name: "folder not found", + input: dashboards.ErrFolderNotFound, + want: response.JSON(http.StatusNotFound, util.DynMap{"status": "not-found", "message": dashboards.ErrFolderNotFound.Error()}), + }, + { + name: "folder with same uid exists", + input: dashboards.ErrFolderWithSameUIDExists, + want: response.Error(http.StatusConflict, dashboards.ErrFolderWithSameUIDExists.Error(), nil), + }, + { + name: "folder version mismatch", + input: dashboards.ErrFolderVersionMismatch, + want: response.JSON(http.StatusPreconditionFailed, util.DynMap{"status": "version-mismatch", "message": dashboards.ErrFolderVersionMismatch.Error()}), + }, + { + name: "folder max depth reached", + input: folder.ErrMaximumDepthReached, + want: response.JSON(http.StatusBadRequest, util.DynMap{"messageId": "folder.maximum-depth-reached", "message": "Maximum nested folder depth reached"}), + }, + { + name: "fallback error", + input: errors.New("some error"), + want: response.ErrOrFallback(http.StatusInternalServerError, "Folder API error", errors.New("some error")), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp := ToFolderErrorResponse(tt.input) + require.Equal(t, tt.want, resp) + }) + } +} From 7c15d333049709e6be81458829a588fbb4222f54 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:43:58 +0100 Subject: [PATCH 271/894] Remove @grafana/experimental in the last various places (#99875) * Remove @grafana/experimental in the last various places * Remove experimental from yarn.lock * Fix import order --- package.json | 1 + .../components/ContactPointsFilter.tsx | 3 +- .../tabs/Query/LokiQueryPreview.tsx | 2 +- .../tabs/Query/PrometheusQueryPreview.tsx | 2 +- .../dashboard/components/GenAI/hooks.ts | 8 ++-- .../dashboard/components/GenAI/utils.test.ts | 26 ++++++------- .../dashboard/components/GenAI/utils.ts | 6 +-- .../expressions/components/SqlExpr.tsx | 2 +- .../expressions/components/Threshold.tsx | 3 +- .../dashboard/DashboardQueryEditor.tsx | 2 +- .../grafana-testdata-datasource/package.json | 1 - .../plugins/datasource/parca/ConfigEditor.tsx | 2 +- yarn.lock | 39 ++----------------- 13 files changed, 30 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index ff42cadd8ae..f1288f6b3b6 100644 --- a/package.json +++ b/package.json @@ -267,6 +267,7 @@ "@grafana/flamegraph": "workspace:*", "@grafana/google-sdk": "0.1.2", "@grafana/lezer-logql": "0.2.7", + "@grafana/llm": "0.12.0", "@grafana/monaco-logql": "^0.0.8", "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/plugin-ui": "0.10.1", diff --git a/public/app/features/alerting/unified/components/contact-points/components/ContactPointsFilter.tsx b/public/app/features/alerting/unified/components/contact-points/components/ContactPointsFilter.tsx index 990352f0fcc..22048a9cf66 100644 --- a/public/app/features/alerting/unified/components/contact-points/components/ContactPointsFilter.tsx +++ b/public/app/features/alerting/unified/components/contact-points/components/ContactPointsFilter.tsx @@ -2,8 +2,7 @@ import { css } from '@emotion/css'; import { useCallback, useState } from 'react'; import { useDebounce } from 'react-use'; -import { Stack } from '@grafana/experimental'; -import { Button, Field, Icon, Input, useStyles2 } from '@grafana/ui'; +import { Button, Field, Icon, Input, Stack, useStyles2 } from '@grafana/ui'; import { useURLSearchParams } from '../../../hooks/useURLSearchParams'; diff --git a/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/LokiQueryPreview.tsx b/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/LokiQueryPreview.tsx index 03d2053a380..1d14149a0a2 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/LokiQueryPreview.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/LokiQueryPreview.tsx @@ -1,4 +1,4 @@ -import { RawQuery } from '@grafana/experimental'; +import { RawQuery } from '@grafana/plugin-ui'; import lokiGrammar from 'app/plugins/datasource/loki/syntax'; interface Props { diff --git a/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/PrometheusQueryPreview.tsx b/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/PrometheusQueryPreview.tsx index 446d8a08720..e2bdeb80b38 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/PrometheusQueryPreview.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/PrometheusQueryPreview.tsx @@ -1,4 +1,4 @@ -import { RawQuery } from '@grafana/experimental'; +import { RawQuery } from '@grafana/plugin-ui'; import { promqlGrammar } from '@grafana/prometheus'; interface Props { diff --git a/public/app/features/dashboard/components/GenAI/hooks.ts b/public/app/features/dashboard/components/GenAI/hooks.ts index eb0dc9ca9ae..c33dafebb3f 100644 --- a/public/app/features/dashboard/components/GenAI/hooks.ts +++ b/public/app/features/dashboard/components/GenAI/hooks.ts @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'reac import { useAsync } from 'react-use'; import { Subscription } from 'rxjs'; -import { llms } from '@grafana/experimental'; +import { openai } from '@grafana/llm'; import { createMonitoringLogger } from '@grafana/runtime'; import { useAppNotification } from 'app/core/copy/appNotification'; @@ -10,7 +10,7 @@ import { isLLMPluginEnabled, DEFAULT_OAI_MODEL } from './utils'; // Declared instead of imported from utils to make this hook modular // Ideally we will want to move the hook itself to a different scope later. -type Message = llms.openai.Message; +type Message = openai.Message; const genAILogger = createMonitoringLogger('features.dashboards.genai'); @@ -93,7 +93,7 @@ export function useOpenAIStream({ model, temperature, onResponse }: Options = de setStreamStatus(StreamStatus.GENERATING); setError(undefined); // Stream the completions. Each element is the next stream chunk. - const stream = llms.openai + const stream = openai .streamChatCompletions({ model, temperature, @@ -102,7 +102,7 @@ export function useOpenAIStream({ model, temperature, onResponse }: Options = de .pipe( // Accumulate the stream content into a stream of strings, where each // element contains the accumulated message so far. - llms.openai.accumulateContent() + openai.accumulateContent() // The stream is just a regular Observable, so we can use standard rxjs // functionality to update state, e.g. recording when the stream // has completed. diff --git a/public/app/features/dashboard/components/GenAI/utils.test.ts b/public/app/features/dashboard/components/GenAI/utils.test.ts index 5707b1c3195..2731d5f4927 100644 --- a/public/app/features/dashboard/components/GenAI/utils.test.ts +++ b/public/app/features/dashboard/components/GenAI/utils.test.ts @@ -1,4 +1,4 @@ -import { llms } from '@grafana/experimental'; +import { openai } from '@grafana/llm'; import { DASHBOARD_SCHEMA_VERSION } from '../../state/DashboardMigrator'; import { createDashboardModelFixture, createPanelSaveModel } from '../../state/__fixtures__/dashboardFixtures'; @@ -6,15 +6,13 @@ import { NEW_PANEL_TITLE } from '../../utils/dashboard'; import { getDashboardChanges, getPanelStrings, isLLMPluginEnabled, sanitizeReply } from './utils'; -// Mock the llms.openai module -jest.mock('@grafana/experimental', () => ({ - ...jest.requireActual('@grafana/experimental'), - llms: { - openai: { - streamChatCompletions: jest.fn(), - accumulateContent: jest.fn(), - health: jest.fn(), - }, +// Mock the openai module +jest.mock('@grafana/llm', () => ({ + ...jest.requireActual('@grafana/llm'), + openai: { + streamChatCompletions: jest.fn(), + accumulateContent: jest.fn(), + health: jest.fn(), }, })); @@ -101,8 +99,8 @@ describe('getDashboardChanges', () => { describe('isLLMPluginEnabled', () => { it('should return false if LLM plugin is not enabled', async () => { - // Mock llms.openai.health to return false - jest.mocked(llms.openai.health).mockResolvedValue({ ok: false, configured: false }); + // Mock openai.health to return false + jest.mocked(openai.health).mockResolvedValue({ ok: false, configured: false }); const enabled = await isLLMPluginEnabled(); @@ -110,8 +108,8 @@ describe('isLLMPluginEnabled', () => { }); it('should return true if LLM plugin is enabled', async () => { - // Mock llms.openai.health to return true - jest.mocked(llms.openai.health).mockResolvedValue({ ok: true, configured: false }); + // Mock openai.health to return true + jest.mocked(openai.health).mockResolvedValue({ ok: true, configured: false }); const enabled = await isLLMPluginEnabled(); diff --git a/public/app/features/dashboard/components/GenAI/utils.ts b/public/app/features/dashboard/components/GenAI/utils.ts index bad69a59e44..8d8938827be 100644 --- a/public/app/features/dashboard/components/GenAI/utils.ts +++ b/public/app/features/dashboard/components/GenAI/utils.ts @@ -1,6 +1,6 @@ import { pick } from 'lodash'; -import { llms } from '@grafana/experimental'; +import { openai } from '@grafana/llm'; import { config } from '@grafana/runtime'; import { Panel } from '@grafana/schema'; @@ -18,7 +18,7 @@ export enum Role { 'user' = 'user', } -export type Message = llms.openai.Message; +export type Message = openai.Message; export enum QuickFeedbackType { Shorter = 'Even shorter', @@ -80,7 +80,7 @@ export async function isLLMPluginEnabled(): Promise { // Check if the LLM plugin is enabled. // If not, we won't be able to make requests, so return early. llmHealthCheck = new Promise((resolve) => { - llms.openai.health().then((response) => { + openai.health().then((response) => { if (!response.ok) { // Health check fail clear cached promise so we can try again later llmHealthCheck = undefined; diff --git a/public/app/features/expressions/components/SqlExpr.tsx b/public/app/features/expressions/components/SqlExpr.tsx index f05ed42c224..5b8d6c0a424 100644 --- a/public/app/features/expressions/components/SqlExpr.tsx +++ b/public/app/features/expressions/components/SqlExpr.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { SQLEditor } from '@grafana/experimental'; +import { SQLEditor } from '@grafana/plugin-ui'; import { ExpressionQuery } from '../types'; diff --git a/public/app/features/expressions/components/Threshold.tsx b/public/app/features/expressions/components/Threshold.tsx index fac93a84c0b..6474c497e16 100644 --- a/public/app/features/expressions/components/Threshold.tsx +++ b/public/app/features/expressions/components/Threshold.tsx @@ -5,8 +5,7 @@ import * as React from 'react'; import { FormEvent, useEffect, useReducer } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { InlineField, InlineFieldRow, InlineSwitch, Input, Select, useStyles2 } from '@grafana/ui'; +import { InlineField, InlineFieldRow, InlineSwitch, Input, Select, useStyles2, Stack } from '@grafana/ui'; import { config } from 'app/core/config'; import { EvalFunction } from 'app/features/alerting/state/alertDef'; diff --git a/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx b/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx index babb3bf438a..4ce823746e2 100644 --- a/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx +++ b/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx @@ -5,7 +5,7 @@ import { useCallback, useMemo } from 'react'; import { useAsync } from 'react-use'; import { DataQuery, GrafanaTheme2, SelectableValue, DataTopic, QueryEditorProps } from '@grafana/data'; -import { OperationsEditorRow } from '@grafana/experimental'; +import { OperationsEditorRow } from '@grafana/plugin-ui'; import { Field, Select, useStyles2, Spinner, RadioButtonGroup, Stack, InlineSwitch } from '@grafana/ui'; import config from 'app/core/config'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 2f33cb2eb8e..cdab8016c21 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -6,7 +6,6 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "11.5.0-pre", - "@grafana/experimental": "2.1.6", "@grafana/runtime": "11.5.0-pre", "@grafana/schema": "11.5.0-pre", "@grafana/ui": "11.5.0-pre", diff --git a/public/app/plugins/datasource/parca/ConfigEditor.tsx b/public/app/plugins/datasource/parca/ConfigEditor.tsx index 32d61acd0b7..88ec40ee1c7 100644 --- a/public/app/plugins/datasource/parca/ConfigEditor.tsx +++ b/public/app/plugins/datasource/parca/ConfigEditor.tsx @@ -8,7 +8,7 @@ import { ConnectionSettings, DataSourceDescription, convertLegacyAuthProps, -} from '@grafana/experimental'; +} from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { Divider, SecureSocksProxySettings, Stack, useStyles2 } from '@grafana/ui'; diff --git a/yarn.lock b/yarn.lock index 84a0986b627..6498f2c7838 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2821,7 +2821,6 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:11.5.0-pre" "@grafana/e2e-selectors": "npm:11.5.0-pre" - "@grafana/experimental": "npm:2.1.6" "@grafana/plugin-configs": "npm:11.5.0-pre" "@grafana/runtime": "npm:11.5.0-pre" "@grafana/schema": "npm:11.5.0-pre" @@ -3312,32 +3311,6 @@ __metadata: languageName: unknown linkType: soft -"@grafana/experimental@npm:2.1.6": - version: 2.1.6 - resolution: "@grafana/experimental@npm:2.1.6" - dependencies: - "@hello-pangea/dnd": "npm:^16.6.0" - "@types/uuid": "npm:^8.3.3" - lodash: "npm:^4.17.21" - prismjs: "npm:^1.29.0" - react-popper-tooltip: "npm:^4.4.2" - react-use: "npm:^17.4.2" - semver: "npm:^7.5.4" - uuid: "npm:^8.3.2" - peerDependencies: - "@emotion/css": ^11.11.2 - "@grafana/data": ^10.4.0 || ^11.0.0 - "@grafana/e2e-selectors": ^10.0.0 || ^11.0.0 - "@grafana/runtime": ^10.4.0 || ^11.0.0 - "@grafana/ui": ^10.4.0 || ^11.0.0 - react: ^18.2.0 - react-dom: ^18.2.0 - react-select: ^5.8.0 - rxjs: ^7.8.1 - checksum: 10/3df81fef944e6dfdf843bf5954a586fe3eb90a1a2671088ae526b29fb9118793c05be96a3edd3d3babfdcbfa801fdfb328e87e6cbb1fd0de7f8f2d2e5c32f66f - languageName: node - linkType: hard - "@grafana/faro-core@npm:^1.12.3, @grafana/faro-core@npm:^1.3.6": version: 1.12.3 resolution: "@grafana/faro-core@npm:1.12.3" @@ -4179,7 +4152,7 @@ __metadata: languageName: node linkType: hard -"@hello-pangea/dnd@npm:16.6.0, @hello-pangea/dnd@npm:^16.6.0": +"@hello-pangea/dnd@npm:16.6.0": version: 16.6.0 resolution: "@hello-pangea/dnd@npm:16.6.0" dependencies: @@ -10320,13 +10293,6 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^8.3.3": - version: 8.3.4 - resolution: "@types/uuid@npm:8.3.4" - checksum: 10/6f11f3ff70f30210edaa8071422d405e9c1d4e53abbe50fdce365150d3c698fe7bbff65c1e71ae080cbfb8fded860dbb5e174da96fdbbdfcaa3fb3daa474d20f - languageName: node - linkType: hard - "@types/webpack-assets-manifest@npm:^5": version: 5.1.4 resolution: "@types/webpack-assets-manifest@npm:5.1.4" @@ -17815,6 +17781,7 @@ __metadata: "@grafana/flamegraph": "workspace:*" "@grafana/google-sdk": "npm:0.1.2" "@grafana/lezer-logql": "npm:0.2.7" + "@grafana/llm": "npm:0.12.0" "@grafana/monaco-logql": "npm:^0.0.8" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/plugin-e2e": "npm:1.17.1" @@ -26614,7 +26581,7 @@ __metadata: languageName: node linkType: hard -"react-use@npm:17.6.0, react-use@npm:^17.3.1, react-use@npm:^17.4.0, react-use@npm:^17.4.2, react-use@npm:^17.5.0": +"react-use@npm:17.6.0, react-use@npm:^17.3.1, react-use@npm:^17.4.0, react-use@npm:^17.5.0": version: 17.6.0 resolution: "react-use@npm:17.6.0" dependencies: From c3599d9236a6309143dd26dae464e6ccafa61323 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:46:26 +0100 Subject: [PATCH 272/894] Dashboards: Use uid instead of id to determine if dashboard exists or not (#99890) Use uid instead of id to determine if dashboard exists or not --- public/app/features/dashboard-scene/settings/utils.ts | 4 ++-- .../components/DashboardSettings/DashboardSettings.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard-scene/settings/utils.ts b/public/app/features/dashboard-scene/settings/utils.ts index d726085c7b3..88a3be11ad6 100644 --- a/public/app/features/dashboard-scene/settings/utils.ts +++ b/public/app/features/dashboard-scene/settings/utils.ts @@ -64,7 +64,7 @@ export function useDashboardEditPageNav(dashboard: DashboardScene, currentEditVi }); } - if (dashboard.state.id && dashboard.state.meta.canSave) { + if (dashboard.state.uid && dashboard.state.meta.canSave) { pageNav.children!.push({ text: t('dashboard-settings.versions.title', 'Versions'), url: locationUtil.getUrlForPartial(location, { editview: 'versions', editIndex: null }), @@ -72,7 +72,7 @@ export function useDashboardEditPageNav(dashboard: DashboardScene, currentEditVi }); } - if (dashboard.state.id && dashboard.state.meta.canAdmin) { + if (dashboard.state.uid && dashboard.state.meta.canAdmin) { if (contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsRead)) { pageNav.children!.push({ text: t('dashboard-settings.permissions.title', 'Permissions'), diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx index adb6af01c37..e940de38441 100644 --- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -134,7 +134,7 @@ function getSettingsPages(dashboard: DashboardModel) { }); } - if (dashboard.id && dashboard.meta.canSave) { + if (dashboard.uid && dashboard.meta.canSave) { pages.push({ title: t('dashboard-settings.versions.title', 'Versions'), id: 'versions', @@ -145,7 +145,7 @@ function getSettingsPages(dashboard: DashboardModel) { const permissionsTitle = t('dashboard-settings.permissions.title', 'Permissions'); - if (dashboard.id && dashboard.meta.canAdmin) { + if (dashboard.uid && dashboard.meta.canAdmin) { if (contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsRead)) { pages.push({ title: permissionsTitle, From 2e5f0bf77bac4bee09134c306548dd570836c21b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:12:33 +0100 Subject: [PATCH 273/894] Update typescript-eslint monorepo to v8.22.0 (#99891) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +- packages/grafana-prometheus/package.json | 4 +- yarn.lock | 124 +++++++++++------------ 3 files changed, 66 insertions(+), 66 deletions(-) diff --git a/package.json b/package.json index f1288f6b3b6..fd551f5fe24 100644 --- a/package.json +++ b/package.json @@ -153,8 +153,8 @@ "@types/webpack-assets-manifest": "^5", "@types/webpack-env": "^1.18.4", "@types/yargs": "17.0.33", - "@typescript-eslint/eslint-plugin": "8.18.1", - "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/eslint-plugin": "8.22.0", + "@typescript-eslint/parser": "8.22.0", "autoprefixer": "10.4.20", "babel-loader": "9.2.1", "blob-polyfill": "9.0.20240710", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 964f0ba834c..253b495cc13 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -102,8 +102,8 @@ "@types/react-window": "1.8.8", "@types/semver": "7.5.8", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.18.1", - "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/eslint-plugin": "8.22.0", + "@typescript-eslint/parser": "8.22.0", "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", "esbuild": "0.24.2", diff --git a/yarn.lock b/yarn.lock index 6498f2c7838..ebd1311fa78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3638,8 +3638,8 @@ __metadata: "@types/react-window": "npm:1.8.8" "@types/semver": "npm:7.5.8" "@types/uuid": "npm:10.0.0" - "@typescript-eslint/eslint-plugin": "npm:8.18.1" - "@typescript-eslint/parser": "npm:8.18.1" + "@typescript-eslint/eslint-plugin": "npm:8.22.0" + "@typescript-eslint/parser": "npm:8.22.0" copy-webpack-plugin: "npm:12.0.2" css-loader: "npm:7.1.2" d3: "npm:7.9.0" @@ -10356,40 +10356,40 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.18.1": - version: 8.18.1 - resolution: "@typescript-eslint/eslint-plugin@npm:8.18.1" +"@typescript-eslint/eslint-plugin@npm:8.22.0": + version: 8.22.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.22.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.18.1" - "@typescript-eslint/type-utils": "npm:8.18.1" - "@typescript-eslint/utils": "npm:8.18.1" - "@typescript-eslint/visitor-keys": "npm:8.18.1" + "@typescript-eslint/scope-manager": "npm:8.22.0" + "@typescript-eslint/type-utils": "npm:8.22.0" + "@typescript-eslint/utils": "npm:8.22.0" + "@typescript-eslint/visitor-keys": "npm:8.22.0" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" - ts-api-utils: "npm:^1.3.0" + ts-api-utils: "npm:^2.0.0" peerDependencies: "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.8.0" - checksum: 10/ec061a9c64477260d1ef0fc6283d8754838181e17aa90b3b8b9a70936a2ca4bae11607070917a7701e13f5301ced2b6da4b4b6e5cf525c484f97481e540b5111 + checksum: 10/7211ad95f20a27182e2b55ef50102dfee4a7084c267c4e24cca24f0a28daa0360074a38bb71e407dad6d99db1165096b324b708cf35904b1d4f62fc9d5fd0f98 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.18.1": - version: 8.18.1 - resolution: "@typescript-eslint/parser@npm:8.18.1" +"@typescript-eslint/parser@npm:8.22.0": + version: 8.22.0 + resolution: "@typescript-eslint/parser@npm:8.22.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.18.1" - "@typescript-eslint/types": "npm:8.18.1" - "@typescript-eslint/typescript-estree": "npm:8.18.1" - "@typescript-eslint/visitor-keys": "npm:8.18.1" + "@typescript-eslint/scope-manager": "npm:8.22.0" + "@typescript-eslint/types": "npm:8.22.0" + "@typescript-eslint/typescript-estree": "npm:8.22.0" + "@typescript-eslint/visitor-keys": "npm:8.22.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.8.0" - checksum: 10/09a601ef8b837962e5bb2687358520f337f9d0bbac5c6d5e159654faa5caaffb24d990e8d6bc4dc51ff5008dd9e182315c35bc5e9e3789090ccef8b8040e7659 + checksum: 10/6b7fee52345e8a32d8cfea1ac4aeb563cb0c44ba46290686afde1cd541b787fcf61bec0e6960559f544e9ba3b72670a68f8eda860384aebb5744101f0f1a68c9 languageName: node linkType: hard @@ -10403,28 +10403,28 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.18.1, @typescript-eslint/scope-manager@npm:^8.15.0": - version: 8.18.1 - resolution: "@typescript-eslint/scope-manager@npm:8.18.1" +"@typescript-eslint/scope-manager@npm:8.22.0, @typescript-eslint/scope-manager@npm:^8.15.0": + version: 8.22.0 + resolution: "@typescript-eslint/scope-manager@npm:8.22.0" dependencies: - "@typescript-eslint/types": "npm:8.18.1" - "@typescript-eslint/visitor-keys": "npm:8.18.1" - checksum: 10/14f7c09924c3a006b20752e5204b33c2b6974fc00bea16c23f471e65f2fb089fcbd3fb5296bcfd6727ac95c32ba24ebb15ba84fbf1deadc17b4cc5ca7f41c72a + "@typescript-eslint/types": "npm:8.22.0" + "@typescript-eslint/visitor-keys": "npm:8.22.0" + checksum: 10/7fb4bae6d9f8b86a43405b24828cd36ba0751cce4346d86821a4827cded93227f92668044e5e6d802a32096b50cfcaf2ce9ab65322310fa68f5e3819bef70168 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.18.1": - version: 8.18.1 - resolution: "@typescript-eslint/type-utils@npm:8.18.1" +"@typescript-eslint/type-utils@npm:8.22.0": + version: 8.22.0 + resolution: "@typescript-eslint/type-utils@npm:8.22.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.18.1" - "@typescript-eslint/utils": "npm:8.18.1" + "@typescript-eslint/typescript-estree": "npm:8.22.0" + "@typescript-eslint/utils": "npm:8.22.0" debug: "npm:^4.3.4" - ts-api-utils: "npm:^1.3.0" + ts-api-utils: "npm:^2.0.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.8.0" - checksum: 10/cde53d05f4ca6e172239918cba2b560b9f837aa1fc7d5220784b1a6af9c8c525db020a5160822087e320305492fe359b7fb191420789b5f1e47a01e0cda21ac9 + checksum: 10/1da2447ce12f09370082daeef88f8922842e39d2a7b0abe3def21442f85bf4250524c60cbb97276d5cd876783b976dcb0ed85aeb8c0b100d83b7f3a59cdfccbf languageName: node linkType: hard @@ -10435,10 +10435,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.18.1, @typescript-eslint/types@npm:^8.9.0": - version: 8.18.1 - resolution: "@typescript-eslint/types@npm:8.18.1" - checksum: 10/57a6141ba17be929291a644991f3a76f94fce330376f6a079decb20fb53378d636ad6878f8f9b6fcb8244cf1ca8b118f9e8901ae04cf3de2aa9f9ff57791d97a +"@typescript-eslint/types@npm:8.22.0, @typescript-eslint/types@npm:^8.9.0": + version: 8.22.0 + resolution: "@typescript-eslint/types@npm:8.22.0" + checksum: 10/b43ea5b05ed0b43dcee8d2fa98b2c3f79c604780cbd56e6ba7f89e3066798b7169848694f59523fd2003e8fa699ddc97f28b0860a4eb04eea26c96d5ac9346bd languageName: node linkType: hard @@ -10460,36 +10460,36 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.18.1": - version: 8.18.1 - resolution: "@typescript-eslint/typescript-estree@npm:8.18.1" +"@typescript-eslint/typescript-estree@npm:8.22.0": + version: 8.22.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.22.0" dependencies: - "@typescript-eslint/types": "npm:8.18.1" - "@typescript-eslint/visitor-keys": "npm:8.18.1" + "@typescript-eslint/types": "npm:8.22.0" + "@typescript-eslint/visitor-keys": "npm:8.22.0" debug: "npm:^4.3.4" fast-glob: "npm:^3.3.2" is-glob: "npm:^4.0.3" minimatch: "npm:^9.0.4" semver: "npm:^7.6.0" - ts-api-utils: "npm:^1.3.0" + ts-api-utils: "npm:^2.0.0" peerDependencies: typescript: ">=4.8.4 <5.8.0" - checksum: 10/8ecc1b50b9fc32116eee1b3b00f3fb29cf18026c0bbb50ab5f6e01db58ef62b8ac01824f2950f132479be6e1b82466a2bfd1e2cb4525aa8dbce4c27fc2494cfc + checksum: 10/e3c0b191e2a0f55101c3e3333904f3a255d635e4ea0d026981cc25e83b62660a3a8a7993ac4a3d0c8756afb7dc272099eec48fd93e100a2b8467a5b80ef0026c languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.18.1, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.13.0, @typescript-eslint/utils@npm:^8.15.0, @typescript-eslint/utils@npm:^8.9.0": - version: 8.18.1 - resolution: "@typescript-eslint/utils@npm:8.18.1" +"@typescript-eslint/utils@npm:8.22.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.13.0, @typescript-eslint/utils@npm:^8.15.0, @typescript-eslint/utils@npm:^8.9.0": + version: 8.22.0 + resolution: "@typescript-eslint/utils@npm:8.22.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.18.1" - "@typescript-eslint/types": "npm:8.18.1" - "@typescript-eslint/typescript-estree": "npm:8.18.1" + "@typescript-eslint/scope-manager": "npm:8.22.0" + "@typescript-eslint/types": "npm:8.22.0" + "@typescript-eslint/typescript-estree": "npm:8.22.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.8.0" - checksum: 10/7b33d2ac273ad606a3dcb776bcf02c901812952550cdc93d4ece272b3b0e5d2a4e05fa92f9bd466f4a296ddd5992902d3b6623aa1c29d09e8e392897103e42a8 + checksum: 10/92a5ae5d79a5988e88fdda8d5e88f73e7b9ce24b339098d72698dba766ded274c24d0e2857bcb799c0aa7a59257e54a273eabdaaab39a5cd20283669201eeb53 languageName: node linkType: hard @@ -10521,13 +10521,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.18.1": - version: 8.18.1 - resolution: "@typescript-eslint/visitor-keys@npm:8.18.1" +"@typescript-eslint/visitor-keys@npm:8.22.0": + version: 8.22.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.22.0" dependencies: - "@typescript-eslint/types": "npm:8.18.1" + "@typescript-eslint/types": "npm:8.22.0" eslint-visitor-keys: "npm:^4.2.0" - checksum: 10/00e88b1640a68c3afea08731395eb09a8216892248fee819cb7526e99093256743239d6b9e880a499f1c0ddfe2ffa4d1ad895d9e778b5d42e702d5880db1a594 + checksum: 10/1a172620d46e23362c5d1e1e7c8186856dff6b6f1c2697d67f9aac1b3dfd0de96c2c73487e4deed80fad3bfa5cf74cfed3519221657c6ede602b04ac091525a4 languageName: node linkType: hard @@ -17888,8 +17888,8 @@ __metadata: "@types/webpack-assets-manifest": "npm:^5" "@types/webpack-env": "npm:^1.18.4" "@types/yargs": "npm:17.0.33" - "@typescript-eslint/eslint-plugin": "npm:8.18.1" - "@typescript-eslint/parser": "npm:8.18.1" + "@typescript-eslint/eslint-plugin": "npm:8.22.0" + "@typescript-eslint/parser": "npm:8.22.0" "@visx/event": "npm:3.12.0" "@visx/gradient": "npm:3.12.0" "@visx/group": "npm:3.12.0" @@ -30024,12 +30024,12 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^1.3.0": - version: 1.3.0 - resolution: "ts-api-utils@npm:1.3.0" +"ts-api-utils@npm:^2.0.0": + version: 2.0.0 + resolution: "ts-api-utils@npm:2.0.0" peerDependencies: - typescript: ">=4.2.0" - checksum: 10/3ee44faa24410cd649b5c864e068d438aa437ef64e9e4a66a41646a6d3024d3097a695eeb3fb26ee364705d3cb9653a65756d009e6a53badb6066a5f447bf7ed + typescript: ">=4.8.4" + checksum: 10/485bdf8bbba98d58712243d958f4fd44742bbe49e559cd77882fb426d866eec6dd05c67ef91935dc4f8a3c776f235859735e1f05be399e4dc9e7ffd580120974 languageName: node linkType: hard From 3d3b781c19eb9b4633731b0012a39bf8e5fa957c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:16:37 +0000 Subject: [PATCH 274/894] Update dependency @stylistic/eslint-plugin-ts to v3 (#99897) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index fd551f5fe24..92812f0a25a 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "@rsdoctor/webpack-plugin": "^0.4.6", "@rtk-query/codegen-openapi": "^2.0.0", "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", - "@stylistic/eslint-plugin-ts": "^2.9.0", + "@stylistic/eslint-plugin-ts": "^3.0.0", "@swc/core": "1.10.12", "@swc/helpers": "0.5.15", "@testing-library/dom": "10.4.0", diff --git a/yarn.lock b/yarn.lock index ebd1311fa78..24b650e7eb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7775,16 +7775,16 @@ __metadata: languageName: node linkType: hard -"@stylistic/eslint-plugin-ts@npm:^2.9.0": - version: 2.13.0 - resolution: "@stylistic/eslint-plugin-ts@npm:2.13.0" +"@stylistic/eslint-plugin-ts@npm:^3.0.0": + version: 3.0.1 + resolution: "@stylistic/eslint-plugin-ts@npm:3.0.1" dependencies: "@typescript-eslint/utils": "npm:^8.13.0" eslint-visitor-keys: "npm:^4.2.0" espree: "npm:^10.3.0" peerDependencies: eslint: ">=8.40.0" - checksum: 10/222d6ec0094177f40ea43f28af257192655539fe4bb1495341bc9ff521bca975da2066e229798fcc8e28ea24e91d0ef4db05b4f212f615cad752ade5c1c68561 + checksum: 10/24dc9f67ff56bc200340807a1fd055e8f49332c244b0ba214ea68ed91773d922790645db34f014e0f703644cc605171e5190df385597dad12d127d92fd72d29b languageName: node linkType: hard @@ -17826,7 +17826,7 @@ __metadata: "@rsdoctor/webpack-plugin": "npm:^0.4.6" "@rtk-query/codegen-openapi": "npm:^2.0.0" "@rtsao/plugin-proposal-class-properties": "npm:7.0.1-patch.1" - "@stylistic/eslint-plugin-ts": "npm:^2.9.0" + "@stylistic/eslint-plugin-ts": "npm:^3.0.0" "@swc/core": "npm:1.10.12" "@swc/helpers": "npm:0.5.15" "@testing-library/dom": "npm:10.4.0" From a3eebd71578432ce83d9a9cb6e96d78e2f317632 Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Fri, 31 Jan 2025 10:20:14 -0500 Subject: [PATCH 275/894] Elasticsearch: Replace term size dropdown with text input (#99718) --- .../TermsSettingsEditor.test.tsx | 39 ++++++++++++++++++- .../SettingsEditor/TermsSettingsEditor.tsx | 25 ++++++------ .../BucketAggregationsEditor/utils.ts | 11 ------ 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx index bbc46703aaa..a0bf21c4046 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx @@ -1,12 +1,15 @@ -import { screen } from '@testing-library/react'; +import { fireEvent, screen } from '@testing-library/react'; import selectEvent from 'react-select-event'; +import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { renderWithESProvider } from '../../../../test-helpers/render'; import { ElasticsearchQuery, Terms, Average, Derivative, TopMetrics } from '../../../../types'; import { describeMetric } from '../../../../utils'; import { TermsSettingsEditor } from './TermsSettingsEditor'; +jest.mock('../../../../hooks/useStatelessReducer'); + describe('Terms Settings Editor', () => { it('Pipeline aggregations should not be in "order by" options', () => { const termsAgg: Terms = { @@ -37,4 +40,38 @@ describe('Terms Settings Editor', () => { // All other metric aggregations can be used in order by expect(screen.getByText(describeMetric(avg))).toBeInTheDocument(); }); + + describe('Handling change', () => { + let dispatch = jest.fn(); + beforeEach(() => { + dispatch.mockClear(); + jest.mocked(useDispatch).mockReturnValue(dispatch); + }); + + test('updating size', async () => { + const termsAgg: Terms = { + id: '1', + type: 'terms', + }; + const avg: Average = { id: '2', type: 'avg', field: '@value' }; + const derivative: Derivative = { id: '3', field: avg.id, type: 'derivative' }; + const topMetrics: TopMetrics = { id: '4', type: 'top_metrics' }; + const query: ElasticsearchQuery = { + refId: 'A', + query: '', + bucketAggs: [termsAgg], + metrics: [avg, derivative, topMetrics], + }; + + renderWithESProvider(, { providerProps: { query } }); + + const sizeInput = screen.getByLabelText('Size'); + fireEvent.change(sizeInput, { target: { value: '30' } }); + fireEvent.blur(sizeInput); + + expect(dispatch).toHaveBeenCalledTimes(1); + expect(dispatch.mock.calls[0][0].payload.settingName).toBe('size'); + expect(dispatch.mock.calls[0][0].payload.newValue).toBe('30'); + }); + }); }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx index e6292c75396..80bbd154e9f 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx @@ -7,11 +7,10 @@ import { InlineField, Select, Input } from '@grafana/ui'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { MetricAggregation, Percentiles, ExtendedStatMetaType, ExtendedStats, Terms } from '../../../../types'; import { describeMetric } from '../../../../utils'; -import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour'; import { useQuery } from '../../ElasticsearchQueryContext'; import { isPipelineAggregation } from '../../MetricAggregationsEditor/aggregations'; import { changeBucketAggregationSetting } from '../state/actions'; -import { bucketAggregationConfig, orderByOptions, orderOptions, sizeOptions } from '../utils'; +import { bucketAggregationConfig, orderByOptions, orderOptions } from '../utils'; import { inlineFieldProps } from '.'; @@ -23,6 +22,12 @@ export const TermsSettingsEditor = ({ bucketAgg }: Props) => { const { metrics } = useQuery(); const orderBy = createOrderByOptions(metrics); const { current: baseId } = useRef(uniqueId('es-terms-')); + let size = bucketAgg.settings?.size || bucketAggregationConfig.terms.defaultSettings?.size; + if (!size || size === '') { + size = '10'; + } else if (size === '0') { + size = '500'; + } const dispatch = useDispatch(); @@ -40,16 +45,12 @@ export const TermsSettingsEditor = ({ bucketAgg }: Props) => { - - + {warning && ( @@ -53,9 +54,11 @@ export const RowOptionsForm = ({ repeat, title, sceneContext, warning, onUpdate, )} + - ); diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowOptionsModal.tsx b/public/app/features/dashboard-scene/scene/layout-default/row-actions/RowOptionsModal.tsx similarity index 79% rename from public/app/features/dashboard-scene/scene/row-actions/RowOptionsModal.tsx rename to public/app/features/dashboard-scene/scene/layout-default/row-actions/RowOptionsModal.tsx index bab4052178b..0976bf2e387 100644 --- a/public/app/features/dashboard-scene/scene/row-actions/RowOptionsModal.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/row-actions/RowOptionsModal.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { SceneObject } from '@grafana/scenes'; import { Modal, useStyles2 } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; import { OnRowOptionsUpdate, RowOptionsForm } from './RowOptionsForm'; @@ -19,7 +20,12 @@ export const RowOptionsModal = ({ repeat, title, parent, onDismiss, onUpdate, wa const styles = useStyles2(getStyles); return ( - + child !== element) }); @@ -126,6 +130,7 @@ export class ResponsiveGridLayoutManager activateRepeaters?(): void { throw new Error('Method not implemented.'); } + public static Component = ({ model }: SceneComponentProps) => { return ; }; diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx index cd95834bcad..e52fd8d46d2 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx @@ -1,17 +1,40 @@ import { css, cx } from '@emotion/css'; -import { useMemo, useRef } from 'react'; +import { ReactNode, useMemo, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { SceneObjectState, SceneObjectBase, SceneComponentProps, sceneGraph } from '@grafana/scenes'; -import { Button, Icon, Input, RadioButtonGroup, Switch, useElementSelection, useStyles2 } from '@grafana/ui'; +import { + SceneObjectState, + SceneObjectBase, + SceneComponentProps, + sceneGraph, + VariableDependencyConfig, +} from '@grafana/scenes'; +import { + Alert, + Button, + Icon, + Input, + RadioButtonGroup, + Switch, + TextLink, + useElementSelection, + useStyles2, +} from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect'; +import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { getDashboardSceneFor, getDefaultVizPanel } from '../../utils/utils'; +import { isClonedKey } from '../../utils/clone'; +import { getDashboardSceneFor, getDefaultVizPanel, getQueryRunnerFor } from '../../utils/utils'; +import { DashboardScene } from '../DashboardScene'; import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector'; import { DashboardLayoutManager, EditableDashboardElement, LayoutParent } from '../types'; +import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior'; import { RowsLayoutManager } from './RowsLayoutManager'; export interface RowItemState extends SceneObjectState { @@ -23,6 +46,10 @@ export interface RowItemState extends SceneObjectState { } export class RowItem extends SceneObjectBase implements LayoutParent, EditableDashboardElement { + protected _variableDependency = new VariableDependencyConfig(this, { + statePaths: ['title'], + }); + public isEditableDashboardElement: true = true; public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { @@ -54,10 +81,25 @@ export class RowItem extends SceneObjectBase implements LayoutPare ); }, [row]); + const rowRepeatOptions = useMemo(() => { + const dashboard = getDashboardSceneFor(row); + + return new OptionsPaneCategoryDescriptor({ + title: 'Repeat options', + id: 'row-repeat-options', + isOpenDefault: true, + }).addItem( + new OptionsPaneItemDescriptor({ + title: 'Variable', + render: () => , + }) + ); + }, [row]); + const { layout } = this.useState(); const layoutOptions = useLayoutCategory(layout); - return [rowOptions, layoutOptions]; + return [rowOptions, rowRepeatOptions, layoutOptions]; } public getTypeName(): string { @@ -69,7 +111,7 @@ export class RowItem extends SceneObjectBase implements LayoutPare layout.removeRow(this); }; - public renderActions(): React.ReactNode { + public renderActions(): ReactNode { return ( <> - {isEditing && ( + {!isClone && isEditing && (
@@ -181,6 +225,7 @@ function getStyles(theme: GrafanaTheme2) { display: 'flex', flexDirection: 'column', width: '100%', + minHeight: '100px', }), wrapperGrow: css({ flexGrow: 1, @@ -188,6 +233,7 @@ function getStyles(theme: GrafanaTheme2) { wrapperCollapsed: css({ flexGrow: 0, borderBottom: `1px solid ${theme.colors.border.weak}`, + minHeight: 'unset', }), rowActions: css({ display: 'flex', @@ -237,3 +283,68 @@ export function RowHeightSelect({ row }: { row: RowItem }) { /> ); } + +export function RowRepeatSelect({ row, dashboard }: { row: RowItem; dashboard: DashboardScene }) { + const { layout, $behaviors } = row.useState(); + + let repeatBehavior: RowItemRepeaterBehavior | undefined = $behaviors?.find( + (b) => b instanceof RowItemRepeaterBehavior + ); + const { variableName } = repeatBehavior?.state ?? {}; + + const isAnyPanelUsingDashboardDS = layout.getVizPanels().some((vizPanel) => { + const runner = getQueryRunnerFor(vizPanel); + return ( + runner?.state.datasource?.uid === SHARED_DASHBOARD_QUERY || + (runner?.state.datasource?.uid === MIXED_DATASOURCE_NAME && + runner?.state.queries.some((query) => query.datasource?.uid === SHARED_DASHBOARD_QUERY)) + ); + }); + + return ( + <> + { + if (repeat) { + // Remove repeat behavior if it exists to trigger repeat when adding new one + if (repeatBehavior) { + repeatBehavior.removeBehavior(); + } + + repeatBehavior = new RowItemRepeaterBehavior({ variableName: repeat }); + row.setState({ $behaviors: [...(row.state.$behaviors ?? []), repeatBehavior] }); + repeatBehavior.activate(); + } else { + repeatBehavior?.removeBehavior(); + } + }} + /> + {isAnyPanelUsingDashboardDS ? ( + +

+ + Panels in this row use the {{ SHARED_DASHBOARD_QUERY }} data source. These panels will reference the panel + in the original row, not the ones in the repeated rows. + +

+ + Learn more + +
+ ) : undefined} + + ); +} diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.test.tsx new file mode 100644 index 00000000000..753987c15ae --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.test.tsx @@ -0,0 +1,227 @@ +import { VariableRefresh } from '@grafana/data'; +import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { + SceneGridRow, + SceneTimeRange, + SceneVariableSet, + TestVariable, + VariableValueOption, + PanelBuilders, +} from '@grafana/scenes'; +import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants'; +import { TextMode } from 'app/plugins/panel/text/panelcfg.gen'; + +import { getCloneKey, isInCloneChain, joinCloneKeys } from '../../utils/clone'; +import { activateFullSceneTree } from '../../utils/test-utils'; +import { DashboardScene } from '../DashboardScene'; +import { DashboardGridItem } from '../layout-default/DashboardGridItem'; +import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; + +import { RowItem } from './RowItem'; +import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior'; +import { RowsLayoutManager } from './RowsLayoutManager'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + setPluginExtensionGetter: jest.fn(), + getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), +})); + +setPluginImportUtils({ + importPanelPlugin: () => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: () => undefined, +}); + +describe('RowItemRepeaterBehavior', () => { + describe('Given scene with variable with 5 values', () => { + let scene: DashboardScene, layout: RowsLayoutManager, repeatBehavior: RowItemRepeaterBehavior; + let layoutStateUpdates: unknown[]; + + beforeEach(async () => { + ({ scene, layout, repeatBehavior } = buildScene({ variableQueryTime: 0 })); + + layoutStateUpdates = []; + layout.subscribeToState((state) => layoutStateUpdates.push(state)); + + activateFullSceneTree(scene); + await new Promise((r) => setTimeout(r, 1)); + }); + + it('Should repeat row', () => { + // Verify that first row still has repeat behavior + const row1 = layout.state.rows[0]; + expect(row1.state.key).toBe(getCloneKey('row-1', 0)); + expect(row1.state.$behaviors?.[0]).toBeInstanceOf(RowItemRepeaterBehavior); + expect(row1.state.$variables!.state.variables[0].getValue()).toBe('A1'); + + const row1Children = getRowChildren(row1); + expect(row1Children[0].state.key!).toBe(joinCloneKeys(row1.state.key!, 'grid-item-0')); + expect(row1Children[0].state.body?.state.key).toBe(joinCloneKeys(row1Children[0].state.key!, 'panel-0')); + + const row2 = layout.state.rows[1]; + expect(row2.state.key).toBe(getCloneKey('row-1', 1)); + expect(row2.state.$behaviors).toEqual([]); + expect(row2.state.$variables!.state.variables[0].getValueText?.()).toBe('B'); + + const row2Children = getRowChildren(row2); + expect(row2Children[0].state.key!).toBe(joinCloneKeys(row2.state.key!, 'grid-item-0')); + expect(row2Children[0].state.body?.state.key).toBe(joinCloneKeys(row2Children[0].state.key!, 'panel-0')); + }); + + it('Repeated rows should be read only', () => { + const row1 = layout.state.rows[0]; + expect(isInCloneChain(row1.state.key!)).toBe(false); + + const row2 = layout.state.rows[1]; + expect(isInCloneChain(row2.state.key!)).toBe(true); + }); + + it('Should push row at the bottom down', () => { + // Should push row at the bottom down + const rowAtTheBottom = layout.state.rows[5]; + expect(rowAtTheBottom.state.title).toBe('Row at the bottom'); + }); + + it('Should handle second repeat cycle and update remove old repeats', async () => { + // trigger another repeat cycle by changing the variable + const variable = scene.state.$variables!.state.variables[0] as TestVariable; + variable.changeValueTo(['B1', 'C1']); + + await new Promise((r) => setTimeout(r, 1)); + + // should now only have 2 repeated rows (and the panel above + the row at the bottom) + expect(layout.state.rows.length).toBe(3); + }); + + it('Should ignore repeat process if variable values are the same', async () => { + // trigger another repeat cycle by changing the variable + repeatBehavior.performRepeat(); + + await new Promise((r) => setTimeout(r, 1)); + + expect(layoutStateUpdates.length).toBe(1); + }); + }); + + describe('Given a scene with empty variable', () => { + it('Should preserve repeat row', async () => { + const { scene, layout } = buildScene({ variableQueryTime: 0 }, []); + activateFullSceneTree(scene); + await new Promise((r) => setTimeout(r, 1)); + + // Should have 2 rows, one without repeat and one with the dummy row + expect(layout.state.rows.length).toBe(2); + expect(layout.state.rows[0].state.$behaviors?.[0]).toBeInstanceOf(RowItemRepeaterBehavior); + }); + }); +}); + +interface SceneOptions { + variableQueryTime: number; + variableRefresh?: VariableRefresh; +} + +function buildTextPanel(key: string, content: string) { + const panel = PanelBuilders.text().setOption('content', content).setOption('mode', TextMode.Markdown).build(); + panel.setState({ key }); + return panel; +} + +function buildScene( + options: SceneOptions, + variableOptions?: VariableValueOption[], + variableStateOverrides?: { isMulti: boolean } +) { + const repeatBehavior = new RowItemRepeaterBehavior({ variableName: 'server' }); + + const rows = [ + new RowItem({ + key: 'row-1', + $behaviors: [repeatBehavior], + layout: DefaultGridLayoutManager.fromGridItems([ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + y: 11, + width: 24, + height: 5, + body: buildTextPanel('text-1', 'Panel inside repeated row, server = $server'), + }), + ]), + }), + new RowItem({ + key: 'row-2', + title: 'Row at the bottom', + layout: DefaultGridLayoutManager.fromGridItems([ + new DashboardGridItem({ + key: 'griditem-2', + x: 0, + y: 17, + body: buildTextPanel('text-2', 'Panel inside row, server = $server'), + }), + new DashboardGridItem({ + key: 'griditem-3', + x: 0, + y: 25, + body: buildTextPanel('text-3', 'Panel inside row, server = $server'), + }), + ]), + }), + ]; + + const layout = new RowsLayoutManager({ rows }); + + const scene = new DashboardScene({ + $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), + $variables: new SceneVariableSet({ + variables: [ + new TestVariable({ + name: 'server', + query: 'A.*', + value: ALL_VARIABLE_VALUE, + text: ALL_VARIABLE_TEXT, + isMulti: true, + includeAll: true, + delayMs: options.variableQueryTime, + refresh: options.variableRefresh, + optionsToReturn: variableOptions ?? [ + { label: 'A', value: 'A1' }, + { label: 'B', value: 'B1' }, + { label: 'C', value: 'C1' }, + { label: 'D', value: 'D1' }, + { label: 'E', value: 'E1' }, + ], + ...variableStateOverrides, + }), + ], + }), + body: layout, + }); + + const rowToRepeat = repeatBehavior.parent as SceneGridRow; + + return { scene, layout, rows, repeatBehavior, rowToRepeat }; +} + +function getRowLayout(row: RowItem): DefaultGridLayoutManager { + const layout = row.getLayout(); + + if (!(layout instanceof DefaultGridLayoutManager)) { + throw new Error('Invalid layout'); + } + + return layout; +} + +function getRowChildren(row: RowItem): DashboardGridItem[] { + const layout = getRowLayout(row); + + const filteredChildren = layout.state.grid.state.children.filter((child) => child instanceof DashboardGridItem); + + if (filteredChildren.length !== layout.state.grid.state.children.length) { + throw new Error('Invalid layout'); + } + + return filteredChildren; +} diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.ts new file mode 100644 index 00000000000..15b376fd0dd --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.ts @@ -0,0 +1,165 @@ +import { isEqual } from 'lodash'; + +import { + LocalValueVariable, + MultiValueVariable, + sceneGraph, + SceneObjectBase, + SceneObjectState, + SceneVariableSet, + VariableDependencyConfig, + VariableValueSingle, +} from '@grafana/scenes'; + +import { isClonedKeyOf, getCloneKey } from '../../utils/clone'; +import { getMultiVariableValues } from '../../utils/utils'; +import { DashboardRepeatsProcessedEvent } from '../types'; + +import { RowItem } from './RowItem'; +import { RowsLayoutManager } from './RowsLayoutManager'; + +interface RowItemRepeaterBehaviorState extends SceneObjectState { + variableName: string; +} + +/** + * This behavior will run an effect function when specified variables change + */ + +export class RowItemRepeaterBehavior extends SceneObjectBase { + protected _variableDependency = new VariableDependencyConfig(this, { + variableNames: [this.state.variableName], + onVariableUpdateCompleted: () => this.performRepeat(), + }); + + private _prevRepeatValues?: VariableValueSingle[]; + private _clonedRows?: RowItem[]; + + public constructor(state: RowItemRepeaterBehaviorState) { + super(state); + + this.addActivationHandler(() => this._activationHandler()); + } + + private _activationHandler() { + this.performRepeat(); + } + + private _getRow(): RowItem { + if (!(this.parent instanceof RowItem)) { + throw new Error('RepeatedRowItemBehavior: Parent is not a RowItem'); + } + + return this.parent; + } + + private _getLayout(): RowsLayoutManager { + const layout = this._getRow().parent; + + if (!(layout instanceof RowsLayoutManager)) { + throw new Error('RepeatedRowItemBehavior: Layout is not a RowsLayoutManager'); + } + + return layout; + } + + public performRepeat(force = false) { + if (this._variableDependency.hasDependencyInLoadingState()) { + return; + } + + const variable = sceneGraph.lookupVariable(this.state.variableName, this.parent?.parent!); + + if (!variable) { + console.error('RepeatedRowItemBehavior: Variable not found'); + return; + } + + if (!(variable instanceof MultiValueVariable)) { + console.error('RepeatedRowItemBehavior: Variable is not a MultiValueVariable'); + return; + } + + const rowToRepeat = this._getRow(); + const layout = this._getLayout(); + const { values, texts } = getMultiVariableValues(variable); + + // Do nothing if values are the same + if (isEqual(this._prevRepeatValues, values) && !force) { + return; + } + + this._prevRepeatValues = values; + + this._clonedRows = []; + + const rowContent = rowToRepeat.getLayout(); + + // when variable has no options (due to error or similar) it will not render any panels at all + // adding a placeholder in this case so that there is at least empty panel that can display error + const emptyVariablePlaceholderOption = { + values: [''], + texts: variable.hasAllValue() ? ['All'] : ['None'], + }; + + const variableValues = values.length ? values : emptyVariablePlaceholderOption.values; + const variableTexts = texts.length ? texts : emptyVariablePlaceholderOption.texts; + + // Loop through variable values and create repeats + for (let rowIndex = 0; rowIndex < variableValues.length; rowIndex++) { + const isSourceRow = rowIndex === 0; + const rowClone = isSourceRow ? rowToRepeat : rowToRepeat.clone({ $behaviors: [] }); + + const rowCloneKey = getCloneKey(rowToRepeat.state.key!, rowIndex); + + rowClone.setState({ + key: rowCloneKey, + $variables: new SceneVariableSet({ + variables: [ + new LocalValueVariable({ + name: this.state.variableName, + value: variableValues[rowIndex], + text: String(variableTexts[rowIndex]), + isMulti: variable.state.isMulti, + includeAll: variable.state.includeAll, + }), + ], + }), + layout: rowContent.cloneLayout?.(rowCloneKey, isSourceRow), + }); + + this._clonedRows.push(rowClone); + } + + updateLayout(layout, this._clonedRows, rowToRepeat.state.key!); + + // Used from dashboard url sync + this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true); + } + + public removeBehavior() { + const row = this._getRow(); + const layout = this._getLayout(); + const rows = getRowsFilterOutRepeatClones(layout, row.state.key!); + + layout.setState({ rows }); + + // Remove behavior and the scoped local variable + row.setState({ $behaviors: row.state.$behaviors!.filter((b) => b !== this), $variables: undefined }); + } +} + +function updateLayout(layout: RowsLayoutManager, rows: RowItem[], rowKey: string) { + const allRows = getRowsFilterOutRepeatClones(layout, rowKey); + const index = allRows.findIndex((row) => row.state.key!.includes(rowKey)); + + if (index === -1) { + throw new Error('RowItemRepeaterBehavior: Row not found in layout'); + } + + layout.setState({ rows: [...allRows.slice(0, index), ...rows, ...allRows.slice(index + 1)] }); +} + +function getRowsFilterOutRepeatClones(layout: RowsLayoutManager, rowKey: string) { + return layout.state.rows.filter((rows) => !isClonedKeyOf(rows.state.key!, rowKey)); +} 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 22ead9389b5..73ca3fa3563 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -12,13 +12,16 @@ import { } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; +import { isClonedKey } from '../../utils/clone'; import { DashboardScene } from '../DashboardScene'; import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; +import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior'; import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager'; import { DashboardLayoutManager, LayoutRegistryItem } from '../types'; import { RowItem } from './RowItem'; +import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior'; interface RowsLayoutManagerState extends SceneObjectState { rows: RowItem[]; @@ -58,6 +61,10 @@ export class RowsLayoutManager extends SceneObjectBase i }); } + public getMaxPanelId(): number { + return Math.max(...this.state.rows.map((row) => row.getLayout().getMaxPanelId())); + } + public getNextPanelId(): number { return 0; } @@ -78,7 +85,7 @@ export class RowsLayoutManager extends SceneObjectBase i const panels: VizPanel[] = []; for (const row of this.state.rows) { - const innerPanels = row.state.layout.getVizPanels(); + const innerPanels = row.getLayout().getVizPanels(); panels.push(...innerPanels); } @@ -89,6 +96,23 @@ export class RowsLayoutManager extends SceneObjectBase i return []; } + public activateRepeaters() { + this.state.rows.forEach((row) => { + if (row.state.$behaviors) { + for (const behavior of row.state.$behaviors) { + if (behavior instanceof RowItemRepeaterBehavior && !row.isActive) { + row.activate(); + break; + } + } + + if (!row.getLayout().isActive) { + row.getLayout().activate(); + } + } + }); + } + public getDescriptor(): LayoutRegistryItem { return RowsLayoutManager.getDescriptor(); } @@ -111,11 +135,16 @@ export class RowsLayoutManager extends SceneObjectBase i } public static createFromLayout(layout: DashboardLayoutManager): RowsLayoutManager { + let rows: RowItem[]; + if (layout instanceof DefaultGridLayoutManager) { const config: Array<{ title?: string; isCollapsed?: boolean; + isDraggable?: boolean; + isResizable?: boolean; children: SceneGridItemLike[]; + repeat?: string; }> = []; let children: SceneGridItemLike[] | undefined; @@ -125,12 +154,19 @@ export class RowsLayoutManager extends SceneObjectBase i } if (child instanceof SceneGridRow) { - if (!child.state.key?.includes('-clone-')) { + if (!isClonedKey(child.state.key!)) { + const behaviour = child.state.$behaviors?.find((b) => b instanceof RowRepeaterBehavior); + config.push({ title: child.state.title, isCollapsed: !!child.state.isCollapsed, + isDraggable: child.state.isDraggable ?? layout.state.grid.state.isDraggable, + isResizable: child.state.isResizable ?? layout.state.grid.state.isResizable, children: child.state.children, + repeat: behaviour?.state.variableName, }); + + // Since we encountered a row item, any subsequent panels should be added to a new row children = undefined; } } else { @@ -143,21 +179,24 @@ export class RowsLayoutManager extends SceneObjectBase i } }); - const rows = config.map( + rows = config.map( (rowConfig) => new RowItem({ title: rowConfig.title ?? 'Row title', isCollapsed: !!rowConfig.isCollapsed, - layout: DefaultGridLayoutManager.fromGridItems(rowConfig.children), + layout: DefaultGridLayoutManager.fromGridItems( + rowConfig.children, + rowConfig.isDraggable, + rowConfig.isResizable + ), + $behaviors: rowConfig.repeat ? [new RowItemRepeaterBehavior({ variableName: rowConfig.repeat })] : [], }) ); - - return new RowsLayoutManager({ rows }); + } else { + rows = [new RowItem({ layout: layout.clone(), title: 'Row title' })]; } - const row = new RowItem({ layout: layout.clone(), title: 'Row title' }); - - return new RowsLayoutManager({ rows: [row] }); + return new RowsLayoutManager({ rows }); } public static Component = ({ model }: SceneComponentProps) => { @@ -167,7 +206,7 @@ export class RowsLayoutManager extends SceneObjectBase i return (
{rows.map((row) => ( - + ))}
); diff --git a/public/app/features/dashboard-scene/scene/types.ts b/public/app/features/dashboard-scene/scene/types.ts index a399e2d5938..fda406eaba8 100644 --- a/public/app/features/dashboard-scene/scene/types.ts +++ b/public/app/features/dashboard-scene/scene/types.ts @@ -10,50 +10,72 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan export interface DashboardLayoutManager extends SceneObject { /** Marks it as a DashboardLayoutManager */ isDashboardLayoutManager: true; + /** * Notify the layout manager that the edit mode has changed * @param isEditing */ editModeChanged(isEditing: boolean): void; + /** * Remove an element / panel * @param element */ removePanel(panel: VizPanel): void; + /** * Creates a copy of an existing element and adds it to the layout * @param element */ duplicatePanel(panel: VizPanel): void; + /** * Adds a new panel to the layout */ addPanel(panel: VizPanel): void; + /** * Add row */ addNewRow(): void; + /** * getVizPanels */ getVizPanels(): VizPanel[]; + /** * Turn into a save model * @param saveModel */ toSaveModel?(): any; + /** * For dynamic panels that need to be viewed in isolation (SoloRoute) */ activateRepeaters?(): void; + /** - * Get's the layout descriptor (which has the name and id) + * Gets the layout descriptor (which has the name and id) */ getDescriptor(): LayoutRegistryItem; + /** * Renders options and layout actions */ getOptions?(): OptionsPaneItemDescriptor[]; + + /** + * Create a clone of the layout manager given an ancestor key + * @param ancestorKey + * @param isSource + */ + cloneLayout?(ancestorKey: string, isSource: boolean): DashboardLayoutManager; + + /** + * Returns the highest panel id in the layout + */ + getMaxPanelId(): number; } export function isDashboardLayoutManager(obj: SceneObject): obj is DashboardLayoutManager { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 4a7b005805d..75be7ae3a42 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -81,11 +81,11 @@ import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { PanelNotices } from '../scene/PanelNotices'; import { PanelTimeRange } from '../scene/PanelTimeRange'; -import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { AngularDeprecation } from '../scene/angular/AngularDeprecation'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { RowActions } from '../scene/row-actions/RowActions'; +import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; +import { RowActions } from '../scene/layout-default/row-actions/RowActions'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState'; import { getDashboardSceneFor, getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils'; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 4f85a0f500c..d9e93e71642 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -30,9 +30,9 @@ import { DashboardDataDTO } from 'app/types'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { PanelTimeRange } from '../scene/PanelTimeRange'; -import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; +import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; import { NEW_LINK } from '../settings/links/utils'; import { getQueryRunnerFor } from '../utils/utils'; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index e483b3bfe19..77960f558ce 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -41,11 +41,11 @@ import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { PanelNotices } from '../scene/PanelNotices'; import { PanelTimeRange } from '../scene/PanelTimeRange'; -import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { AngularDeprecation } from '../scene/angular/AngularDeprecation'; import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { RowActions } from '../scene/row-actions/RowActions'; +import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; +import { RowActions } from '../scene/layout-default/row-actions/RowActions'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState'; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index a0fc2a828ec..fbe3b3c4c0d 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -25,9 +25,9 @@ import { DashboardDataDTO } from 'app/types'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; -import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; +import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; import { NEW_LINK } from '../settings/links/utils'; import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils'; import { getVizPanelKeyForPanelId } from '../utils/utils'; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index c5a945c86fc..89710e4c7f1 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -33,9 +33,10 @@ import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene } from '../scene/DashboardScene'; import { PanelTimeRange } from '../scene/PanelTimeRange'; -import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; +import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; +import { isClonedKey } from '../utils/clone'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { calculateGridItemDimensions, @@ -72,7 +73,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa if (child instanceof SceneGridRow) { // Skip repeat clones or when generating a snapshot - if (child.state.key!.indexOf('-clone-') > 0 && !isSnapshot) { + if (isClonedKey(child.state.key!) && !isSnapshot) { continue; } gridRowToSaveModel(child, panels, isSnapshot); diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index de87184820a..43121f54d8b 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -30,9 +30,9 @@ import { DashboardControls } from '../scene/DashboardControls'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; -import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; +import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2'; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 6253cf9a187..67cae374214 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -47,9 +47,10 @@ import { import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { PanelTimeRange } from '../scene/PanelTimeRange'; -import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; +import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; +import { isClonedKey } from '../utils/clone'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getLibraryPanelBehavior, @@ -174,7 +175,7 @@ function getGridLayoutItems( elements.push(gridItemToGridLayoutItemKind(child, isSnapshot)); } } else if (child instanceof SceneGridRow) { - if (child.state.key!.indexOf('-clone-') > 0 && !isSnapshot) { + if (isClonedKey(child.state.key!) && !isSnapshot) { // Skip repeat rows continue; } diff --git a/public/app/features/dashboard-scene/solo/useSoloPanel.test.tsx b/public/app/features/dashboard-scene/solo/useSoloPanel.test.tsx index 174374aaa25..95ce381024d 100644 --- a/public/app/features/dashboard-scene/solo/useSoloPanel.test.tsx +++ b/public/app/features/dashboard-scene/solo/useSoloPanel.test.tsx @@ -46,7 +46,7 @@ describe('useSoloPanel', () => { it('should return the cloned panel when panel is found', () => { const { dashboard } = setup(); - const { result } = renderHook(() => useSoloPanel(dashboard, 'panel-1_clone')); + const { result } = renderHook(() => useSoloPanel(dashboard, 'panel-1-clone-1')); const panel = findVizPanelByKey(dashboard, 'panel-1'); expect(result.current[0]).not.toBe(panel); diff --git a/public/app/features/dashboard-scene/solo/useSoloPanel.ts b/public/app/features/dashboard-scene/solo/useSoloPanel.ts index 8905a79ce32..3db708c0ae9 100644 --- a/public/app/features/dashboard-scene/solo/useSoloPanel.ts +++ b/public/app/features/dashboard-scene/solo/useSoloPanel.ts @@ -4,7 +4,8 @@ import { VizPanel, UrlSyncManager } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { DashboardRepeatsProcessedEvent } from '../scene/types'; -import { findVizPanelByKey, isPanelClone } from '../utils/utils'; +import { containsCloneKey } from '../utils/clone'; +import { findVizPanelByKey } from '../utils/utils'; export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPanel | undefined, string | undefined] { const [panel, setPanel] = useState(); @@ -26,7 +27,7 @@ export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPa if (panel) { activateParents(panel); setPanel(panel); - } else if (isPanelClone(panelId)) { + } else if (containsCloneKey(panelId)) { findRepeatClone(dashboard, panelId).then((panel) => { if (panel) { setPanel(panel); diff --git a/public/app/features/dashboard-scene/utils/clone.test.ts b/public/app/features/dashboard-scene/utils/clone.test.ts new file mode 100644 index 00000000000..28441c4dbe1 --- /dev/null +++ b/public/app/features/dashboard-scene/utils/clone.test.ts @@ -0,0 +1,118 @@ +import { + getCloneKey, + getOriginalKey, + isInCloneChain, + isClonedKey, + joinCloneKeys, + containsCloneKey, + getLastKeyFromClone, + isClonedKeyOf, +} from './clone'; + +describe('clone', () => { + describe('getCloneKey', () => { + it('should return the clone key', () => { + expect(getCloneKey('panel', 1)).toBe('panel-clone-1'); + expect(getCloneKey('panel-clone-2', 1)).toBe('panel-clone-1'); + }); + + it('should not alter ancestors', () => { + expect(getCloneKey('row-clone-1/panel', 2)).toBe('row-clone-1/panel-clone-2'); + expect(getCloneKey('tab-clone-0/row-clone-1/panel', 2)).toBe('tab-clone-0/row-clone-1/panel-clone-2'); + expect(getCloneKey('row-clone-1/panel-clone-3', 2)).toBe('row-clone-1/panel-clone-2'); + expect(getCloneKey('tab-clone-0/row-clone-1/panel-clone-3', 2)).toBe('tab-clone-0/row-clone-1/panel-clone-2'); + }); + }); + + describe('getOriginalKey', () => { + it('should return the original key', () => { + expect(getOriginalKey('panel')).toBe('panel'); + expect(getOriginalKey('panel-clone-1')).toBe('panel'); + expect(getOriginalKey('row-clone-1/panel-clone-2')).toBe('panel'); + expect(getOriginalKey('tab-clone-0/row-clone-1/panel-clone-2')).toBe('panel'); + }); + }); + + describe('isClonedKey', () => { + it('should return true for cloned keys', () => { + expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-2')).toBe(true); + expect(isClonedKey('row-clone-0/panel-clone-1')).toBe(true); + expect(isClonedKey('panel-clone-1')).toBe(true); + }); + + it('should return false for non-cloned keys', () => { + expect(isClonedKey('panel-clone-0')).toBe(false); + expect(isClonedKey('tab-clone-1/row-clone-2/panel-clone-0')).toBe(false); + expect(isClonedKey('row-clone-1/panel-clone-0')).toBe(false); + expect(isClonedKey('panel')).toBe(false); + expect(isClonedKey('tab-clone-1/row-clone-2/panel')).toBe(false); + expect(isClonedKey('row-clone-1/panel')).toBe(false); + }); + }); + + describe('isClonedKeyOf', () => { + it('should return true for cloned keys', () => { + expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel-clone-2')).toBe(true); + expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel')).toBe(true); + expect(isClonedKeyOf('panel-clone-2', 'panel-clone-2')).toBe(true); + expect(isClonedKeyOf('panel-clone-2', 'panel')).toBe(true); + }); + + it('should return false for non-cloned keys', () => { + expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel2-clone-2')).toBe(false); + expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel2')).toBe(false); + expect(isClonedKeyOf('panel-clone-2', 'panel2-clone-2')).toBe(false); + expect(isClonedKeyOf('panel-clone-2', 'panel2')).toBe(false); + }); + }); + + describe('isInCloneChain', () => { + it('should return true for keys with cloned ancestors', () => { + expect(isInCloneChain('tab-clone-1/row-clone-0/panel-clone-0')).toBe(true); + expect(isInCloneChain('row-clone-0/row-clone-1/panel-clone-0')).toBe(true); + expect(isInCloneChain('row-clone-0/row-clone-0/panel-clone-1')).toBe(true); + expect(isInCloneChain('panel-clone-1')).toBe(true); + }); + + it('should return false for keys without cloned ancestors', () => { + expect(isInCloneChain('panel-clone-0')).toBe(false); + expect(isInCloneChain('row-clone-0/panel-clone-0')).toBe(false); + expect(isInCloneChain('tab-clone-0/row-clone-0/panel-clone-0')).toBe(false); + expect(isInCloneChain('panel')).toBe(false); + expect(isInCloneChain('tab-clone-0/row-clone-0/panel')).toBe(false); + expect(isInCloneChain('tab-clone-0/row/panel')).toBe(false); + expect(isInCloneChain('tab-clone-0/row/panel-0')).toBe(false); + expect(isInCloneChain('tab/row-clone-0/panel-0')).toBe(false); + expect(isInCloneChain('row-clone-0/panel')).toBe(false); + }); + }); + + describe('getLastKeyFromClone', () => { + it('should return the last key', () => { + expect(getLastKeyFromClone('tab-clone-1/row-clone-2/panel-clone-3')).toBe('panel-clone-3'); + expect(getLastKeyFromClone('row-clone-1/panel-clone-2')).toBe('panel-clone-2'); + expect(getLastKeyFromClone('row-clone-1/panel')).toBe('panel'); + expect(getLastKeyFromClone('panel')).toBe('panel'); + }); + }); + + describe('joinCloneKeys', () => { + it('should join keys with a separator', () => { + expect(joinCloneKeys('row', 'panel-clone-1')).toBe('row/panel-clone-1'); + }); + }); + + describe('containsCloneKey', () => { + it('should return true for keys with clone key', () => { + expect(containsCloneKey('row-clone-0/panel-clone-1')).toBe(true); + expect(containsCloneKey('tab-clone-0/row-clone-1/panel-clone-2')).toBe(true); + expect(containsCloneKey('panel-clone-1')).toBe(true); + }); + + it('should return false for keys without clone key', () => { + expect(containsCloneKey('panel')).toBe(false); + expect(containsCloneKey('tab-0/row-1/panel-2')).toBe(false); + expect(containsCloneKey('row-1/panel-2')).toBe(false); + }); + }); +}); diff --git a/public/app/features/dashboard-scene/utils/clone.ts b/public/app/features/dashboard-scene/utils/clone.ts new file mode 100644 index 00000000000..4e5d2b79fab --- /dev/null +++ b/public/app/features/dashboard-scene/utils/clone.ts @@ -0,0 +1,73 @@ +const CLONE_KEY = '-clone-'; +const CLONE_SEPARATOR = '/'; + +const CLONED_KEY_REGEX = new RegExp(`${CLONE_KEY}[1-9]+$`); +const ORIGINAL_REGEX = new RegExp(`${CLONE_KEY}\\d+$`); + +/** + * Create or alter the last key for a key + * @param key + * @param index + */ +export function getCloneKey(key: string, index: number): string { + const parts = key.split(CLONE_SEPARATOR).slice(0, -1); + const lastKey = getOriginalKey(getLastKeyFromClone(key)); + return [...parts, `${lastKey}${CLONE_KEY}${index}`].join(CLONE_SEPARATOR); +} + +/** + * Get the original key from a clone key + * @param key + */ +export function getOriginalKey(key: string): string { + return getLastKeyFromClone(key).replace(ORIGINAL_REGEX, ''); +} + +/** + * Checks if the last key is a clone key + * @param key + */ +export function isClonedKey(key: string): boolean { + return CLONED_KEY_REGEX.test(getLastKeyFromClone(key)); +} + +/** + * Checks if key1 is a clone of key2 + * @param key1 + * @param key2 + */ +export function isClonedKeyOf(key1: string, key2: string): boolean { + return isClonedKey(key1) && getOriginalKey(key1) === getOriginalKey(key2); +} + +/** + * Checks if the key or any of its ancestors are cloned + * @param key + */ +export function isInCloneChain(key: string): boolean { + return key.split(CLONE_SEPARATOR).some(isClonedKey); +} + +/** + * Get the last key from a clone key + * @param key + */ +export function getLastKeyFromClone(key: string): string { + return key.split(CLONE_SEPARATOR).pop() ?? ''; +} + +/** + * Join clone keys + * @param keys + */ +export function joinCloneKeys(...keys: string[]): string { + return keys.filter(Boolean).join(CLONE_SEPARATOR); +} + +/** + * Checks if a key contains the '-clone-' string + * @param key + */ +export function containsCloneKey(key: string): boolean { + return key.includes(CLONE_KEY); +} diff --git a/public/app/features/dashboard-scene/utils/test-utils.ts b/public/app/features/dashboard-scene/utils/test-utils.ts index ebcf45d0c5d..40a547f68e1 100644 --- a/public/app/features/dashboard-scene/utils/test-utils.ts +++ b/public/app/features/dashboard-scene/utils/test-utils.ts @@ -17,9 +17,9 @@ import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/co import { DashboardDTO } from 'app/types'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; -import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; +import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; export function setupLoadDashboardMock(rsp: DeepPartial, spy?: jest.Mock) { const loadDashboardMock = (spy || jest.fn()).mockResolvedValue(rsp); diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 8a288f9b1a4..64df007a931 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -21,6 +21,8 @@ import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types'; +import { getLastKeyFromClone, getOriginalKey } from './clone'; + export const NEW_PANEL_HEIGHT = 8; export const NEW_PANEL_WIDTH = 12; @@ -29,7 +31,7 @@ export function getVizPanelKeyForPanelId(panelId: number) { } export function getPanelIdForVizPanel(panel: SceneObject): number { - return parseInt(panel.state.key!.replace('panel-', ''), 10); + return parseInt(getOriginalKey(panel.state.key!).replace('panel-', ''), 10); } /** @@ -62,7 +64,7 @@ function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizP const panel = sceneGraph.findObject(scene, (obj) => { const objKey = obj.state.key!; - if (objKey === key) { + if (objKey === key || getLastKeyFromClone(objKey) === getLastKeyFromClone(key) || getOriginalKey(objKey) === key) { return true; } @@ -212,30 +214,6 @@ export function getClosestVizPanel(sceneObject: SceneObject): VizPanel | null { return null; } -export function isPanelClone(key: string) { - return key.includes('clone'); -} - -/** - * Recursivly check the scene graph up until it finds a read only clone. - * If the key contains clone-0 it is the reference object and can be edited - */ -export function isReadOnlyClone(sceneObject: SceneObject): boolean { - const key = sceneObject.state.key!; - - // Regular expression to match 'clone-' followed by a number, but not 'clone-0' as the is the reference object - const pattern = /clone-(?!0)/; - if (pattern.test(key)) { - return true; - } - - if (sceneObject.parent) { - return isReadOnlyClone(sceneObject.parent); - } - - return false; -} - export function getDefaultVizPanel(): VizPanel { return new VizPanel({ title: 'Panel Title', @@ -351,3 +329,7 @@ export function getLayoutManagerFor(sceneObject: SceneObject): DashboardLayoutMa throw new Error('Could not find layout manager for scene object'); } + +export function getGridItemKeyForPanelId(panelId: number): string { + return `grid-item-${panelId}`; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 3a270e122c3..9f7da371ab5 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -882,6 +882,36 @@ "redirect-link": "List in Grafana Alerting", "subtitle": "Alert rules related to this dashboard" }, + "default-layout": { + "row-actions": { + "delete": "Delete row", + "modal": { + "alt-action": "Delete row only", + "text": "Are you sure you want to remove this row and all its panels?", + "title": "Delete row" + }, + "repeat": { + "warning": { + "learn-more": "Learn more", + "text": "Panels in this row use the {{SHARED_DASHBOARD_QUERY}} data source. These panels will reference the panel in the original row, not the ones in the repeated rows." + } + } + }, + "row-options": { + "button": { + "label": "Row options" + }, + "form": { + "cancel": "Cancel", + "repeat-for": "Repeat for", + "title": "Title", + "update": "Update" + }, + "modal": { + "title": "Row options" + } + } + }, "empty": { "add-library-panel-body": "Add visualizations that are shared with other dashboards.", "add-library-panel-button": "Add library panel", @@ -954,6 +984,14 @@ "no-rules": "There are no alert rules linked to this panel." } }, + "rows-layout": { + "row": { + "repeat": { + "learn-more": "Learn more", + "warning": "Panels in this row use the {{SHARED_DASHBOARD_QUERY}} data source. These panels will reference the panel in the original row, not the ones in the repeated rows." + } + } + }, "toolbar": { "add": "Add", "alert-rules": "Alert rules", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 1495a78caee..b2558bd0edb 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -882,6 +882,36 @@ "redirect-link": "Ŀįşŧ įʼn Ğřäƒäʼnä Åľęřŧįʼnģ", "subtitle": "Åľęřŧ řūľęş řęľäŧęđ ŧő ŧĥįş đäşĥþőäřđ" }, + "default-layout": { + "row-actions": { + "delete": "Đęľęŧę řőŵ", + "modal": { + "alt-action": "Đęľęŧę řőŵ őʼnľy", + "text": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő řęmővę ŧĥįş řőŵ äʼnđ äľľ įŧş päʼnęľş?", + "title": "Đęľęŧę řőŵ" + }, + "repeat": { + "warning": { + "learn-more": "Ŀęäřʼn mőřę", + "text": "Päʼnęľş įʼn ŧĥįş řőŵ ūşę ŧĥę {{SHARED_DASHBOARD_QUERY}} đäŧä şőūřčę. Ŧĥęşę päʼnęľş ŵįľľ řęƒęřęʼnčę ŧĥę päʼnęľ įʼn ŧĥę őřįģįʼnäľ řőŵ, ʼnőŧ ŧĥę őʼnęş įʼn ŧĥę řępęäŧęđ řőŵş." + } + } + }, + "row-options": { + "button": { + "label": "Ŗőŵ őpŧįőʼnş" + }, + "form": { + "cancel": "Cäʼnčęľ", + "repeat-for": "Ŗępęäŧ ƒőř", + "title": "Ŧįŧľę", + "update": "Ůpđäŧę" + }, + "modal": { + "title": "Ŗőŵ őpŧįőʼnş" + } + } + }, "empty": { "add-library-panel-body": "Åđđ vįşūäľįžäŧįőʼnş ŧĥäŧ äřę şĥäřęđ ŵįŧĥ őŧĥęř đäşĥþőäřđş.", "add-library-panel-button": "Åđđ ľįþřäřy päʼnęľ", @@ -954,6 +984,14 @@ "no-rules": "Ŧĥęřę äřę ʼnő äľęřŧ řūľęş ľįʼnĸęđ ŧő ŧĥįş päʼnęľ." } }, + "rows-layout": { + "row": { + "repeat": { + "learn-more": "Ŀęäřʼn mőřę", + "warning": "Päʼnęľş įʼn ŧĥįş řőŵ ūşę ŧĥę {{SHARED_DASHBOARD_QUERY}} đäŧä şőūřčę. Ŧĥęşę päʼnęľş ŵįľľ řęƒęřęʼnčę ŧĥę päʼnęľ įʼn ŧĥę őřįģįʼnäľ řőŵ, ʼnőŧ ŧĥę őʼnęş įʼn ŧĥę řępęäŧęđ řőŵş." + } + } + }, "toolbar": { "add": "Åđđ", "alert-rules": "Åľęřŧ řūľęş", From 74e3beabd06db23e36e480fff6662fb330e1cf89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Mon, 3 Feb 2025 11:41:54 +0100 Subject: [PATCH 288/894] MultiCombobox: add `CustomValue` as an option (#99815) --- .betterer.results | 6 ++-- .../src/components/Combobox/Combobox.test.tsx | 17 ++++++---- .../src/components/Combobox/Combobox.tsx | 8 ++--- .../Combobox/MultiCombobox.test.tsx | 34 +++++++++++++++++++ .../src/components/Combobox/MultiCombobox.tsx | 19 ++++++++--- .../src/components/Combobox/filter.ts | 3 -- .../src/components/Combobox/useOptions.ts | 34 ++++++++++++++++--- public/locales/en-US/grafana.json | 2 +- public/locales/pseudo-LOCALE/grafana.json | 2 +- 9 files changed, 97 insertions(+), 28 deletions(-) diff --git a/.betterer.results b/.betterer.results index 160b522a8c2..06a027a4207 100644 --- a/.betterer.results +++ b/.betterer.results @@ -538,8 +538,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] ], "packages/grafana-ui/src/components/Combobox/Combobox.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -547,6 +546,9 @@ exports[`better eslint`] = { "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"] ], diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx index 7787e38d00c..7ae3f1c5ec5 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx @@ -237,11 +237,11 @@ describe('Combobox', () => { const onChangeHandler = jest.fn(); render(); const input = screen.getByRole('combobox'); - await userEvent.type(input, 'custom value'); + await userEvent.type(input, 'Use custom value'); await userEvent.keyboard('{Enter}'); - expect(screen.getByDisplayValue('custom value')).toBeInTheDocument(); - expect(onChangeHandler).toHaveBeenCalledWith(expect.objectContaining({ value: 'custom value' })); + expect(screen.getByDisplayValue('Use custom value')).toBeInTheDocument(); + expect(onChangeHandler).toHaveBeenCalledWith(expect.objectContaining({ value: 'Use custom value' })); }); it('should provide custom string when all options are numbers', async () => { @@ -256,10 +256,10 @@ describe('Combobox', () => { render(); const input = screen.getByRole('combobox'); - await userEvent.type(input, 'custom value'); + await userEvent.type(input, 'Use custom value'); await userEvent.keyboard('{Enter}'); - expect(screen.getByDisplayValue('custom value')).toBeInTheDocument(); + expect(screen.getByDisplayValue('Use custom value')).toBeInTheDocument(); expect(typeof onChangeHandler.mock.calls[0][0].value === 'string').toBeTruthy(); expect(typeof onChangeHandler.mock.calls[0][0].value === 'number').toBeFalsy(); @@ -411,9 +411,12 @@ describe('Combobox', () => { jest.advanceTimersByTime(500); // Custom value while typing }); - const customItem = screen.queryByRole('option', { name: 'Custom value: fir' }); - + const customItem = screen.getByRole('option'); + const customValue = customItem.getElementsByTagName('span')[0].textContent; + const customDescription = customItem.getElementsByTagName('span')[1].textContent; expect(customItem).toBeInTheDocument(); + expect(customValue).toBe('fir'); + expect(customDescription).toBe('Use custom value'); }); it('should display message when there is an error loading async options', async () => { diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index cf59c4910f8..4c2f6a97b63 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -135,12 +135,10 @@ export const Combobox = (props: ComboboxProps) => if (!optionMatchingInput) { const customValueOption = { - label: t('combobox.custom-value.label', 'Custom value: ') + inputValue, + label: inputValue, // Type casting needed to make this work when T is a number - value: inputValue as unknown as T, - /* TODO: Add this back when we do support descriptions and have need for it - description: t('combobox.custom-value.create', 'Create custom value'), - */ + value: inputValue as T, + description: t('combobox.custom-value.description', 'Use custom value'), }; itemsToSet = items.slice(0); diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx index fa1274b5666..bee10de6783 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx @@ -128,6 +128,40 @@ describe('MultiCombobox', () => { expect(await screen.findByText('d')).toBeInTheDocument(); }); + it('should be able to set custom value', async () => { + const options = [ + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + ]; + const onChange = jest.fn(); + render(); + const input = screen.getByRole('combobox'); + await user.click(input); + await user.type(input, 'D'); + await user.keyboard('{arrowdown}{enter}'); + expect(onChange).toHaveBeenCalledWith([{ label: 'D', value: 'D', description: 'Use custom value' }]); + }); + + it('should be able to add custom value to the selected options', async () => { + const options = [ + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + ]; + const onChange = jest.fn(); + render(); + const input = screen.getByRole('combobox'); + await user.click(input); + await user.type(input, 'D'); + await user.keyboard('{arrowdown}{enter}'); + expect(onChange).toHaveBeenCalledWith([ + { value: 'a' }, + { value: 'c' }, + { label: 'D', value: 'D', description: 'Use custom value' }, + ]); + }); + it('should remove value when clicking on the close icon of the pill', async () => { const options = [ { label: 'A', value: 'a' }, diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index a2d5063b096..849e2a26271 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -37,8 +37,19 @@ interface MultiComboboxBaseProps extends Omit = MultiComboboxBaseProps & AutoSizeConditionals; export const MultiCombobox = (props: MultiComboboxProps) => { - const { placeholder, onChange, value, width, enableAllOption, invalid, disabled, minWidth, maxWidth, isClearable } = - props; + const { + placeholder, + onChange, + value, + width, + enableAllOption, + invalid, + disabled, + minWidth, + maxWidth, + isClearable, + createCustomValue = false, + } = props; const styles = useStyles2(getComboboxStyles); const [inputValue, setInputValue] = useState(''); @@ -55,7 +66,7 @@ export const MultiCombobox = (props: MultiComboboxPro }, [inputValue]); // Handle async options and the 'All' option - const { options: baseOptions, updateOptions, asyncLoading } = useOptions(props.options); + const { options: baseOptions, updateOptions, asyncLoading } = useOptions(props.options, createCustomValue); const options = useMemo(() => { // Only add the 'All' option if there's more than 1 option const addAllOption = enableAllOption && baseOptions.length > 1; @@ -202,14 +213,12 @@ export const MultiCombobox = (props: MultiComboboxPro const filteredSet = new Set(realOptions.map((item) => item.value)); newSelectedItems = selectedItems.filter((item) => !filteredSet.has(item.value)); } - setSelectedItems(newSelectedItems); } else if (newSelectedItem && isOptionSelected(newSelectedItem)) { removeSelectedItem(newSelectedItem); } else if (newSelectedItem) { addSelectedItem(newSelectedItem); } - break; case useCombobox.stateChangeTypes.InputChange: setInputValue(newInputValue ?? ''); diff --git a/packages/grafana-ui/src/components/Combobox/filter.ts b/packages/grafana-ui/src/components/Combobox/filter.ts index cb7a14d86fe..a03cbec475c 100644 --- a/packages/grafana-ui/src/components/Combobox/filter.ts +++ b/packages/grafana-ui/src/components/Combobox/filter.ts @@ -20,9 +20,6 @@ export function itemToString(item?: ComboboxOption if (item == null) { return ''; } - if (item.label?.startsWith('Custom value: ')) { - return item.value.toString(); - } return item.label ?? item.value.toString(); } diff --git a/packages/grafana-ui/src/components/Combobox/useOptions.ts b/packages/grafana-ui/src/components/Combobox/useOptions.ts index 504f3584e1d..a66e60a2747 100644 --- a/packages/grafana-ui/src/components/Combobox/useOptions.ts +++ b/packages/grafana-ui/src/components/Combobox/useOptions.ts @@ -1,6 +1,8 @@ import { debounce } from 'lodash'; import { useState, useCallback, useMemo } from 'react'; +import { t } from '../../utils/i18n'; + import { itemFilter } from './filter'; import { ComboboxOption } from './types'; import { StaleResultError, useLatestAsyncCall } from './useLatestAsyncCall'; @@ -20,7 +22,7 @@ const asyncNoop = () => Promise.resolve([]); * - function to call when user types (to filter, or call async fn) * - loading and error states */ -export function useOptions(rawOptions: AsyncOptions) { +export function useOptions(rawOptions: AsyncOptions, createCustomValue: boolean) { const isAsync = typeof rawOptions === 'function'; const loadOptions = useLatestAsyncCall(isAsync ? rawOptions : asyncNoop); @@ -56,6 +58,27 @@ export function useOptions(rawOptions: AsyncOptions>) => { + let currentOptions: Array> = opts; + if (createCustomValue && userTypedSearch) { + const customValueExists = opts.some((opt) => opt.value === userTypedSearch); + if (!customValueExists) { + currentOptions = [ + { + label: userTypedSearch, + value: userTypedSearch as T, + description: t('combobox.custom-value.description', 'Use custom value'), + }, + ...currentOptions, + ]; + } + } + return currentOptions; + }, + [createCustomValue, userTypedSearch] + ); + const updateOptions = useCallback( (inputValue: string) => { if (!isAsync) { @@ -71,12 +94,15 @@ export function useOptions(rawOptions: AsyncOptions { + let currentOptions = []; if (isAsync) { - return asyncOptions; + currentOptions = addCustomValue(asyncOptions); } else { - return rawOptions.filter(itemFilter(userTypedSearch)); + currentOptions = addCustomValue(rawOptions.filter(itemFilter(userTypedSearch))); } - }, [rawOptions, asyncOptions, isAsync, userTypedSearch]); + + return currentOptions; + }, [isAsync, addCustomValue, asyncOptions, rawOptions, userTypedSearch]); return { options: finalOptions, updateOptions, asyncLoading, asyncError }; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 9f7da371ab5..92612e85853 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -709,7 +709,7 @@ "title": "Clear value" }, "custom-value": { - "label": "Custom value: " + "description": "Use custom value" }, "options": { "no-found": "No options found." diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index b2558bd0edb..b64424a9734 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -709,7 +709,7 @@ "title": "Cľęäř väľūę" }, "custom-value": { - "label": "Cūşŧőm väľūę: " + "description": "Ůşę čūşŧőm väľūę" }, "options": { "no-found": "Ńő őpŧįőʼnş ƒőūʼnđ." From 4cd2ebe186e31e7b23925e6635c8fa271105d88e Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 3 Feb 2025 11:45:17 +0100 Subject: [PATCH 289/894] Grafana Advisor: Automatically generate frontend types (#99807) * Grafana Advisor: Frontend types * fix bad merge --- apps/advisor/.gitignore | 1 + apps/advisor/kinds/check.cue | 29 +++--- .../apis/advisor/v0alpha1/check_status_gen.go | 48 +++++----- .../apis/advisor/v0alpha1/zz_openapi_gen.go | 92 +++++++++---------- .../pkg/app/checks/datasourcecheck/check.go | 10 +- .../pkg/app/checks/plugincheck/check.go | 10 +- .../pkg/app/checks/plugincheck/check_test.go | 20 ++-- apps/advisor/plugin/README.md | 7 ++ 8 files changed, 114 insertions(+), 103 deletions(-) create mode 100644 apps/advisor/.gitignore create mode 100644 apps/advisor/plugin/README.md diff --git a/apps/advisor/.gitignore b/apps/advisor/.gitignore new file mode 100644 index 00000000000..9e73bafbae8 --- /dev/null +++ b/apps/advisor/.gitignore @@ -0,0 +1 @@ +plugin/src \ No newline at end of file diff --git a/apps/advisor/kinds/check.cue b/apps/advisor/kinds/check.cue index cf1cfd58d39..fdf641abdf3 100644 --- a/apps/advisor/kinds/check.cue +++ b/apps/advisor/kinds/check.cue @@ -7,7 +7,7 @@ check: { versions: { "v0alpha1": { codegen: { - frontend: false + frontend: true backend: true } validation: { @@ -17,24 +17,27 @@ check: { ] } schema: { - spec: { + #Data: { // Generic data input that a check can receive data?: [string]: string } - status: { - report: { + #ReportError: { + // Severity of the error + severity: "high" | "low" + // Human readable reason for the error + reason: string + // Action to take to resolve the error + action: string + } + #Report: { // Number of elements analyzed count: int // List of errors - errors: [...{ - // Severity of the error - severity: "high" | "low" - // Human readable reason for the error - reason: string - // Action to take to resolve the error - action: string - }] - } + errors: [...#ReportError] + } + spec: #Data + status: { + report: #Report } } } diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go index 057ed6c4b7e..ac73ee86ec4 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go @@ -2,6 +2,21 @@ package v0alpha1 +// +k8s:openapi-gen=true +type CheckReportError struct { + // Severity of the error + Severity CheckReportErrorSeverity `json:"severity"` + // Human readable reason for the error + Reason string `json:"reason"` + // Action to take to resolve the error + Action string `json:"action"` +} + +// NewCheckReportError creates a new CheckReportError object. +func NewCheckReportError() *CheckReportError { + return &CheckReportError{} +} + // +k8s:openapi-gen=true type CheckstatusOperatorState struct { // lastEvaluation is the ResourceVersion last evaluated @@ -37,6 +52,14 @@ func NewCheckStatus() *CheckStatus { } } +// +k8s:openapi-gen=true +type CheckReportErrorSeverity string + +const ( + CheckReportErrorSeverityHigh CheckReportErrorSeverity = "high" + CheckReportErrorSeverityLow CheckReportErrorSeverity = "low" +) + // +k8s:openapi-gen=true type CheckStatusOperatorStateState string @@ -46,35 +69,12 @@ const ( CheckStatusOperatorStateStateFailed CheckStatusOperatorStateState = "failed" ) -// +k8s:openapi-gen=true -type CheckStatusSeverity string - -const ( - CheckStatusSeverityHigh CheckStatusSeverity = "high" - CheckStatusSeverityLow CheckStatusSeverity = "low" -) - -// +k8s:openapi-gen=true -type CheckV0alpha1StatusReportErrors struct { - // Severity of the error - Severity CheckStatusSeverity `json:"severity"` - // Human readable reason for the error - Reason string `json:"reason"` - // Action to take to resolve the error - Action string `json:"action"` -} - -// NewCheckV0alpha1StatusReportErrors creates a new CheckV0alpha1StatusReportErrors object. -func NewCheckV0alpha1StatusReportErrors() *CheckV0alpha1StatusReportErrors { - return &CheckV0alpha1StatusReportErrors{} -} - // +k8s:openapi-gen=true type CheckV0alpha1StatusReport struct { // Number of elements analyzed Count int64 `json:"count"` // List of errors - Errors []CheckV0alpha1StatusReportErrors `json:"errors"` + Errors []CheckReportError `json:"errors"` } // NewCheckV0alpha1StatusReport creates a new CheckV0alpha1StatusReport object. diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go index a78d033bb26..74ccb4861e6 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go @@ -12,13 +12,13 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.Check": schema_pkg_apis_advisor_v0alpha1_Check(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckList": schema_pkg_apis_advisor_v0alpha1_CheckList(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec": schema_pkg_apis_advisor_v0alpha1_CheckSpec(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckStatus": schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport": schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReportErrors": schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReportErrors(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState": schema_pkg_apis_advisor_v0alpha1_CheckstatusOperatorState(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.Check": schema_pkg_apis_advisor_v0alpha1_Check(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckList": schema_pkg_apis_advisor_v0alpha1_CheckList(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportError": schema_pkg_apis_advisor_v0alpha1_CheckReportError(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec": schema_pkg_apis_advisor_v0alpha1_CheckSpec(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckStatus": schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport": schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState": schema_pkg_apis_advisor_v0alpha1_CheckstatusOperatorState(ref), } } @@ -117,6 +117,43 @@ func schema_pkg_apis_advisor_v0alpha1_CheckList(ref common.ReferenceCallback) co } } +func schema_pkg_apis_advisor_v0alpha1_CheckReportError(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "severity": { + SchemaProps: spec.SchemaProps{ + Description: "Severity of the error", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "reason": { + SchemaProps: spec.SchemaProps{ + Description: "Human readable reason for the error", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "action": { + SchemaProps: spec.SchemaProps{ + Description: "Action to take to resolve the error", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"severity", "reason", "action"}, + }, + }, + } +} + func schema_pkg_apis_advisor_v0alpha1_CheckSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -218,7 +255,7 @@ func schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref common.Refer Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReportErrors"), + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportError"), }, }, }, @@ -229,44 +266,7 @@ func schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReportErrors"}, - } -} - -func schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReportErrors(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "severity": { - SchemaProps: spec.SchemaProps{ - Description: "Severity of the error", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "reason": { - SchemaProps: spec.SchemaProps{ - Description: "Human readable reason for the error", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "action": { - SchemaProps: spec.SchemaProps{ - Description: "Action to take to resolve the error", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"severity", "reason", "action"}, - }, - }, + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportError"}, } } diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go index 8ff4c1a1dc3..5c5d7d8bdcc 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -49,13 +49,13 @@ func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.Check return nil, err } - dsErrs := []advisor.CheckV0alpha1StatusReportErrors{} + dsErrs := []advisor.CheckReportError{} for _, ds := range dss { // Data source UID validation err := util.ValidateUID(ds.UID) if err != nil { - dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{ - Severity: advisor.CheckStatusSeverityLow, + dsErrs = append(dsErrs, advisor.CheckReportError{ + Severity: advisor.CheckReportErrorSeverityLow, Reason: fmt.Sprintf("Invalid UID '%s' for data source %s", ds.UID, ds.Name), Action: "Check the documentation for more information.", }) @@ -81,8 +81,8 @@ func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.Check continue } if resp.Status != backend.HealthStatusOk { - dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{ - Severity: advisor.CheckStatusSeverityHigh, + dsErrs = append(dsErrs, advisor.CheckReportError{ + Severity: advisor.CheckReportErrorSeverityHigh, Reason: fmt.Sprintf("Health check failed for %s", ds.Name), Action: fmt.Sprintf( "Go to the data source configuration"+ diff --git a/apps/advisor/pkg/app/checks/plugincheck/check.go b/apps/advisor/pkg/app/checks/plugincheck/check.go index 793fc935946..8a20820f450 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check.go @@ -43,7 +43,7 @@ func (c *check) Type() string { func (c *check) Run(ctx context.Context, _ *advisor.CheckSpec) (*advisor.CheckV0alpha1StatusReport, error) { ps := c.PluginStore.Plugins(ctx) - errs := []advisor.CheckV0alpha1StatusReportErrors{} + errs := []advisor.CheckReportError{} for _, p := range ps { // Skip if it's a core plugin if p.IsCorePlugin() { @@ -56,8 +56,8 @@ func (c *check) Run(ctx context.Context, _ *advisor.CheckSpec) (*advisor.CheckV0 continue } if i.Status == "deprecated" { - errs = append(errs, advisor.CheckV0alpha1StatusReportErrors{ - Severity: advisor.CheckStatusSeverityHigh, + errs = append(errs, advisor.CheckReportError{ + Severity: advisor.CheckReportErrorSeverityHigh, Reason: fmt.Sprintf("Plugin deprecated: %s", p.ID), Action: "Check the documentation for recommended steps.", }) @@ -73,8 +73,8 @@ func (c *check) Run(ctx context.Context, _ *advisor.CheckSpec) (*advisor.CheckV0 continue } if hasUpdate(p, info) { - errs = append(errs, advisor.CheckV0alpha1StatusReportErrors{ - Severity: advisor.CheckStatusSeverityLow, + errs = append(errs, advisor.CheckReportError{ + Severity: advisor.CheckReportErrorSeverityLow, Reason: fmt.Sprintf("New version available for %s", p.ID), Action: fmt.Sprintf( "Go to the plugin admin page"+ diff --git a/apps/advisor/pkg/app/checks/plugincheck/check_test.go b/apps/advisor/pkg/app/checks/plugincheck/check_test.go index 3fa7f1b20cb..41c0705e904 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check_test.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check_test.go @@ -21,12 +21,12 @@ func TestRun(t *testing.T) { pluginArchives map[string]*repo.PluginArchiveInfo pluginPreinstalled []string pluginManaged []string - expectedErrors []advisor.CheckV0alpha1StatusReportErrors + expectedErrors []advisor.CheckReportError }{ { name: "No plugins", plugins: []pluginstore.Plugin{}, - expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{}, + expectedErrors: []advisor.CheckReportError{}, }, { name: "Deprecated plugin", @@ -39,9 +39,9 @@ func TestRun(t *testing.T) { pluginArchives: map[string]*repo.PluginArchiveInfo{ "plugin1": {Version: "1.0.0"}, }, - expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{ + expectedErrors: []advisor.CheckReportError{ { - Severity: advisor.CheckStatusSeverityHigh, + Severity: advisor.CheckReportErrorSeverityHigh, Reason: "Plugin deprecated: plugin1", Action: "Check the documentation for recommended steps.", }, @@ -58,9 +58,9 @@ func TestRun(t *testing.T) { pluginArchives: map[string]*repo.PluginArchiveInfo{ "plugin2": {Version: "1.1.0"}, }, - expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{ + expectedErrors: []advisor.CheckReportError{ { - Severity: advisor.CheckStatusSeverityLow, + Severity: advisor.CheckReportErrorSeverityLow, Reason: "New version available for plugin2", Action: "Go to the plugin admin page and upgrade to the latest version.", }, @@ -77,9 +77,9 @@ func TestRun(t *testing.T) { pluginArchives: map[string]*repo.PluginArchiveInfo{ "plugin2": {Version: "beta"}, }, - expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{ + expectedErrors: []advisor.CheckReportError{ { - Severity: advisor.CheckStatusSeverityLow, + Severity: advisor.CheckReportErrorSeverityLow, Reason: "New version available for plugin2", Action: "Go to the plugin admin page and upgrade to the latest version.", }, @@ -97,7 +97,7 @@ func TestRun(t *testing.T) { "plugin3": {Version: "1.1.0"}, }, pluginPreinstalled: []string{"plugin3"}, - expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{}, + expectedErrors: []advisor.CheckReportError{}, }, { name: "Managed plugin", @@ -111,7 +111,7 @@ func TestRun(t *testing.T) { "plugin4": {Version: "1.1.0"}, }, pluginManaged: []string{"plugin4"}, - expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{}, + expectedErrors: []advisor.CheckReportError{}, }, } diff --git a/apps/advisor/plugin/README.md b/apps/advisor/plugin/README.md new file mode 100644 index 00000000000..5b9e2bc8744 --- /dev/null +++ b/apps/advisor/plugin/README.md @@ -0,0 +1,7 @@ +This folder contains the automatically generated types for the frontend that are used in the app plugin. + +To update the types: + +1. Make any necessary changes in the `kinds` directory +2. Run `make generate` +3. Copy the `plugin` directory to the frontend app: https://github.com/grafana/grafana-advisor-app/tree/main/src/plugin From d16374d339eb400f7e2b7565aca0457a7e2559c0 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Mon, 3 Feb 2025 12:14:28 +0100 Subject: [PATCH 290/894] Authz: For list collect all folder permisions into items (#99955) * For list collect all folder permisions into items --------- Co-authored-by: Gabriel MABILLE --- pkg/services/authz/rbac/service.go | 70 +++++++++++++++++-------- pkg/services/authz/rbac/service_test.go | 46 +++++++++++----- 2 files changed, 83 insertions(+), 33 deletions(-) diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index 9c8a82a8ee1..26bd352903c 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -619,27 +619,28 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, } } - folderSet := make(map[string]struct{}, len(scopeMap)) - - prefix := t.prefix() - itemSet := make(map[string]struct{}, len(scopeMap)) - for scope := range scopeMap { - if strings.HasPrefix(scope, "folders:uid:") { - identifier := strings.TrimPrefix(scope, "folders:uid:") - if _, ok := folderSet[identifier]; ok { - continue - } - folderSet[identifier] = struct{}{} - getChildren(folderMap, identifier, folderSet) - } else { - identifier := strings.TrimPrefix(scope, prefix) - itemSet[identifier] = struct{}{} - } + var res *authzv1.ListResponse + if strings.HasPrefix(req.Action, "folders:") { + res = buildFolderList(scopeMap, folderMap) + } else { + res = buildItemList(scopeMap, folderMap, t.prefix()) } - folderList := make([]string, 0, len(folderSet)) - for folder := range folderSet { - folderList = append(folderList, folder) + span.SetAttributes(attribute.Int("num_folders", len(res.Folders)), attribute.Int("num_items", len(res.Items))) + return res, nil +} + +func buildFolderList(scopes map[string]bool, tree map[string]FolderNode) *authzv1.ListResponse { + itemSet := make(map[string]struct{}, len(scopes)) + + for scope := range scopes { + identifier := strings.TrimPrefix(scope, "folders:uid:") + if _, ok := itemSet[identifier]; ok { + continue + } + + itemSet[identifier] = struct{}{} + getChildren(tree, identifier, itemSet) } itemList := make([]string, 0, len(itemSet)) @@ -647,8 +648,35 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, itemList = append(itemList, item) } - span.SetAttributes(attribute.Int("num_folders", len(folderList)), attribute.Int("num_items", len(itemList))) - return &authzv1.ListResponse{Folders: folderList, Items: itemList}, nil + return &authzv1.ListResponse{Items: itemList} +} + +func buildItemList(scopes map[string]bool, tree map[string]FolderNode, prefix string) *authzv1.ListResponse { + folderSet := make(map[string]struct{}, len(scopes)) + itemSet := make(map[string]struct{}, len(scopes)) + + for scope := range scopes { + if identifier, ok := strings.CutPrefix(scope, "folders:uid:"); ok { + if _, ok := folderSet[identifier]; ok { + continue + } + folderSet[identifier] = struct{}{} + getChildren(tree, identifier, folderSet) + } else { + identifier := strings.TrimPrefix(scope, prefix) + itemSet[identifier] = struct{}{} + } + } + folderList := make([]string, 0, len(folderSet)) + for folder := range folderSet { + folderList = append(folderList, folder) + } + itemList := make([]string, 0, len(itemSet)) + for item := range itemSet { + itemList = append(itemList, item) + } + + return &authzv1.ListResponse{Folders: folderList, Items: itemList} } func getChildren(folderMap map[string]FolderNode, folderUID string, folderSet map[string]struct{}) { diff --git a/pkg/services/authz/rbac/service_test.go b/pkg/services/authz/rbac/service_test.go index 586f528f863..a8ad76d61fe 100644 --- a/pkg/services/authz/rbac/service_test.go +++ b/pkg/services/authz/rbac/service_test.go @@ -452,13 +452,13 @@ func TestService_buildFolderTree(t *testing.T) { func TestService_listPermission(t *testing.T) { type testCase struct { - name string - permissions []accesscontrol.Permission - folderTree map[string]FolderNode - list ListRequest - expectedDashboards []string - expectedFolders []string - expectedAll bool + name string + permissions []accesscontrol.Permission + folderTree map[string]FolderNode + list ListRequest + expectedItems []string + expectedFolders []string + expectedAll bool } testCases := []testCase{ @@ -512,8 +512,8 @@ func TestService_listPermission(t *testing.T) { Group: "dashboard.grafana.app", Resource: "dashboards", }, - expectedDashboards: []string{"some_dashboard"}, - expectedFolders: []string{"some_folder_1", "some_folder_2"}, + expectedItems: []string{"some_dashboard"}, + expectedFolders: []string{"some_folder_1", "some_folder_2"}, }, { name: "should return folders that user has inherited access to", @@ -568,8 +568,8 @@ func TestService_listPermission(t *testing.T) { Group: "dashboard.grafana.app", Resource: "dashboards", }, - expectedDashboards: []string{"some_dashboard"}, - expectedFolders: []string{"some_folder_parent", "some_folder_child"}, + expectedItems: []string{"some_dashboard"}, + expectedFolders: []string{"some_folder_parent", "some_folder_child"}, }, { name: "should deduplicate folders that user has inherited as well as direct access to", @@ -613,6 +613,28 @@ func TestService_listPermission(t *testing.T) { Resource: "dashboards", }, }, + { + name: "should collect folder permissions into items", + permissions: []accesscontrol.Permission{ + { + Action: "folders:read", + Scope: "folders:uid:some_folder_parent", + Kind: "folders", + Attribute: "uid", + Identifier: "some_folder_parent", + }, + }, + folderTree: map[string]FolderNode{ + "some_folder_parent": {UID: "some_folder_parent", ChildrenUIDs: []string{"some_folder_child"}}, + "some_folder_child": {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, + }, + list: ListRequest{ + Action: "folders:read", + Group: "folder.grafana.app", + Resource: "folders", + }, + expectedItems: []string{"some_folder_parent", "some_folder_child"}, + }, } for _, tc := range testCases { @@ -626,7 +648,7 @@ func TestService_listPermission(t *testing.T) { got, err := s.listPermission(context.Background(), getScopeMap(tc.permissions), &tc.list) require.NoError(t, err) assert.Equal(t, tc.expectedAll, got.All) - assert.ElementsMatch(t, tc.expectedDashboards, got.Items) + assert.ElementsMatch(t, tc.expectedItems, got.Items) assert.ElementsMatch(t, tc.expectedFolders, got.Folders) }) } From 2aa78139c43ee7dc960c3c88019ebb2e21da7c8a Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:15:22 +0000 Subject: [PATCH 291/894] Release: update changelog for 11.5.1 (#99956) * Update changelog * update changelog manually --------- Co-authored-by: github-actions[bot] Co-authored-by: joshhunt --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 927936a27d6..024f80e0ecf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + + +# 11.5.1 (2025-02-03) + +### Bug fixes + +- **CodeEditor:** Fix cursor alignment [#99090](https://github.com/grafana/grafana/pull/99090), [@ashharrison90](https://github.com/ashharrison90) +- **TransformationFilter**: Include transformation outputs in transformation filtering options: Include transformation outputs in transformation filtering options [#98323](https://github.com/grafana/grafana/pull/98323), [@Sergej-Vlasov](https://github.com/Sergej-Vlasov) + + # 11.5.0 (2025-01-28) From b636b81b166c3620aa426ddcea6ca0890e0e55f4 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 3 Feb 2025 14:24:35 +0300 Subject: [PATCH 292/894] K8s/IAM: Use raw handler for display (not rest.Connector) (#99898) --- pkg/apis/iam/v0alpha1/types_display.go | 7 +- pkg/apis/iam/v0alpha1/zz_generated.openapi.go | 8 +- pkg/registry/apis/iam/register.go | 16 +- pkg/registry/apis/iam/user/rest_display.go | 233 +++++++++------- pkg/registry/apis/iam/user/rest_user_team.go | 6 - pkg/services/apiserver/builder/helper.go | 6 - .../iam.grafana.app-v0alpha1.json | 256 ++++++++---------- 7 files changed, 270 insertions(+), 262 deletions(-) diff --git a/pkg/apis/iam/v0alpha1/types_display.go b/pkg/apis/iam/v0alpha1/types_display.go index 02c06662d09..9d3f27b4a41 100644 --- a/pkg/apis/iam/v0alpha1/types_display.go +++ b/pkg/apis/iam/v0alpha1/types_display.go @@ -33,16 +33,17 @@ type Display struct { // AvatarURL is the url where we can get the avatar for identity AvatarURL string `json:"avatarURL,omitempty"` - // InternalID is the legacy numreric id for identity, this is deprecated and should be phased out + // InternalID is the legacy numeric id for identity, + // Deprecated: use the identityRef where possible InternalID int64 `json:"internalId,omitempty"` } type IdentityRef struct { // Type of identity e.g. "user". - // For a full list see https://github.com/grafana/authlib/blob/2f8d13a83ca3e82da08b53726de1697ee5b5b4cc/claims/type.go#L15-L24 + // For a full list see https://github.com/grafana/authlib/blob/d6737a7dc8f55e9d42834adb83b5da607ceed293/types/type.go#L15 Type claims.IdentityType `json:"type"` - // Name is the unique identifier for identity, guaranteed jo be a unique value for the type within a namespace. + // Name is the unique identifier for identity, guaranteed to be a unique value for the type within a namespace. Name string `json:"name"` } diff --git a/pkg/apis/iam/v0alpha1/zz_generated.openapi.go b/pkg/apis/iam/v0alpha1/zz_generated.openapi.go index 51c53868df8..391aa5579d1 100644 --- a/pkg/apis/iam/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/iam/v0alpha1/zz_generated.openapi.go @@ -72,7 +72,7 @@ func schema_pkg_apis_iam_v0alpha1_Display(ref common.ReferenceCallback) common.O }, "internalId": { SchemaProps: spec.SchemaProps{ - Description: "InternalID is the legacy numreric id for identity, this is deprecated and should be phased out", + Description: "InternalID is the legacy numeric id for identity, Deprecated: use the identityRef where possible", Type: []string{"integer"}, Format: "int64", }, @@ -188,7 +188,7 @@ func schema_pkg_apis_iam_v0alpha1_IdentityRef(ref common.ReferenceCallback) comm Properties: map[string]spec.Schema{ "type": { SchemaProps: spec.SchemaProps{ - Description: "Type of identity e.g. \"user\". For a full list see https://github.com/grafana/authlib/blob/2f8d13a83ca3e82da08b53726de1697ee5b5b4cc/claims/type.go#L15-L24", + Description: "Type of identity e.g. \"user\". For a full list see https://github.com/grafana/authlib/blob/d6737a7dc8f55e9d42834adb83b5da607ceed293/types/type.go#L15", Default: "", Type: []string{"string"}, Format: "", @@ -196,7 +196,7 @@ func schema_pkg_apis_iam_v0alpha1_IdentityRef(ref common.ReferenceCallback) comm }, "name": { SchemaProps: spec.SchemaProps{ - Description: "Name is the unique identifier for identity, guaranteed jo be a unique value for the type within a namespace.", + Description: "Name is the unique identifier for identity, guaranteed to be a unique value for the type within a namespace.", Default: "", Type: []string{"string"}, Format: "", @@ -763,7 +763,7 @@ func schema_pkg_apis_iam_v0alpha1_TeamMember(ref common.ReferenceCallback) commo }, "internalId": { SchemaProps: spec.SchemaProps{ - Description: "InternalID is the legacy numreric id for identity, this is deprecated and should be phased out", + Description: "InternalID is the legacy numeric id for identity, Deprecated: use the identityRef where possible", Type: []string{"integer"}, Format: "int64", }, diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 42f69649a61..2789d859347 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -10,6 +10,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/validation/spec" "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -34,6 +35,9 @@ type IdentityAccessManagementAPIBuilder struct { authorizer authorizer.Authorizer accessClient types.AccessClient + // non-k8s api route + display *user.LegacyDisplayREST + // Not set for multi-tenant deployment for now sso ssosettings.Service } @@ -52,6 +56,7 @@ func RegisterAPIService( sso: ssoService, authorizer: authorizer, accessClient: client, + display: user.NewLegacyDisplayREST(store), } apiregistration.RegisterAPI(builder) @@ -60,7 +65,8 @@ func RegisterAPIService( func NewAPIService(store legacy.LegacyIdentityStore) *IdentityAccessManagementAPIBuilder { return &IdentityAccessManagementAPIBuilder{ - store: store, + store: store, + display: user.NewLegacyDisplayREST(store), authorizer: authorizer.AuthorizerFunc( func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { user, err := identity.GetRequester(ctx) @@ -114,9 +120,6 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.sso) } - // The display endpoint -- NOTE, this uses a rewrite hack to allow requests without a name parameter - storage["display"] = user.NewLegacyDisplayREST(b.store) - apiGroupInfo.VersionedResourcesStorageMap[iamv0.VERSION] = storage return nil } @@ -125,6 +128,11 @@ func (b *IdentityAccessManagementAPIBuilder) GetOpenAPIDefinitions() common.GetO return iamv0.GetOpenAPIDefinitions } +func (b *IdentityAccessManagementAPIBuilder) GetAPIRoutes() *builder.APIRoutes { + defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} }) + return b.display.GetAPIRoutes(defs) +} + func (b *IdentityAccessManagementAPIBuilder) GetAuthorizer() authorizer.Authorizer { return b.authorizer } diff --git a/pkg/registry/apis/iam/user/rest_display.go b/pkg/registry/apis/iam/user/rest_display.go index 112db40233c..c82567878c7 100644 --- a/pkg/registry/apis/iam/user/rest_display.go +++ b/pkg/registry/apis/iam/user/rest_display.go @@ -1,123 +1,154 @@ package user import ( - "context" + "encoding/json" "net/http" "strconv" "strings" - errorsK8s "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/rest" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" - claims "github.com/grafana/authlib/types" + authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/api/dtos" - iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" + iam "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util/errhttp" ) type LegacyDisplayREST struct { store legacy.LegacyIdentityStore } -var ( - _ rest.Storage = (*LegacyDisplayREST)(nil) - _ rest.SingularNameProvider = (*LegacyDisplayREST)(nil) - _ rest.Connecter = (*LegacyDisplayREST)(nil) - _ rest.Scoper = (*LegacyDisplayREST)(nil) - _ rest.StorageMetadata = (*LegacyDisplayREST)(nil) -) - func NewLegacyDisplayREST(store legacy.LegacyIdentityStore) *LegacyDisplayREST { return &LegacyDisplayREST{store} } -func (r *LegacyDisplayREST) New() runtime.Object { - return &iamv0.DisplayList{} -} - -func (r *LegacyDisplayREST) Destroy() {} - -func (r *LegacyDisplayREST) NamespaceScoped() bool { - return true -} - -func (r *LegacyDisplayREST) GetSingularName() string { - return "display" -} - -func (r *LegacyDisplayREST) ProducesMIMETypes(verb string) []string { - return []string{"application/json"} -} - -func (r *LegacyDisplayREST) ProducesObject(verb string) any { - return &iamv0.DisplayList{} -} - -func (r *LegacyDisplayREST) ConnectMethods() []string { - return []string{http.MethodGet} -} - -func (r *LegacyDisplayREST) NewConnectOptions() (runtime.Object, bool, string) { - return nil, false, "" // true means you can use the trailing path as a variable +func (r *LegacyDisplayREST) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *builder.APIRoutes { + listSchema := defs["github.com/grafana/grafana/pkg/apis/iam/v0alpha1.DisplayList"].Schema + displaySchema := defs["github.com/grafana/grafana/pkg/apis/iam/v0alpha1.Display"].Schema + identitySchema := defs["github.com/grafana/grafana/pkg/apis/iam/v0alpha1.IdentityRef"].Schema + listSchema.Properties["display"].Items.Schema = &displaySchema // not sure why this is lost + displaySchema.Properties["identity"] = identitySchema // not sure why this is lost + return &builder.APIRoutes{ + Namespace: []builder.APIRouteHandler{ + { + Path: "display", + Spec: &spec3.PathProps{ + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + OperationId: "getDisplayMapping", // This is used by RTK client generator + Tags: []string{"Display"}, + Description: "Show user display information", + Parameters: []*spec3.Parameter{ + { + ParameterProps: spec3.ParameterProps{ + Name: "namespace", + In: "path", + Required: true, + Example: "default", + Description: "workspace", + Schema: spec.StringProperty(), + }, + }, + { + ParameterProps: spec3.ParameterProps{ + Name: "key", + In: "query", + Description: "Display keys", + Required: true, + Example: "user:u000000001", + Schema: spec.ArrayProperty(spec.StringProperty()), + // Style: "form", + Explode: true, + }, + }, + }, + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + StatusCodeResponses: map[int]*spec3.Response{ + 200: { + ResponseProps: spec3.ResponseProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &listSchema, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Handler: r.handleDisplay, + }, + }, + } } // This will always have an empty app url var fakeCfgForGravatar = &setting.Cfg{} -func (r *LegacyDisplayREST) Connect(ctx context.Context, name string, _ runtime.Object, responder rest.Responder) (http.Handler, error) { - // See: /pkg/services/apiserver/builder/helper.go#L34 - // The name is set with a rewriter hack - if name != "name" { - return nil, errorsK8s.NewNotFound(schema.GroupResource{}, name) +func (r *LegacyDisplayREST) handleDisplay(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + user, ok := authlib.AuthInfoFrom(ctx) + if !ok { + errhttp.Write(ctx, apierrors.NewUnauthorized("missing auth info"), w) + return } - ns, err := request.NamespaceInfoFrom(ctx, true) + + ns, err := authlib.ParseNamespace(user.GetNamespace()) if err != nil { - return nil, err + errhttp.Write(ctx, err, w) + return + } + keys := parseKeys(req.URL.Query()["key"]) + users, err := r.store.ListDisplay(ctx, ns, legacy.ListDisplayQuery{ + OrgID: ns.OrgID, + UIDs: keys.uids, + IDs: keys.ids, + }) + if err != nil { + errhttp.Write(ctx, err, w) + return } - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - keys := parseKeys(req.URL.Query()["key"]) - users, err := r.store.ListDisplay(ctx, ns, legacy.ListDisplayQuery{ - OrgID: ns.OrgID, - UIDs: keys.uids, - IDs: keys.ids, - }) - if err != nil { - responder.Error(err) - return + rsp := &iam.DisplayList{ + Keys: keys.keys, + InvalidKeys: keys.invalid, + Items: make([]iam.Display, 0, len(users.Users)+len(keys.disp)+1), + } + for _, user := range users.Users { + disp := iam.Display{ + Identity: iam.IdentityRef{ + Type: authlib.TypeUser, + Name: user.UID, + }, + DisplayName: user.NameOrFallback(), + InternalID: user.ID, // nolint:staticcheck } + if user.IsServiceAccount { + disp.Identity.Type = authlib.TypeServiceAccount + } + disp.AvatarURL = dtos.GetGravatarUrlWithDefault(fakeCfgForGravatar, user.Email, disp.DisplayName) + rsp.Items = append(rsp.Items, disp) + } - rsp := &iamv0.DisplayList{ - Keys: keys.keys, - InvalidKeys: keys.invalid, - Items: make([]iamv0.Display, 0, len(users.Users)+len(keys.disp)+1), - } - for _, user := range users.Users { - disp := iamv0.Display{ - Identity: iamv0.IdentityRef{ - Type: claims.TypeUser, - Name: user.UID, - }, - DisplayName: user.NameOrFallback(), - InternalID: user.ID, - } - if user.IsServiceAccount { - disp.Identity.Type = claims.TypeServiceAccount - } - disp.AvatarURL = dtos.GetGravatarUrlWithDefault(fakeCfgForGravatar, user.Email, disp.DisplayName) - rsp.Items = append(rsp.Items, disp) - } + // Append the constants here + if len(keys.disp) > 0 { + rsp.Items = append(rsp.Items, keys.disp...) + } - // Append the constants here - if len(keys.disp) > 0 { - rsp.Items = append(rsp.Items, keys.disp...) - } - responder.Object(200, rsp) - }), nil + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(rsp) } type dispKeys struct { @@ -127,7 +158,7 @@ type dispKeys struct { invalid []string // For terminal keys, this is a constant - disp []iamv0.Display + disp []iam.Display } func parseKeys(req []string) dispKeys { @@ -139,7 +170,7 @@ func parseKeys(req []string) dispKeys { for _, key := range req { idx := strings.Index(key, ":") if idx > 0 { - t, err := claims.ParseType(key[0:idx]) + t, err := authlib.ParseType(key[0:idx]) if err != nil { keys.invalid = append(keys.invalid, key) continue @@ -147,18 +178,18 @@ func parseKeys(req []string) dispKeys { key = key[idx+1:] switch t { - case claims.TypeAnonymous: - keys.disp = append(keys.disp, iamv0.Display{ - Identity: iamv0.IdentityRef{ + case authlib.TypeAnonymous: + keys.disp = append(keys.disp, iam.Display{ + Identity: iam.IdentityRef{ Type: t, }, DisplayName: "Anonymous", AvatarURL: dtos.GetGravatarUrl(fakeCfgForGravatar, string(t)), }) continue - case claims.TypeAPIKey: - keys.disp = append(keys.disp, iamv0.Display{ - Identity: iamv0.IdentityRef{ + case authlib.TypeAPIKey: + keys.disp = append(keys.disp, iam.Display{ + Identity: iam.IdentityRef{ Type: t, Name: key, }, @@ -166,9 +197,9 @@ func parseKeys(req []string) dispKeys { AvatarURL: dtos.GetGravatarUrl(fakeCfgForGravatar, string(t)), }) continue - case claims.TypeProvisioning: - keys.disp = append(keys.disp, iamv0.Display{ - Identity: iamv0.IdentityRef{ + case authlib.TypeProvisioning: + keys.disp = append(keys.disp, iam.Display{ + Identity: iam.IdentityRef{ Type: t, }, DisplayName: "Provisioning", @@ -184,9 +215,9 @@ func parseKeys(req []string) dispKeys { id, err := strconv.ParseInt(key, 10, 64) if err == nil { if id == 0 { - keys.disp = append(keys.disp, iamv0.Display{ - Identity: iamv0.IdentityRef{ - Type: claims.TypeUser, + keys.disp = append(keys.disp, iam.Display{ + Identity: iam.IdentityRef{ + Type: authlib.TypeUser, Name: key, }, DisplayName: "System admin", diff --git a/pkg/registry/apis/iam/user/rest_user_team.go b/pkg/registry/apis/iam/user/rest_user_team.go index d9d0c22d2ca..79f781fa7e0 100644 --- a/pkg/registry/apis/iam/user/rest_user_team.go +++ b/pkg/registry/apis/iam/user/rest_user_team.go @@ -15,7 +15,6 @@ import ( var ( _ rest.Storage = (*LegacyUserTeamREST)(nil) - _ rest.Scoper = (*LegacyUserTeamREST)(nil) _ rest.StorageMetadata = (*LegacyUserTeamREST)(nil) _ rest.Connecter = (*LegacyUserTeamREST)(nil) ) @@ -36,11 +35,6 @@ func (s *LegacyUserTeamREST) New() runtime.Object { // Destroy implements rest.Storage. func (s *LegacyUserTeamREST) Destroy() {} -// NamespaceScoped implements rest.Scoper. -func (s *LegacyUserTeamREST) NamespaceScoped() bool { - return true -} - // ProducesMIMETypes implements rest.StorageMetadata. func (s *LegacyUserTeamREST) ProducesMIMETypes(verb string) []string { return []string{"application/json"} diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index fd95a2832b4..c1f81ae6a3b 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -52,12 +52,6 @@ var PathRewriters = []filters.PathRewriter{ return matches[1] + "/name" // connector requires a name }, }, - { - Pattern: regexp.MustCompile(`(/apis/iam.grafana.app/v0alpha1/namespaces/.*/display$)`), - ReplaceFunc: func(matches []string) string { - return matches[1] + "/name" // connector requires a name - }, - }, { Pattern: regexp.MustCompile(`(/apis/.*/v0alpha1/namespaces/.*/queryconvert$)`), ReplaceFunc: func(matches []string) string { diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index a7377164309..4ad7f837fdb 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -35,54 +35,133 @@ } } }, - "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/display/{name}": { + "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/display": { "get": { "tags": [ - "DisplayList" + "Display" + ], + "description": "Show user display information", + "operationId": "getDisplayMapping", + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "workspace", + "required": true, + "schema": { + "type": "string" + }, + "example": "default" + }, + { + "name": "key", + "in": "query", + "description": "Display keys", + "required": true, + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "example": "user:u000000001" + } ], - "description": "connect GET requests to DisplayList", - "operationId": "getDisplayList", "responses": { "200": { - "description": "OK", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.DisplayList" + "type": "object", + "required": [ + "keys", + "display" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "display": { + "description": "Matching items (the caller may need to remap from keys to results)", + "type": "array", + "items": { + "type": "object", + "required": [ + "identity", + "displayName" + ], + "properties": { + "avatarURL": { + "description": "AvatarURL is the url where we can get the avatar for identity", + "type": "string" + }, + "displayName": { + "description": "Display name for identity.", + "type": "string", + "default": "" + }, + "identity": { + "type": "object", + "required": [ + "type", + "name" + ], + "properties": { + "name": { + "description": "Name is the unique identifier for identity, guaranteed to be a unique value for the type within a namespace.", + "type": "string", + "default": "" + }, + "type": { + "description": "Type of identity e.g. \"user\". For a full list see https://github.com/grafana/authlib/blob/d6737a7dc8f55e9d42834adb83b5da607ceed293/types/type.go#L15", + "type": "string", + "default": "" + } + } + }, + "internalId": { + "description": "InternalID is the legacy numeric id for identity, Deprecated: use the identityRef where possible", + "type": "integer", + "format": "int64" + } + } + }, + "x-kubernetes-list-type": "atomic" + }, + "invalidKeys": { + "description": "Input keys that were not useable", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set" + }, + "keys": { + "description": "Request keys used to lookup the display value", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } } } } } - }, - "x-kubernetes-action": "connect", - "x-kubernetes-group-version-kind": { - "group": "iam.grafana.app", - "version": "v0alpha1", - "kind": "DisplayList" } - }, - "parameters": [ - { - "name": "name", - "in": "path", - "description": "name of the DisplayList", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - } - ] + } }, "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/serviceaccounts": { "get": { @@ -2432,105 +2511,6 @@ "additionalProperties": true, "x-kubernetes-preserve-unknown-fields": true }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.Display": { - "type": "object", - "required": [ - "identity", - "displayName" - ], - "properties": { - "avatarURL": { - "description": "AvatarURL is the url where we can get the avatar for identity", - "type": "string" - }, - "displayName": { - "description": "Display name for identity.", - "type": "string", - "default": "" - }, - "identity": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.IdentityRef" - } - ] - }, - "internalId": { - "description": "InternalID is the legacy numreric id for identity, this is deprecated and should be phased out", - "type": "integer", - "format": "int64" - } - } - }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.DisplayList": { - "type": "object", - "required": [ - "keys", - "display" - ], - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "display": { - "description": "Matching items (the caller may need to remap from keys to results)", - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.Display" - } - ] - }, - "x-kubernetes-list-type": "atomic" - }, - "invalidKeys": { - "description": "Input keys that were not useable", - "type": "array", - "items": { - "type": "string", - "default": "" - }, - "x-kubernetes-list-type": "set" - }, - "keys": { - "description": "Request keys used to lookup the display value", - "type": "array", - "items": { - "type": "string", - "default": "" - }, - "x-kubernetes-list-type": "set" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "iam.grafana.app", - "kind": "DisplayList", - "version": "__internal" - }, - { - "group": "iam.grafana.app", - "kind": "DisplayList", - "version": "v0alpha1" - } - ] - }, "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.IdentityRef": { "type": "object", "required": [ @@ -2539,12 +2519,12 @@ ], "properties": { "name": { - "description": "Name is the unique identifier for identity, guaranteed jo be a unique value for the type within a namespace.", + "description": "Name is the unique identifier for identity, guaranteed to be a unique value for the type within a namespace.", "type": "string", "default": "" }, "type": { - "description": "Type of identity e.g. \"user\". For a full list see https://github.com/grafana/authlib/blob/2f8d13a83ca3e82da08b53726de1697ee5b5b4cc/claims/type.go#L15-L24", + "description": "Type of identity e.g. \"user\". For a full list see https://github.com/grafana/authlib/blob/d6737a7dc8f55e9d42834adb83b5da607ceed293/types/type.go#L15", "type": "string", "default": "" } @@ -2977,7 +2957,7 @@ ] }, "internalId": { - "description": "InternalID is the legacy numreric id for identity, this is deprecated and should be phased out", + "description": "InternalID is the legacy numeric id for identity, Deprecated: use the identityRef where possible", "type": "integer", "format": "int64" }, From a0901456ae5654ef62c6dcc57b0c3502c4d8339b Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Mon, 3 Feb 2025 13:50:15 +0200 Subject: [PATCH 293/894] RTK Clients: Fix namespace filter (#99949) * Fix namespace filter * Update comment * Update filter --- scripts/process-specs.ts | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/scripts/process-specs.ts b/scripts/process-specs.ts index b101f80b74e..a53f6e6e6e1 100644 --- a/scripts/process-specs.ts +++ b/scripts/process-specs.ts @@ -18,8 +18,8 @@ function processOpenAPISpec(spec: OpenAPIV3.Document) { // Process 'paths' property const newPaths: Record = {}; for (const [path, pathItem] of Object.entries(newSpec.paths)) { - // Remove 'watch' paths as they're deprecated / remove empty path items - if (path.includes('/watch/') || !pathItem) { + // Remove empty path items + if (!pathItem) { continue; } // Remove the specified part from the path key @@ -27,12 +27,13 @@ function processOpenAPISpec(spec: OpenAPIV3.Document) { // Process each method in the path (e.g., get, post) const newPathItem: Record = {}; - for (const method of Object.keys(pathItem)) { - // Filter out the 'namespace' param - if (method === 'parameters' && Array.isArray(pathItem.parameters)) { - pathItem.parameters = pathItem.parameters?.filter((param) => 'name' in param && param.name !== 'namespace'); - } + // Filter out namespace parameter at path level + if (Array.isArray(pathItem.parameters)) { + pathItem.parameters = filterNamespaceParameters(pathItem.parameters); + } + + for (const method of Object.keys(pathItem)) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const operation = pathItem[method as keyof OpenAPIV3.PathItemObject]; @@ -45,6 +46,16 @@ function processOpenAPISpec(spec: OpenAPIV3.Document) { continue; } + // Filter out namespace parameter at operation level + if ( + operation && + typeof operation === 'object' && + 'parameters' in operation && + Array.isArray(operation.parameters) + ) { + operation.parameters = filterNamespaceParameters(operation.parameters); + } + updateRefs(operation); newPathItem[method] = operation; @@ -69,6 +80,13 @@ function processOpenAPISpec(spec: OpenAPIV3.Document) { return newSpec; } +/** + * Filter out namespace parameters from an array of parameters + */ +function filterNamespaceParameters(parameters: Array) { + return parameters.filter((param) => 'name' in param && param.name !== 'namespace'); +} + /** * Recursively update all $ref fields to remove k8s metadata from names */ From c85a1752120169610ee6b04adbb6aaf6b531e1d8 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Mon, 3 Feb 2025 13:56:25 +0100 Subject: [PATCH 294/894] RBAC: only query folder service when fetching parent folders (#99893) * only query folder service when fetching parent folders * Perform validation and inehrited scopes solvers as service instead of caller --- pkg/api/folder_bench_test.go | 2 +- .../ossaccesscontrol/dashboard.go | 21 +++++++------------ .../accesscontrol/ossaccesscontrol/folder.go | 18 +++++++++------- .../ossaccesscontrol/testutil/testutil.go | 2 -- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index c1114a3a48d..c94de153484 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -468,7 +468,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db, permreg.ProvidePermissionRegistry(), nil, folderServiceWithFlagOn, ) folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( - cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets) + cfg, features, routing.NewRouteRegister(), sc.db, ac, license, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets) require.NoError(b, err) dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl( sc.cfg, dashStore, folderStore, diff --git a/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go b/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go index 79b5c6ffcbc..14fa5eca892 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -117,6 +118,7 @@ func ProvideDashboardPermissions( ctx, span := tracer.Start(ctx, "accesscontrol.ossaccesscontrol.ProvideDashboardPermissions.ResourceValidator") defer span.End() + ctx, _ = identity.WithServiceIdentitiy(ctx, orgID) dashboard, err := getDashboard(ctx, orgID, resourceID) if err != nil { return err @@ -129,32 +131,25 @@ func ProvideDashboardPermissions( return nil }, InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) { - wildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix) - scopes := []string(wildcards) - + ctx, _ = identity.WithServiceIdentitiy(ctx, orgID) dashboard, err := getDashboard(ctx, orgID, resourceID) if err != nil { return nil, err } + + scopes := []string(accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix)) metrics.MFolderIDsServiceCount.WithLabelValues(metrics.AccessControl).Inc() - // nolint:staticcheck if dashboard.FolderUID != "" { - query := &dashboards.GetDashboardQuery{UID: dashboard.FolderUID, OrgID: orgID} - queryResult, err := dashboardService.GetDashboard(ctx, query) - if err != nil { - return nil, err - } - parentScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(queryResult.UID) - - nestedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, queryResult.UID, folderService) + nestedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderService) if err != nil { return nil, err } - scopes = append(scopes, parentScope) + scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.FolderUID)) scopes = append(scopes, nestedScopes...) return scopes, nil } + return append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)), nil }, Assignments: resourcepermissions.Assignments{ diff --git a/pkg/services/accesscontrol/ossaccesscontrol/folder.go b/pkg/services/accesscontrol/ossaccesscontrol/folder.go index 8904567d935..0ca7921723c 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/folder.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/folder.go @@ -2,9 +2,9 @@ package ossaccesscontrol import ( "context" - "errors" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" @@ -84,7 +84,7 @@ func registerFolderRoles(cfg *setting.Cfg, features featuremgmt.FeatureToggles, func ProvideFolderPermissions( cfg *setting.Cfg, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, accesscontrol accesscontrol.AccessControl, - license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, + license licensing.Licensing, folderService folder.Service, service accesscontrol.Service, teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService, ) (*FolderPermissionsService, error) { if err := registerFolderRoles(cfg, features, service); err != nil { @@ -98,19 +98,21 @@ func ProvideFolderPermissions( ctx, span := tracer.Start(ctx, "accesscontrol.ossaccesscontrol.ProvideFolderPermissions.ResourceValidator") defer span.End() - query := &dashboards.GetDashboardQuery{UID: resourceID, OrgID: orgID} - queryResult, err := dashboardStore.GetDashboard(ctx, query) + ctx, ident := identity.WithServiceIdentitiy(ctx, orgID) + _, err := folderService.Get(ctx, &folder.GetFolderQuery{ + UID: &resourceID, + OrgID: orgID, + SignedInUser: ident, + }) + if err != nil { return err } - if !queryResult.IsFolder { - return errors.New("not found") - } - return nil }, InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) { + ctx, _ = identity.WithServiceIdentitiy(ctx, orgID) return dashboards.GetInheritedScopes(ctx, orgID, resourceID, folderService) }, Assignments: resourcepermissions.Assignments{ diff --git a/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go b/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go index a5f469ec293..9379bfb7abe 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/authz/zanzana" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder/folderimpl" @@ -88,7 +87,6 @@ func ProvideFolderPermissions( sqlStore, ac, license, - &dashboards.FakeDashboardStore{}, fService, acSvc, teamSvc, From 30bf2bcde1a6a6ace7a5a7d1b7f54644f10672b6 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 3 Feb 2025 13:05:47 +0000 Subject: [PATCH 295/894] API client generation: create a central `getAPIBaseURL` function (#99961) * create a getAPIBaseURL function * use base url in iam api --- public/app/api/utils.ts | 10 ++++++ public/app/core/reducers/root.ts | 2 +- .../QueryLibrary/QueryLibraryDrawer.tsx | 2 +- public/app/features/iam/api/api.ts | 6 ++-- .../query-library/api/{factory.ts => api.ts} | 5 +-- .../query-library/api/endpoints.gen.ts | 36 +++++++++---------- .../app/features/query-library/api/mappers.ts | 3 -- .../app/features/query-library/api/mocks.ts | 2 +- .../app/features/query-library/api/query.ts | 20 ----------- public/app/features/query-library/index.ts | 2 +- public/app/store/configureStore.ts | 2 +- 11 files changed, 38 insertions(+), 52 deletions(-) rename public/app/features/query-library/api/{factory.ts => api.ts} (77%) delete mode 100644 public/app/features/query-library/api/query.ts diff --git a/public/app/api/utils.ts b/public/app/api/utils.ts index 51ba395ad43..6770b117b12 100644 --- a/public/app/api/utils.ts +++ b/public/app/api/utils.ts @@ -1,3 +1,13 @@ import { config } from '@grafana/runtime'; export const getAPINamespace = () => config.namespace; + +/** + * Get a base URL for a k8s API endpoint with parameterised namespace given it's group and version + * @param group the k8s group, e.g. dashboard.grafana.app + * @param version e.g. v0alpha1 + * @returns + */ +export const getAPIBaseURL = (group: string, version: string) => { + return `/apis/${group}/${version}/namespaces/${getAPINamespace()}`; +}; diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index 86027133c82..320e62bc9c4 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -30,7 +30,7 @@ import templatingReducers from 'app/features/variables/state/keyedVariablesReduc import { alertingApi } from '../../features/alerting/unified/api/alertingApi'; import { iamApi } from '../../features/iam/api/api'; import { userPreferencesAPI } from '../../features/preferences/api'; -import { queryLibraryApi } from '../../features/query-library/api/factory'; +import { queryLibraryApi } from '../../features/query-library/api/api'; import { cleanUpAction } from '../actions/cleanUp'; const rootReducers = { diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx index 18e2caead3d..8b96626dd96 100644 --- a/public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx +++ b/public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx @@ -5,7 +5,7 @@ import { TabbedContainer, TabConfig } from '@grafana/ui'; import { t } from '../../../core/internationalization'; import { useListQueryTemplateQuery } from '../../query-library'; -import { QUERY_LIBRARY_GET_LIMIT } from '../../query-library/api/factory'; +import { QUERY_LIBRARY_GET_LIMIT } from '../../query-library/api/api'; import { ExploreDrawer } from '../ExploreDrawer'; import { QueryLibrary } from './QueryLibrary'; diff --git a/public/app/features/iam/api/api.ts b/public/app/features/iam/api/api.ts index 1bc93cb693e..07a735c561a 100644 --- a/public/app/features/iam/api/api.ts +++ b/public/app/features/iam/api/api.ts @@ -1,11 +1,9 @@ import { createApi } from '@reduxjs/toolkit/query/react'; import { createBaseQuery } from '../../../api/createBaseQuery'; -import { getAPINamespace } from '../../../api/utils'; +import { getAPIBaseURL } from '../../../api/utils'; -export const API_VERSION = 'iam.grafana.app/v0alpha1'; - -export const BASE_URL = `/apis/${API_VERSION}/namespaces/${getAPINamespace()}`; +export const BASE_URL = getAPIBaseURL('iam.grafana.app', 'v0alpha1'); export const iamApi = createApi({ baseQuery: createBaseQuery({ baseURL: BASE_URL }), diff --git a/public/app/features/query-library/api/factory.ts b/public/app/features/query-library/api/api.ts similarity index 77% rename from public/app/features/query-library/api/factory.ts rename to public/app/features/query-library/api/api.ts index 95ce02d6fa3..ec318cb7e84 100644 --- a/public/app/features/query-library/api/factory.ts +++ b/public/app/features/query-library/api/api.ts @@ -1,13 +1,14 @@ import { createApi } from '@reduxjs/toolkit/query/react'; import { createBaseQuery } from '../../../api/createBaseQuery'; - -import { BASE_URL } from './query'; +import { getAPIBaseURL } from '../../../api/utils'; // Currently, we are loading all query templates // Organizations can have maximum of 1000 query templates export const QUERY_LIBRARY_GET_LIMIT = 1000; +export const BASE_URL = getAPIBaseURL('peakq.grafana.app', 'v0alpha1'); + export const queryLibraryApi = createApi({ baseQuery: createBaseQuery({ baseURL: BASE_URL }), reducerPath: 'queryLibraryAPI', diff --git a/public/app/features/query-library/api/endpoints.gen.ts b/public/app/features/query-library/api/endpoints.gen.ts index 133cad6a83e..db8bd22e85e 100644 --- a/public/app/features/query-library/api/endpoints.gen.ts +++ b/public/app/features/query-library/api/endpoints.gen.ts @@ -1,4 +1,4 @@ -import { queryLibraryApi as api } from './factory'; +import { queryLibraryApi as api } from './api'; export const addTagTypes = ['QueryTemplate'] as const; const injectedRtkApi = api .enhanceEndpoints({ @@ -81,7 +81,7 @@ export type ListQueryTemplateApiArg = { /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ allowWatchBookmarks?: boolean; /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". - + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ continue?: string; /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ @@ -89,19 +89,19 @@ export type ListQueryTemplateApiArg = { /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ labelSelector?: string; /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. - + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ limit?: number; /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - + Defaults to unset */ resourceVersion?: string; /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - + Defaults to unset */ resourceVersionMatch?: string; /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. - + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan is interpreted as "data at least as new as the provided `resourceVersion`" and the bookmark event is send when the state is synced @@ -111,7 +111,7 @@ export type ListQueryTemplateApiArg = { when request started being processed. - `resourceVersionMatch` set to any other value or unset Invalid error is returned. - + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ sendInitialEvents?: boolean; /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ @@ -208,21 +208,21 @@ export type ObjectMeta = { [key: string]: string; }; /** CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. - + Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ creationTimestamp?: Time; /** Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only. */ deletionGracePeriodSeconds?: number; /** DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested. - + Populated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ deletionTimestamp?: Time; /** Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list. */ finalizers?: string[]; /** GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server. - + If this field is specified and the generated name exists, the server will return a 409. - + Applied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency */ generateName?: string; /** A sequence number representing a specific generation of the desired state. Populated by the system. Read-only. */ @@ -236,19 +236,19 @@ export type ObjectMeta = { /** Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ name?: string; /** Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty. - + Must be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces */ namespace?: string; /** List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller. */ ownerReferences?: OwnerReference[]; /** An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources. - + Populated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */ resourceVersion?: string; /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ selfLink?: string; /** UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations. - + Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ uid?: string; }; @@ -284,8 +284,8 @@ export type DataQuery = { /** Maximum frame count */ maxFrames?: number; /** Type asserts that the frame matches a known type structure. - - + + Possible enum values: - `""` - `"timeseries-wide"` @@ -333,7 +333,7 @@ export type TemplatePosition = { }; export type TemplateVariableReplacement = { /** How values should be interpolated - + Possible enum values: - `"csv"` Formats variables with multiple values as a comma-separated string. - `"doublequote"` Formats single- and multi-valued variables into a comma-separated string @@ -406,7 +406,7 @@ export type QueryTemplateList = { }; export type StatusCause = { /** The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional. - + Examples: "name" - the field "name" on the current resource "items[0].name" - the field "name" on the first array entry in "items" */ diff --git a/public/app/features/query-library/api/mappers.ts b/public/app/features/query-library/api/mappers.ts index c0568c75d1a..26cd684c207 100644 --- a/public/app/features/query-library/api/mappers.ts +++ b/public/app/features/query-library/api/mappers.ts @@ -4,7 +4,6 @@ import { AnnoKeyCreatedBy } from '../../apiserver/types'; import { AddQueryTemplateCommand, QueryTemplate } from '../types'; import { ListQueryTemplateApiResponse, QueryTemplate as QT } from './endpoints.gen'; -import { API_VERSION, QueryTemplateKinds } from './query'; export const convertDataQueryResponseToQueryTemplates = (result: ListQueryTemplateApiResponse): QueryTemplate[] => { if (!result.items) { @@ -30,8 +29,6 @@ export const convertDataQueryResponseToQueryTemplates = (result: ListQueryTempla export const convertAddQueryTemplateCommandToDataQuerySpec = (addQueryTemplateCommand: AddQueryTemplateCommand): QT => { const { title, targets } = addQueryTemplateCommand; return { - apiVersion: API_VERSION, - kind: QueryTemplateKinds.QueryTemplate, metadata: { /** * Server will append to whatever is passed here, but just to be safe we generate a uuid diff --git a/public/app/features/query-library/api/mocks.ts b/public/app/features/query-library/api/mocks.ts index 0fb6d5fe8d5..ed88eeba17c 100644 --- a/public/app/features/query-library/api/mocks.ts +++ b/public/app/features/query-library/api/mocks.ts @@ -1,4 +1,4 @@ -import { BASE_URL } from './query'; +import { BASE_URL } from './api'; import { getIdentityDisplayList } from './testdata/identityDisplayList'; import { getTestQueryList } from './testdata/testQueryList'; diff --git a/public/app/features/query-library/api/query.ts b/public/app/features/query-library/api/query.ts deleted file mode 100644 index d0cc6db0209..00000000000 --- a/public/app/features/query-library/api/query.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getAPINamespace } from '../../../api/utils'; - -/** - * @alpha - */ -export const API_VERSION = 'peakq.grafana.app/v0alpha1'; - -/** - * @alpha - */ -export enum QueryTemplateKinds { - QueryTemplate = 'QueryTemplate', -} - -/** - * Query Library is an experimental feature. API (including the URL path) will likely change. - * - * @alpha - */ -export const BASE_URL = `/apis/${API_VERSION}/namespaces/${getAPINamespace()}`; diff --git a/public/app/features/query-library/index.ts b/public/app/features/query-library/index.ts index f81a701e632..246ca735681 100644 --- a/public/app/features/query-library/index.ts +++ b/public/app/features/query-library/index.ts @@ -9,8 +9,8 @@ import { config } from '@grafana/runtime'; +import { QUERY_LIBRARY_GET_LIMIT } from './api/api'; import { generatedQueryLibraryApi } from './api/endpoints.gen'; -import { QUERY_LIBRARY_GET_LIMIT } from './api/factory'; import { mockData } from './api/mocks'; export const { diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 45bb3351e98..2102c936b5f 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -12,7 +12,7 @@ import { buildInitialState } from '../core/reducers/navModel'; import { addReducer, createRootReducer } from '../core/reducers/root'; import { alertingApi } from '../features/alerting/unified/api/alertingApi'; import { iamApi } from '../features/iam/api/api'; -import { queryLibraryApi } from '../features/query-library/api/factory'; +import { queryLibraryApi } from '../features/query-library/api/api'; import { setStore } from './store'; From 39605a93abb1e484eaa7e5e1b95391eaccfb3a8b Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Mon, 3 Feb 2025 14:15:29 +0100 Subject: [PATCH 296/894] E2E: Adding API tests for panel edit settings (#99038) * Updating plugin-e2e! * Added API tests for new panel edit APIs in plugin-e2e * Added API tests. * rebased main. * removed only. --- .../as-admin-user/panelEditPage.spec.ts | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts index b700c247006..2de0a9e57fd 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts @@ -6,6 +6,7 @@ import { scenarios } from '../mocks/resources'; const PANEL_TITLE = 'Table panel E2E test'; const TABLE_VIZ_NAME = 'Table'; +const TIME_SERIES_VIZ_NAME = 'Time series'; const STANDARD_OTIONS_CATEGORY = 'Standard options'; const DISPLAY_NAME_LABEL = 'Display name'; const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; @@ -84,6 +85,118 @@ test.describe('edit panel plugin settings', () => { formatExpectError('Expected section to be collapsed') ).toBeVisible(); }); + + test('Select time zone in timezone picker', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = await panelEditPage.getCustomOptions('Axis'); + const timeZonePicker = axisOptions.getSelect('Time zone'); + + await timeZonePicker.selectOption('Europe/Stockholm'); + await expect(timeZonePicker).toHaveSelected('Europe/Stockholm'); + }); + + test('select unit in unit picker', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const standardOptions = panelEditPage.getStandardOptions(); + const unitPicker = standardOptions.getUnitPicker('Unit'); + + await unitPicker.selectOption('Misc > Pixels'); + + await expect(unitPicker).toHaveSelected('Pixels'); + }); + + test('enter value in number input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const lineWith = axisOptions.getNumberInput('Soft min'); + + await lineWith.fill('10'); + + await expect(lineWith).toHaveValue('10'); + }); + + test('enter value in slider', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const graphOptions = panelEditPage.getCustomOptions('Graph styles'); + const lineWidth = graphOptions.getSliderInput('Line width'); + + await lineWidth.fill('10'); + + await expect(lineWidth).toHaveValue('10'); + }); + + test('select value in single value select', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const standardOptions = panelEditPage.getStandardOptions(); + const colorSchemeSelect = standardOptions.getSelect('Color scheme'); + + await colorSchemeSelect.selectOption('Classic palette'); + await expect(colorSchemeSelect).toHaveSelected('Classic palette'); + }); + + test('clear input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const panelOptions = panelEditPage.getPanelOptions(); + const title = panelOptions.getTextInput('Title'); + + await expect(title).toHaveValue('Panel Title'); + await title.clear(); + await expect(title).toHaveValue(''); + }); + + test('enter value in input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const panelOptions = panelEditPage.getPanelOptions(); + const description = panelOptions.getTextInput('Description'); + + await expect(description).toHaveValue(''); + await description.fill('This is a panel'); + await expect(description).toHaveValue('This is a panel'); + }); + + test('unchecking switch', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const showBorder = axisOptions.getSwitch('Show border'); + + await expect(showBorder).toBeChecked({ checked: false }); + await showBorder.check(); + await expect(showBorder).toBeChecked(); + + await showBorder.uncheck(); + await expect(showBorder).toBeChecked({ checked: false }); + }); + + test('checking switch', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const showBorder = axisOptions.getSwitch('Show border'); + + await expect(showBorder).toBeChecked({ checked: false }); + await showBorder.check(); + await expect(showBorder).toBeChecked(); + }); + + test('re-selecting value in radio button group', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const placement = axisOptions.getRadioGroup('Placement'); + + await placement.check('Right'); + await expect(placement).toHaveChecked('Right'); + + await placement.check('Auto'); + await expect(placement).toHaveChecked('Auto'); + }); + + test('selecting value in radio button group', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const placement = axisOptions.getRadioGroup('Placement'); + + await placement.check('Right'); + await expect(placement).toHaveChecked('Right'); + }); }); test('backToDashboard method should navigate to dashboard page', async ({ gotoPanelEditPage, page }) => { From 34b2cb5e02f66ea0a0d32903db483255fb93e730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Mon, 3 Feb 2025 14:24:49 +0100 Subject: [PATCH 297/894] Features: Remove openSearchBackendFlowEnabled feature toggle (#99068) --- .../configure-grafana/feature-toggles/index.md | 1 - packages/grafana-data/src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 7 ------- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/featuremgmt/toggles_gen.json | 1 + 6 files changed, 1 insertion(+), 14 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 8dae19485ea..a9395c559ae 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -72,7 +72,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `newDashboardSharingComponent` | Enables the new sharing drawer design | Yes | | `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | | | `pinNavItems` | Enables pinning of nav items | Yes | -| `openSearchBackendFlowEnabled` | Enables the backend query flow for Open Search datasource plugin | Yes | | `alertingApiServer` | Register Alerting APIs with the K8s API server | Yes | | `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes | | `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 2792ff8a74a..a5708006967 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -189,7 +189,6 @@ export interface FeatureToggles { azureMonitorPrometheusExemplars?: boolean; pinNavItems?: boolean; authZGRPCServer?: boolean; - openSearchBackendFlowEnabled?: boolean; ssoSettingsLDAP?: boolean; failWrongDSUID?: boolean; zanzana?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f1cd1fe8cf8..a99c4ab251e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1297,13 +1297,6 @@ var ( HideFromAdminPage: true, HideFromDocs: true, }, - { - Name: "openSearchBackendFlowEnabled", - Description: "Enables the backend query flow for Open Search datasource plugin", - Stage: FeatureStageGeneralAvailability, - Owner: awsDatasourcesSquad, - Expression: "true", - }, { Name: "ssoSettingsLDAP", Description: "Use the new SSO Settings API to configure LDAP", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 4d9fae9c95c..25f5855541b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -170,7 +170,6 @@ sqlQuerybuilderFunctionParameters,experimental,@grafana/oss-big-tent,false,false azureMonitorPrometheusExemplars,preview,@grafana/partner-datasources,false,false,false pinNavItems,GA,@grafana/grafana-frontend-platform,false,false,false authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false -openSearchBackendFlowEnabled,GA,@grafana/aws-datasources,false,false,false ssoSettingsLDAP,preview,@grafana/identity-access-team,false,true,false failWrongDSUID,experimental,@grafana/plugins-platform-backend,false,false,false zanzana,experimental,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index e149127edf7..e75d59f0257 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -691,10 +691,6 @@ const ( // Enables the gRPC server for authorization FlagAuthZGRPCServer = "authZGRPCServer" - // FlagOpenSearchBackendFlowEnabled - // Enables the backend query flow for Open Search datasource plugin - FlagOpenSearchBackendFlowEnabled = "openSearchBackendFlowEnabled" - // FlagSsoSettingsLDAP // Use the new SSO Settings API to configure LDAP FlagSsoSettingsLDAP = "ssoSettingsLDAP" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 580121c5120..934a3078871 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2794,6 +2794,7 @@ "name": "openSearchBackendFlowEnabled", "resourceVersion": "1724141158995", "creationTimestamp": "2024-06-17T09:41:50Z", + "deletionTimestamp": "2025-01-16T11:09:59Z", "annotations": { "grafana.app/updatedTimestamp": "2024-08-20 08:05:58.995762 +0000 UTC" } From e0151528a4a255413eeb56950e6649608e5f95cf Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 3 Feb 2025 13:40:36 +0000 Subject: [PATCH 298/894] API client generation: Update iam client (#99963) * update generated iam client * update API * with meta api * regenerate client * with identify ref --------- Co-authored-by: Ryan McKinley --- pkg/registry/apis/iam/register.go | 54 +++++ pkg/registry/apis/iam/user/rest_display.go | 11 +- .../iam.grafana.app-v0alpha1.json | 208 +++++++++++------- .../QueryLibrary/utils/dataFetching.ts | 7 +- public/app/features/iam/api/endpoints.gen.ts | 27 ++- .../iam/api/scripts/generate-rtk-apis.ts | 2 +- public/app/features/iam/index.ts | 2 +- 7 files changed, 205 insertions(+), 106 deletions(-) diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 2789d859347..9112101033e 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -2,6 +2,7 @@ package iam import ( "context" + "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -10,6 +11,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" "github.com/grafana/authlib/types" @@ -128,6 +130,58 @@ func (b *IdentityAccessManagementAPIBuilder) GetOpenAPIDefinitions() common.GetO return iamv0.GetOpenAPIDefinitions } +func (b *IdentityAccessManagementAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { + oas.Info.Description = "Identity and Access Management" + + defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} }) + defsBase := "github.com/grafana/grafana/pkg/apis/iam/v0alpha1." + + // Add missing schemas + for k, v := range defs { + clean := strings.Replace(k, defsBase, "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.", 1) + if oas.Components.Schemas[clean] == nil { + oas.Components.Schemas[clean] = &v.Schema + } + } + compBase := "com.github.grafana.grafana.pkg.apis.iam.v0alpha1." + schema := oas.Components.Schemas[compBase+"DisplayList"].Properties["display"] + schema.Items = &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + AllOf: []spec.Schema{ + { + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef("#/components/schemas/" + compBase + "Display"), + }, + }, + }, + }, + }, + } + oas.Components.Schemas[compBase+"DisplayList"].Properties["display"] = schema + oas.Components.Schemas[compBase+"DisplayList"].Properties["metadata"] = spec.Schema{ + SchemaProps: spec.SchemaProps{ + AllOf: []spec.Schema{ + { + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef("#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"), + }, + }, + }}, + } + oas.Components.Schemas[compBase+"Display"].Properties["identity"] = spec.Schema{ + SchemaProps: spec.SchemaProps{ + AllOf: []spec.Schema{ + { + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef("#/components/schemas/" + compBase + "IdentityRef"), + }, + }, + }}, + } + return oas, nil +} + func (b *IdentityAccessManagementAPIBuilder) GetAPIRoutes() *builder.APIRoutes { defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} }) return b.display.GetAPIRoutes(defs) diff --git a/pkg/registry/apis/iam/user/rest_display.go b/pkg/registry/apis/iam/user/rest_display.go index c82567878c7..b1b4232005e 100644 --- a/pkg/registry/apis/iam/user/rest_display.go +++ b/pkg/registry/apis/iam/user/rest_display.go @@ -29,11 +29,6 @@ func NewLegacyDisplayREST(store legacy.LegacyIdentityStore) *LegacyDisplayREST { } func (r *LegacyDisplayREST) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *builder.APIRoutes { - listSchema := defs["github.com/grafana/grafana/pkg/apis/iam/v0alpha1.DisplayList"].Schema - displaySchema := defs["github.com/grafana/grafana/pkg/apis/iam/v0alpha1.Display"].Schema - identitySchema := defs["github.com/grafana/grafana/pkg/apis/iam/v0alpha1.IdentityRef"].Schema - listSchema.Properties["display"].Items.Schema = &displaySchema // not sure why this is lost - displaySchema.Properties["identity"] = identitySchema // not sure why this is lost return &builder.APIRoutes{ Namespace: []builder.APIRouteHandler{ { @@ -76,7 +71,11 @@ func (r *LegacyDisplayREST) GetAPIRoutes(defs map[string]common.OpenAPIDefinitio Content: map[string]*spec3.MediaType{ "application/json": { MediaTypeProps: spec3.MediaTypeProps{ - Schema: &listSchema, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef("#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.DisplayList"), + }, + }, }, }, }, diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index 4ad7f837fdb..f6bb170a1c2 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -1,6 +1,7 @@ { "openapi": "3.0.0", "info": { + "description": "Identity and Access Management", "title": "iam.grafana.app/v0alpha1" }, "paths": { @@ -73,89 +74,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "keys", - "display" - ], - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "display": { - "description": "Matching items (the caller may need to remap from keys to results)", - "type": "array", - "items": { - "type": "object", - "required": [ - "identity", - "displayName" - ], - "properties": { - "avatarURL": { - "description": "AvatarURL is the url where we can get the avatar for identity", - "type": "string" - }, - "displayName": { - "description": "Display name for identity.", - "type": "string", - "default": "" - }, - "identity": { - "type": "object", - "required": [ - "type", - "name" - ], - "properties": { - "name": { - "description": "Name is the unique identifier for identity, guaranteed to be a unique value for the type within a namespace.", - "type": "string", - "default": "" - }, - "type": { - "description": "Type of identity e.g. \"user\". For a full list see https://github.com/grafana/authlib/blob/d6737a7dc8f55e9d42834adb83b5da607ceed293/types/type.go#L15", - "type": "string", - "default": "" - } - } - }, - "internalId": { - "description": "InternalID is the legacy numeric id for identity, Deprecated: use the identityRef where possible", - "type": "integer", - "format": "int64" - } - } - }, - "x-kubernetes-list-type": "atomic" - }, - "invalidKeys": { - "description": "Input keys that were not useable", - "type": "array", - "items": { - "type": "string", - "default": "" - }, - "x-kubernetes-list-type": "set" - }, - "keys": { - "description": "Request keys used to lookup the display value", - "type": "array", - "items": { - "type": "string", - "default": "" - }, - "x-kubernetes-list-type": "set" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {} - } - } + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.DisplayList" } } } @@ -2511,6 +2430,90 @@ "additionalProperties": true, "x-kubernetes-preserve-unknown-fields": true }, + "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.Display": { + "type": "object", + "required": [ + "identity", + "displayName" + ], + "properties": { + "avatarURL": { + "description": "AvatarURL is the url where we can get the avatar for identity", + "type": "string" + }, + "displayName": { + "description": "Display name for identity.", + "type": "string", + "default": "" + }, + "identity": { + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.IdentityRef" + } + ] + }, + "internalId": { + "description": "InternalID is the legacy numeric id for identity, Deprecated: use the identityRef where possible", + "type": "integer", + "format": "int64" + } + } + }, + "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.DisplayList": { + "type": "object", + "required": [ + "keys", + "display" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "display": { + "description": "Matching items (the caller may need to remap from keys to results)", + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.Display" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "invalidKeys": { + "description": "Input keys that were not useable", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set" + }, + "keys": { + "description": "Request keys used to lookup the display value", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + } + }, "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.IdentityRef": { "type": "object", "required": [ @@ -2734,6 +2737,45 @@ } } }, + "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountToken": { + "type": "object", + "required": [ + "created" + ], + "properties": { + "created": {}, + "expires": {}, + "lastUsed": {}, + "name": { + "type": "string" + }, + "revoked": { + "type": "boolean" + } + } + }, + "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountTokenList": { + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.Team": { "type": "object", "properties": { diff --git a/public/app/features/explore/QueryLibrary/utils/dataFetching.ts b/public/app/features/explore/QueryLibrary/utils/dataFetching.ts index 03eea720d62..e874d6a331e 100644 --- a/public/app/features/explore/QueryLibrary/utils/dataFetching.ts +++ b/public/app/features/explore/QueryLibrary/utils/dataFetching.ts @@ -7,17 +7,16 @@ import { getDataSourceSrv } from '@grafana/runtime'; import { DataQuery, DataSourceRef } from '@grafana/schema'; import { createQueryText } from '../../../../core/utils/richHistory'; -import { useGetDisplayListQuery } from '../../../iam'; +import { useGetDisplayMappingQuery } from '../../../iam'; import { getDatasourceSrv } from '../../../plugins/datasource_srv'; import { QueryTemplate } from '../../../query-library/types'; export function useLoadUsers(userUIDs: string[] | undefined) { const userQtList = uniq(compact(userUIDs)); - const usersParam = userQtList.map((userUid) => `key=${encodeURIComponent(userUid)}`).join('&'); - return useGetDisplayListQuery( + return useGetDisplayMappingQuery( userUIDs ? { - name: `name?${usersParam}`, + key: userQtList, } : skipToken ); diff --git a/public/app/features/iam/api/endpoints.gen.ts b/public/app/features/iam/api/endpoints.gen.ts index 5a9acf6c123..9f424e5ec09 100644 --- a/public/app/features/iam/api/endpoints.gen.ts +++ b/public/app/features/iam/api/endpoints.gen.ts @@ -1,28 +1,33 @@ import { iamApi as api } from './api'; -export const addTagTypes = ['DisplayList'] as const; +export const addTagTypes = ['Display'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, }) .injectEndpoints({ endpoints: (build) => ({ - getDisplayList: build.query({ - query: (queryArg) => ({ url: `/display/${queryArg.name}` }), - providesTags: ['DisplayList'], + getDisplayMapping: build.query({ + query: (queryArg) => ({ + url: `/display`, + params: { + key: queryArg.key, + }, + }), + providesTags: ['Display'], }), }), overrideExisting: false, }); export { injectedRtkApi as generatedIamApi }; -export type GetDisplayListApiResponse = /** status 200 OK */ DisplayList; -export type GetDisplayListApiArg = { - /** name of the DisplayList */ - name: string; +export type GetDisplayMappingApiResponse = /** status 200 undefined */ DisplayList; +export type GetDisplayMappingApiArg = { + /** Display keys */ + key: string[]; }; export type IdentityRef = { - /** Name is the unique identifier for identity, guaranteed jo be a unique value for the type within a namespace. */ + /** Name is the unique identifier for identity, guaranteed to be a unique value for the type within a namespace. */ name: string; - /** Type of identity e.g. "user". For a full list see https://github.com/grafana/authlib/blob/2f8d13a83ca3e82da08b53726de1697ee5b5b4cc/claims/type.go#L15-L24 */ + /** Type of identity e.g. "user". For a full list see https://github.com/grafana/authlib/blob/d6737a7dc8f55e9d42834adb83b5da607ceed293/types/type.go#L15 */ type: string; }; export type Display = { @@ -31,7 +36,7 @@ export type Display = { /** Display name for identity. */ displayName: string; identity: IdentityRef; - /** InternalID is the legacy numreric id for identity, this is deprecated and should be phased out */ + /** InternalID is the legacy numeric id for identity, Deprecated: use the identityRef where possible */ internalId?: number; }; export type ListMeta = { diff --git a/public/app/features/iam/api/scripts/generate-rtk-apis.ts b/public/app/features/iam/api/scripts/generate-rtk-apis.ts index 60c8affe5a0..238d0f6bc75 100644 --- a/public/app/features/iam/api/scripts/generate-rtk-apis.ts +++ b/public/app/features/iam/api/scripts/generate-rtk-apis.ts @@ -28,7 +28,7 @@ const config: ConfigFile = { '../endpoints.gen.ts': { apiFile: '../api.ts', apiImport: 'iamApi', - filterEndpoints: ['getDisplayList'], + filterEndpoints: ['getDisplayMapping'], exportName: 'generatedIamApi', flattenArg: false, }, diff --git a/public/app/features/iam/index.ts b/public/app/features/iam/index.ts index ff22af53364..5236538f8dd 100644 --- a/public/app/features/iam/index.ts +++ b/public/app/features/iam/index.ts @@ -1,3 +1,3 @@ import { generatedIamApi } from './api/endpoints.gen'; -export const { useGetDisplayListQuery } = generatedIamApi; +export const { useGetDisplayMappingQuery } = generatedIamApi; From 64967051206263788d05a09a08ac06bb1235e2b4 Mon Sep 17 00:00:00 2001 From: jackyin Date: Mon, 3 Feb 2025 21:55:41 +0800 Subject: [PATCH 299/894] Dashboards: Remove default empty string from variable create view (#98922) * default empty string * optimize --- e2e/dashboards-suite/new-query-variable.spec.ts | 2 +- .../dashboard-scene/settings/variables/VariableEditorForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/dashboards-suite/new-query-variable.spec.ts b/e2e/dashboards-suite/new-query-variable.spec.ts index d6ead2937c4..daed9222d17 100644 --- a/e2e/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/dashboards-suite/new-query-variable.spec.ts @@ -72,7 +72,7 @@ describe('Variables - Query - Add variable', () => { cy.get('input[type="checkbox"]').should('not.be.checked'); }); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.have.text'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.exist'); e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().should('not.exist'); }); diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx index 4e2e2777825..d3285be7584 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx @@ -113,7 +113,7 @@ export function VariableEditorForm({ {EditorToRender && } - {isHasVariableOptions && } + {isHasVariableOptions && }
From 489c5006b44bb8c005971a82dbd9e3bb6cdb9438 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:00:32 +0100 Subject: [PATCH 300/894] Alerting: Update irm links for incident and oncall in case new irm plugin is present (#99952) Update irm links for incident and oncall in case new irm plugin is present --- .../configuration-tracker/incidents/hooks.ts | 4 +-- .../gops/configuration-tracker/irmHooks.ts | 25 +++++++++++-------- .../configuration-tracker/onCall/hooks.ts | 4 +-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/public/app/features/gops/configuration-tracker/incidents/hooks.ts b/public/app/features/gops/configuration-tracker/incidents/hooks.ts index aabbaf56a2b..748c21b4b11 100644 --- a/public/app/features/gops/configuration-tracker/incidents/hooks.ts +++ b/public/app/features/gops/configuration-tracker/incidents/hooks.ts @@ -1,6 +1,6 @@ import { incidentsApi } from 'app/features/alerting/unified/api/incidentsApi'; import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge'; -import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; +import { getIrmIfPresentOrIncidentPluginId } from 'app/features/alerting/unified/utils/config'; interface IncidentsPluginConfig { isInstalled: boolean; @@ -11,7 +11,7 @@ interface IncidentsPluginConfig { export function useGetIncidentPluginConfig(): IncidentsPluginConfig { const { installed: incidentPluginInstalled, loading: loadingPluginSettings } = usePluginBridge( - SupportedPlugin.Incident + getIrmIfPresentOrIncidentPluginId() ); const { data: incidentsConfig, isLoading: loadingPluginConfig } = incidentsApi.endpoints.getIncidentsPluginConfig.useQuery(); diff --git a/public/app/features/gops/configuration-tracker/irmHooks.ts b/public/app/features/gops/configuration-tracker/irmHooks.ts index 3510de2e5e5..1f02021aff6 100644 --- a/public/app/features/gops/configuration-tracker/irmHooks.ts +++ b/public/app/features/gops/configuration-tracker/irmHooks.ts @@ -3,6 +3,10 @@ import { useMemo } from 'react'; import { locationService } from '@grafana/runtime'; import { useGrafanaContactPoints } from 'app/features/alerting/unified/components/contact-points/useContactPoints'; import { useNotificationPolicyRoute } from 'app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute'; +import { + getIrmIfPresentOrIncidentPluginId, + getIrmIfPresentOrOnCallPluginId, +} from 'app/features/alerting/unified/utils/config'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { RelativeUrl, createRelativeUrl } from 'app/features/alerting/unified/utils/url'; @@ -222,11 +226,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: '/a/grafana-incident-app/walkthrough/generate-key', + url: `/a/${getIrmIfPresentOrIncidentPluginId()}/walkthrough/generate-key`, }, label: 'Initialize', urlLinkOnDone: { - url: '/a/grafana-incident-app', + url: `/a/${getIrmIfPresentOrIncidentPluginId()}`, }, labelOnDone: 'View', }, @@ -238,12 +242,12 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: '/a/grafana-oncall-app/settings', + url: `/a/${getIrmIfPresentOrOnCallPluginId()}/settings`, queryParams: { tab: 'ChatOps', chatOpsTab: 'Slack' }, }, label: 'Connect', urlLinkOnDone: { - url: '/a/grafana-oncall-app/settings', + url: `/a/${getIrmIfPresentOrOnCallPluginId()}/settings`, queryParams: { tab: 'ChatOps' }, }, labelOnDone: 'View', @@ -257,11 +261,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: '/a/grafana-incident-app/integrations/grate.slack', + url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/grate.slack`, }, label: 'Connect', urlLinkOnDone: { - url: '/a/grafana-incident-app/integrations', + url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations`, }, }, done: isChatOpsInstalled, @@ -272,11 +276,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: '/a/grafana-oncall-app/integrations/', + url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, }, label: 'Add', urlLinkOnDone: { - url: '/a/grafana-oncall-app/integrations/', + url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, }, labelOnDone: 'View', }, @@ -295,7 +299,8 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { type: 'dropDown', label: 'Select integration', options: onCallOptions, - onClickOption: (value) => onIntegrationClick(value, '/a/grafana-oncall-app/integrations/'), + onClickOption: (value) => + onIntegrationClick(value, `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`), stepNotAvailableText: 'No integrations available', }, }, @@ -305,7 +310,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: '/a/grafana-incident-app', + url: `/a/${getIrmIfPresentOrIncidentPluginId()}`, queryParams: { declare: 'new', drill: '1' }, }, label: 'Start drill', diff --git a/public/app/features/gops/configuration-tracker/onCall/hooks.ts b/public/app/features/gops/configuration-tracker/onCall/hooks.ts index 7fb4862970d..fe7fb50c056 100644 --- a/public/app/features/gops/configuration-tracker/onCall/hooks.ts +++ b/public/app/features/gops/configuration-tracker/onCall/hooks.ts @@ -1,9 +1,9 @@ import { onCallApi } from 'app/features/alerting/unified/api/onCallApi'; import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge'; -import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; +import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config'; export function useGetOnCallIntegrations() { - const { installed: onCallPluginInstalled } = usePluginBridge(SupportedPlugin.OnCall); + const { installed: onCallPluginInstalled } = usePluginBridge(getIrmIfPresentOrOnCallPluginId()); const { data: onCallIntegrations } = onCallApi.endpoints.grafanaOnCallIntegrations.useQuery(undefined, { skip: !onCallPluginInstalled, From c3a55ab8cb1d8e6ca83a7732892cdd6c94791e88 Mon Sep 17 00:00:00 2001 From: Mitch Seaman Date: Mon, 3 Feb 2025 15:09:36 +0100 Subject: [PATCH 301/894] Docs: Correct license token renewal frequency (#99969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Irene Rodríguez --- docs/sources/administration/enterprise-licensing/_index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/administration/enterprise-licensing/_index.md b/docs/sources/administration/enterprise-licensing/_index.md index b532bcf74db..16d7e9899f1 100644 --- a/docs/sources/administration/enterprise-licensing/_index.md +++ b/docs/sources/administration/enterprise-licensing/_index.md @@ -246,9 +246,9 @@ Your license is controlled by the following rules: As the license expiration date approaches, you will see a banner in Grafana that encourages you to renew. To learn about how to renew your license and what happens in Grafana when a license expires, refer to [License expiration]({{< relref "#license-expiration" >}}). -**License token expiration:** Your license must contain a valid token, which renews periodically. +**License token expiration:** Grafana Enterprise requires a valid token, which is automatically renewed. -A license token is a digital key that activates your license. By default, license tokens renew every 7 days by calling the Grafana.com API. Short-lived license tokens enable more frequent validation that licenses are compliant, and allow for more frequent license updates - for example, adding users or invalidating a compromised license. +A license token is a digital key that activates your license. By default, the license token is renewed every 24 hours by calling the Grafana API. Short-lived license tokens enable more frequent validation that licenses are compliant, and allow for more frequent license updates - for example, adding users or invalidating a compromised license. To view the details of your license token, sign in to Grafana Enterprise as a Server Administrator and visit **Administration** > **General** > **Statistics and licensing**. Token details are in the Token section under License Details. From 302e90b8f8c56dcb35631364dd50bf8ccb8ee770 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:09:51 +0000 Subject: [PATCH 302/894] TraceView: Add class name for Explore Traces to hide span details row (#98946) Add class name for Explore Traces to hide span details row --- .../components/TraceTimelineViewer/VirtualizedTraceView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx index 1a15f6ace0d..c769f585b77 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { isEqual } from 'lodash'; import memoizeOne from 'memoize-one'; import * as React from 'react'; @@ -566,7 +566,7 @@ export class UnthemedVirtualizedTraceView extends React.Component +
Date: Mon, 3 Feb 2025 14:14:41 +0000 Subject: [PATCH 303/894] Tempo: Fix devenv (#99750) Fix Tempo devenv --- devenv/docker/blocks/tempo/tempo.yaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/devenv/docker/blocks/tempo/tempo.yaml b/devenv/docker/blocks/tempo/tempo.yaml index 7cfb1230a3f..b1130a6e27c 100644 --- a/devenv/docker/blocks/tempo/tempo.yaml +++ b/devenv/docker/blocks/tempo/tempo.yaml @@ -6,15 +6,23 @@ distributor: jaeger: # the receives all come from the OpenTelemetry collector. more configuration information can protocols: # be found there: https://github.com/open-telemetry/opentelemetry-collector/tree/main/receiver thrift_http: # - grpc: # for a production deployment you should only enable the receivers you need! + endpoint: "tempo:14268" # for a production deployment you should only enable the receivers you need! + grpc: + endpoint: "tempo:14250" thrift_binary: + endpoint: "tempo:6832" thrift_compact: + endpoint: "tempo:6831" zipkin: + endpoint: "tempo:9411" otlp: protocols: - http: grpc: + endpoint: "tempo:4317" + http: + endpoint: "tempo:4318" opencensus: + endpoint: "tempo:55678" compactor: compaction: From d6c1e3bb453908c7766af92cd56f320f255d4862 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Mon, 3 Feb 2025 15:38:16 +0100 Subject: [PATCH 304/894] Alerting: Use org store to read organization IDs (#99938) --- pkg/services/ngalert/ngalert.go | 2 +- .../ngalert/notifier/multiorg_alertmanager.go | 2 +- .../notifier/multiorg_alertmanager_test.go | 2 +- pkg/services/ngalert/notifier/testing.go | 2 +- pkg/services/ngalert/state/manager.go | 13 ++-- pkg/services/ngalert/state/manager_test.go | 45 ++++++++---- .../ngalert/state/multi_instance_reader.go | 26 ------- .../state/multi_instance_reader_test.go | 73 ------------------- pkg/services/ngalert/state/persist.go | 5 +- pkg/services/ngalert/state/testing.go | 2 - .../ngalert/store/instance_database.go | 23 ------ pkg/services/ngalert/store/org.go | 4 +- pkg/services/ngalert/store/org_test.go | 47 ++++++++++++ .../ngalert/store/proto_instance_database.go | 23 ------ pkg/services/ngalert/testutil/testutil.go | 8 ++ 15 files changed, 104 insertions(+), 173 deletions(-) create mode 100644 pkg/services/ngalert/store/org_test.go diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index c7fe68d1d68..841870f48d8 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -616,7 +616,7 @@ func (ng *AlertNG) Run(ctx context.Context) error { // Also note that this runs synchronously to ensure state is loaded // before rule evaluation begins, hence we use ctx and not subCtx. // - ng.stateManager.Warm(ctx, ng.store, ng.StartupInstanceReader) + ng.stateManager.Warm(ctx, ng.store, ng.store, ng.StartupInstanceReader) children.Go(func() error { return ng.schedule.Run(subCtx) diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager.go b/pkg/services/ngalert/notifier/multiorg_alertmanager.go index 9c7f2cd3fa5..c66cc6a7772 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager.go @@ -254,7 +254,7 @@ func (moa *MultiOrgAlertmanager) Run(ctx context.Context) error { func (moa *MultiOrgAlertmanager) LoadAndSyncAlertmanagersForOrgs(ctx context.Context) error { moa.logger.Debug("Synchronizing Alertmanagers for orgs") // First, load all the organizations from the database. - orgIDs, err := moa.orgStore.GetOrgs(ctx) + orgIDs, err := moa.orgStore.FetchOrgIds(ctx) if err != nil { return err } diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go b/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go index e53da1fa451..2fb610d3d5a 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go @@ -127,7 +127,7 @@ func TestMultiOrgAlertmanager_SyncAlertmanagersForOrgsWithFailures(t *testing.T) 2: {AlertmanagerConfiguration: brokenConfig, OrgID: orgWithBadConfig}, }) - orgs, err := mam.orgStore.GetOrgs(ctx) + orgs, err := mam.orgStore.FetchOrgIds(ctx) require.NoError(t, err) // No successfully applied configurations should be found at first. { diff --git a/pkg/services/ngalert/notifier/testing.go b/pkg/services/ngalert/notifier/testing.go index 8c268c3de93..c26b8307a46 100644 --- a/pkg/services/ngalert/notifier/testing.go +++ b/pkg/services/ngalert/notifier/testing.go @@ -224,7 +224,7 @@ func NewFakeOrgStore(t *testing.T, orgs []int64) *FakeOrgStore { } } -func (f *FakeOrgStore) GetOrgs(_ context.Context) ([]int64, error) { +func (f *FakeOrgStore) FetchOrgIds(_ context.Context) ([]int64, error) { return f.orgs, nil } diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index b503f7ad25c..e037c673d4b 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -128,20 +128,21 @@ func (st *Manager) Run(ctx context.Context) error { return nil } -func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceReader InstanceReader) { +func (st *Manager) Warm(ctx context.Context, orgReader OrgReader, rulesReader RuleReader, instanceReader InstanceReader) { logger := st.log.FromContext(ctx) - if st.instanceStore == nil { - logger.Info("Skip warming the state because instance store is not configured") + if orgReader == nil || rulesReader == nil || instanceReader == nil { + logger.Error("Unable to warm state cache, missing required store readers") return } startTime := time.Now() logger.Info("Warming state cache for startup") - orgIds, err := instanceReader.FetchOrgIds(ctx) + orgIds, err := orgReader.FetchOrgIds(ctx) if err != nil { - logger.Error("Unable to fetch orgIds", "error", err) + logger.Error("Unable to warm state cache, failed to fetch org IDs", "error", err) + return } statesCount := 0 @@ -203,7 +204,7 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceRea if entry.ResultFingerprint != "" { fp, err := strconv.ParseUint(entry.ResultFingerprint, 16, 64) if err != nil { - logger.Error("Failed to parse result fingerprint of alert instance", "error", err, "ruleUID", entry.RuleUID) + logger.Error("Failed to parse result fingerprint of alert instance", "error", err, "rule_uid", entry.RuleUID) } resultFp = data.Fingerprint(fp) } diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 8bc43fee306..b6b863f40df 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -35,6 +35,9 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/state/historian" "github.com/grafana/grafana/pkg/services/ngalert/tests" + alertTestUtil "github.com/grafana/grafana/pkg/services/ngalert/testutil" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) @@ -49,8 +52,12 @@ func TestWarmStateCache(t *testing.T) { ctx := context.Background() ng, dbstore := tests.SetupTestEnv(t, 1) - const mainOrgID int64 = 1 - rule := tests.CreateTestAlertRule(t, ctx, dbstore, 600, mainOrgID) + orgService, err := alertTestUtil.SetupOrgService(t, dbstore.SQLStore, setting.NewCfg()) + require.NoError(t, err) + mainOrg, err := orgService.CreateWithMember(ctx, &org.CreateOrgCommand{}) + require.NoError(t, err) + + rule := tests.CreateTestAlertRule(t, ctx, dbstore, 600, mainOrg.ID) expectedEntries := []*state.State{ { @@ -230,7 +237,7 @@ func TestWarmStateCache(t *testing.T) { Log: log.New("ngalert.state.manager"), } st := state.NewManager(cfg, state.NewNoopPersister()) - st.Warm(ctx, dbstore, ng.InstanceStore) + st.Warm(ctx, dbstore, dbstore, ng.InstanceStore) t.Run("instance cache has expected entries", func(t *testing.T) { for _, entry := range expectedEntries { @@ -277,7 +284,7 @@ func TestDashboardAnnotations(t *testing.T) { "test2": "{{ $labels.instance_label }}", }) - st.Warm(ctx, dbstore, ng.InstanceStore) + st.Warm(ctx, dbstore, dbstore, ng.InstanceStore) bValue := float64(42) cValue := float64(1) _ = st.ProcessEvalResults(ctx, evaluationTime, rule, eval.Results{{ @@ -1699,8 +1706,12 @@ func TestStaleResultsHandler(t *testing.T) { ctx := context.Background() ng, dbstore := tests.SetupTestEnv(t, 1) - const mainOrgID int64 = 1 - rule := tests.CreateTestAlertRule(t, ctx, dbstore, int64(interval.Seconds()), mainOrgID) + orgService, err := alertTestUtil.SetupOrgService(t, dbstore.SQLStore, setting.NewCfg()) + require.NoError(t, err) + mainOrg, err := orgService.CreateWithMember(ctx, &org.CreateOrgCommand{}) + require.NoError(t, err) + + rule := tests.CreateTestAlertRule(t, ctx, dbstore, int64(interval.Seconds()), mainOrg.ID) lastEval := evaluationTime.Add(-2 * interval) labels1 := models.InstanceLabels{ @@ -1813,7 +1824,7 @@ func TestStaleResultsHandler(t *testing.T) { Log: log.New("ngalert.state.manager"), } st := state.NewManager(cfg, state.NewNoopPersister()) - st.Warm(ctx, dbstore, ng.InstanceStore) + st.Warm(ctx, dbstore, dbstore, ng.InstanceStore) existingStatesForRule := st.GetStatesForRuleUID(rule.OrgID, rule.UID) // We have loaded the expected number of entries from the db @@ -1980,8 +1991,12 @@ func TestDeleteStateByRuleUID(t *testing.T) { ctx := context.Background() ng, dbstore := tests.SetupTestEnv(t, 1) - const mainOrgID int64 = 1 - rule := tests.CreateTestAlertRule(t, ctx, dbstore, int64(interval.Seconds()), mainOrgID) + orgService, err := alertTestUtil.SetupOrgService(t, dbstore.SQLStore, setting.NewCfg()) + require.NoError(t, err) + mainOrg, err := orgService.CreateWithMember(ctx, &org.CreateOrgCommand{}) + require.NoError(t, err) + + rule := tests.CreateTestAlertRule(t, ctx, dbstore, int64(interval.Seconds()), mainOrg.ID) labels1 := models.InstanceLabels{"test1": "testValue1"} _, hash1, _ := labels1.StringAndHash() @@ -2073,7 +2088,7 @@ func TestDeleteStateByRuleUID(t *testing.T) { Log: log.New("ngalert.state.manager"), } st := state.NewManager(cfg, state.NewNoopPersister()) - st.Warm(ctx, dbstore, ng.InstanceStore) + st.Warm(ctx, dbstore, dbstore, ng.InstanceStore) q := &models.ListAlertInstancesQuery{RuleOrgID: rule.OrgID, RuleUID: rule.UID} alerts, _ := ng.InstanceStore.ListAlertInstances(ctx, q) existingStatesForRule := st.GetStatesForRuleUID(rule.OrgID, rule.UID) @@ -2122,8 +2137,12 @@ func TestResetStateByRuleUID(t *testing.T) { ctx := context.Background() ng, dbstore := tests.SetupTestEnv(t, 1) - const mainOrgID int64 = 1 - rule := tests.CreateTestAlertRule(t, ctx, dbstore, int64(interval.Seconds()), mainOrgID) + orgService, err := alertTestUtil.SetupOrgService(t, dbstore.SQLStore, setting.NewCfg()) + require.NoError(t, err) + mainOrg, err := orgService.CreateWithMember(ctx, &org.CreateOrgCommand{}) + require.NoError(t, err) + + rule := tests.CreateTestAlertRule(t, ctx, dbstore, int64(interval.Seconds()), mainOrg.ID) labels1 := models.InstanceLabels{"test1": "testValue1"} _, hash1, _ := labels1.StringAndHash() @@ -2214,7 +2233,7 @@ func TestResetStateByRuleUID(t *testing.T) { Log: log.New("ngalert.state.manager"), } st := state.NewManager(cfg, state.NewNoopPersister()) - st.Warm(ctx, dbstore, ng.InstanceStore) + st.Warm(ctx, dbstore, dbstore, ng.InstanceStore) q := &models.ListAlertInstancesQuery{RuleOrgID: rule.OrgID, RuleUID: rule.UID} alerts, _ := ng.InstanceStore.ListAlertInstances(ctx, q) existingStatesForRule := st.GetStatesForRuleUID(rule.OrgID, rule.UID) diff --git a/pkg/services/ngalert/state/multi_instance_reader.go b/pkg/services/ngalert/state/multi_instance_reader.go index bf71bd85aab..d6f15993edc 100644 --- a/pkg/services/ngalert/state/multi_instance_reader.go +++ b/pkg/services/ngalert/state/multi_instance_reader.go @@ -3,8 +3,6 @@ package state import ( "context" "fmt" - "maps" - "slices" "time" "github.com/grafana/grafana/pkg/infra/log" @@ -28,30 +26,6 @@ func NewMultiInstanceReader(logger log.Logger, r1, r2 InstanceReader) *MultiInst } } -// FetchOrgIds merges org IDs from both readers. -func (m *MultiInstanceReader) FetchOrgIds(ctx context.Context) ([]int64, error) { - orgsOne, err := m.ProtoDBReader.FetchOrgIds(ctx) - if err != nil { - return nil, fmt.Errorf("failed to fetch org IDs from ProtoDBReader: %w", err) - } - - orgsTwo, err := m.DBReader.FetchOrgIds(ctx) - if err != nil { - return nil, fmt.Errorf("failed to fetch org IDs from DBReader: %w", err) - } - - orgsSet := make(map[int64]struct{}) - - for _, orgID := range orgsOne { - orgsSet[orgID] = struct{}{} - } - for _, orgID := range orgsTwo { - orgsSet[orgID] = struct{}{} - } - - return slices.Collect(maps.Keys(orgsSet)), nil -} - // ListAlertInstances fetches alert instances for a query from both readers, // groups them by rule UID, and returns the newest instances for each rule as a // single slice. diff --git a/pkg/services/ngalert/state/multi_instance_reader_test.go b/pkg/services/ngalert/state/multi_instance_reader_test.go index 5e20b594162..dad65996e9f 100644 --- a/pkg/services/ngalert/state/multi_instance_reader_test.go +++ b/pkg/services/ngalert/state/multi_instance_reader_test.go @@ -17,84 +17,11 @@ type mockInstanceReader struct { mock.Mock } -func (m *mockInstanceReader) FetchOrgIds(ctx context.Context) ([]int64, error) { - args := m.Called(ctx) - return args.Get(0).([]int64), args.Error(1) -} - func (m *mockInstanceReader) ListAlertInstances(ctx context.Context, cmd *models.ListAlertInstancesQuery) ([]*models.AlertInstance, error) { args := m.Called(ctx, cmd) return args.Get(0).([]*models.AlertInstance), args.Error(1) } -func TestMultiInstanceReader_FetchOrgIds(t *testing.T) { - tests := []struct { - name string - mockAOrgIDs []int64 - mockBOrgIDs []int64 - mockAError error - mockBError error - expectedOrgIDs []int64 - expectError bool - }{ - { - name: "both readers empty, no errors", - mockAOrgIDs: []int64{}, - mockBOrgIDs: []int64{}, - expectedOrgIDs: []int64{}, - }, - { - name: "simple union, no errors", - mockAOrgIDs: []int64{1, 2}, - mockBOrgIDs: []int64{2, 3}, - expectedOrgIDs: []int64{1, 2, 3}, - }, - { - name: "error in readerA", - mockAError: errors.New("some error"), - expectError: true, - }, - { - name: "error in readerB", - mockBError: errors.New("another error"), - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - - readerA := &mockInstanceReader{} - if tc.mockAError != nil { - readerA.On("FetchOrgIds", mock.Anything).Return([]int64(nil), tc.mockAError).Once() - } else { - readerA.On("FetchOrgIds", mock.Anything).Return(tc.mockAOrgIDs, nil).Once() - } - - readerB := &mockInstanceReader{} - if tc.mockBError != nil { - readerB.On("FetchOrgIds", mock.Anything).Return([]int64(nil), tc.mockBError).Once() - } else { - readerB.On("FetchOrgIds", mock.Anything).Return(tc.mockBOrgIDs, nil).Once() - } - - multi := NewMultiInstanceReader(&logtest.Fake{}, readerA, readerB) - orgIDs, err := multi.FetchOrgIds(ctx) - - if tc.expectError { - require.Error(t, err) - return - } - require.NoError(t, err) - require.ElementsMatch(t, tc.expectedOrgIDs, orgIDs) - - readerA.AssertExpectations(t) - readerB.AssertExpectations(t) - }) - } -} - func TestMultiInstanceReader_ListAlertInstances(t *testing.T) { t1 := time.Unix(100, 0) t2 := time.Unix(200, 0) diff --git a/pkg/services/ngalert/state/persist.go b/pkg/services/ngalert/state/persist.go index 841a7dacdd9..7c133fcdfd8 100644 --- a/pkg/services/ngalert/state/persist.go +++ b/pkg/services/ngalert/state/persist.go @@ -15,7 +15,6 @@ type InstanceStore interface { // InstanceReader provides methods to fetch alert instances. type InstanceReader interface { - FetchOrgIds(ctx context.Context) ([]int64, error) ListAlertInstances(ctx context.Context, cmd *models.ListAlertInstancesQuery) ([]*models.AlertInstance, error) } @@ -29,6 +28,10 @@ type InstanceWriter interface { FullSync(ctx context.Context, instances []models.AlertInstance, batchSize int) error } +type OrgReader interface { + FetchOrgIds(ctx context.Context) ([]int64, error) +} + // RuleReader represents the ability to fetch alert rules. type RuleReader interface { ListAlertRules(ctx context.Context, query *models.ListAlertRulesQuery) (models.RulesGroup, error) diff --git a/pkg/services/ngalert/state/testing.go b/pkg/services/ngalert/state/testing.go index 8ed194cbf25..40da1c38eb0 100644 --- a/pkg/services/ngalert/state/testing.go +++ b/pkg/services/ngalert/state/testing.go @@ -44,8 +44,6 @@ func (f *FakeInstanceStore) SaveAlertInstance(_ context.Context, q models.AlertI return nil } -func (f *FakeInstanceStore) FetchOrgIds(_ context.Context) ([]int64, error) { return []int64{}, nil } - func (f *FakeInstanceStore) DeleteAlertInstances(ctx context.Context, q ...models.AlertInstanceKey) error { f.mtx.Lock() defer f.mtx.Unlock() diff --git a/pkg/services/ngalert/store/instance_database.go b/pkg/services/ngalert/store/instance_database.go index 6201188c9ff..4633920edb3 100644 --- a/pkg/services/ngalert/store/instance_database.go +++ b/pkg/services/ngalert/store/instance_database.go @@ -96,29 +96,6 @@ func (st InstanceDBStore) SaveAlertInstance(ctx context.Context, alertInstance m }) } -func (st InstanceDBStore) FetchOrgIds(ctx context.Context) ([]int64, error) { - orgIds := []int64{} - - err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { - s := strings.Builder{} - params := make([]any, 0) - - addToQuery := func(stmt string, p ...any) { - s.WriteString(stmt) - params = append(params, p...) - } - - addToQuery("SELECT DISTINCT rule_org_id FROM alert_instance") - - if err := sess.SQL(s.String(), params...).Find(&orgIds); err != nil { - return err - } - return nil - }) - - return orgIds, err -} - // DeleteAlertInstances deletes instances with the provided keys in a single transaction. func (st InstanceDBStore) DeleteAlertInstances(ctx context.Context, keys ...models.AlertInstanceKey) error { if len(keys) == 0 { diff --git a/pkg/services/ngalert/store/org.go b/pkg/services/ngalert/store/org.go index 1e63c65b17c..7acb4696781 100644 --- a/pkg/services/ngalert/store/org.go +++ b/pkg/services/ngalert/store/org.go @@ -7,10 +7,10 @@ import ( ) type OrgStore interface { - GetOrgs(ctx context.Context) ([]int64, error) + FetchOrgIds(ctx context.Context) ([]int64, error) } -func (st DBstore) GetOrgs(ctx context.Context) ([]int64, error) { +func (st DBstore) FetchOrgIds(ctx context.Context) ([]int64, error) { orgs := make([]int64, 0) err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { q := "SELECT id FROM org" diff --git a/pkg/services/ngalert/store/org_test.go b/pkg/services/ngalert/store/org_test.go new file mode 100644 index 00000000000..da73f255972 --- /dev/null +++ b/pkg/services/ngalert/store/org_test.go @@ -0,0 +1,47 @@ +package store + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/ngalert/testutil" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/setting" +) + +func TestFetchOrgIds(t *testing.T) { + ctx := context.Background() + + t.Run("returns empty result when no orgs exist", func(t *testing.T) { + sqlStore := db.InitTestDB(t) + store := &DBstore{SQLStore: sqlStore} + orgIDs, err := store.FetchOrgIds(ctx) + require.NoError(t, err) + require.Empty(t, orgIDs) + }) + + t.Run("returns all org IDs", func(t *testing.T) { + sqlStore := db.InitTestDB(t) + store := &DBstore{SQLStore: sqlStore} + orgService, err := testutil.SetupOrgService(t, sqlStore, setting.NewCfg()) + require.NoError(t, err) + + createdOrgIDs := make([]int64, 3) + + for i := range 3 { + require.NoError(t, err) + newOrg, err := orgService.CreateWithMember(ctx, &org.CreateOrgCommand{Name: fmt.Sprintf("org-%d", i)}) + require.NoError(t, err) + createdOrgIDs[i] = newOrg.ID + } + + orgIDs, err := store.FetchOrgIds(ctx) + + require.NoError(t, err) + require.ElementsMatch(t, createdOrgIDs, orgIDs) + }) +} diff --git a/pkg/services/ngalert/store/proto_instance_database.go b/pkg/services/ngalert/store/proto_instance_database.go index e713d43be31..4e8c8c60520 100644 --- a/pkg/services/ngalert/store/proto_instance_database.go +++ b/pkg/services/ngalert/store/proto_instance_database.go @@ -93,29 +93,6 @@ func (st ProtoInstanceDBStore) SaveAlertInstance(ctx context.Context, alertInsta return errors.New("save alert instance is not implemented for proto instance database store") } -func (st ProtoInstanceDBStore) FetchOrgIds(ctx context.Context) ([]int64, error) { - orgIds := []int64{} - - err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { - s := strings.Builder{} - params := make([]any, 0) - - addToQuery := func(stmt string, p ...any) { - s.WriteString(stmt) - params = append(params, p...) - } - - addToQuery("SELECT DISTINCT org_id FROM alert_rule_state") - - if err := sess.SQL(s.String(), params...).Find(&orgIds); err != nil { - return err - } - return nil - }) - - return orgIds, err -} - func (st ProtoInstanceDBStore) DeleteAlertInstances(ctx context.Context, keys ...models.AlertInstanceKey) error { logger := st.Logger.FromContext(ctx) logger.Error("DeleteAlertInstances called and not implemented") diff --git a/pkg/services/ngalert/testutil/testutil.go b/pkg/services/ngalert/testutil/testutil.go index 284322e1571..7aa8c1d45b3 100644 --- a/pkg/services/ngalert/testutil/testutil.go +++ b/pkg/services/ngalert/testutil/testutil.go @@ -19,6 +19,8 @@ import ( "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" @@ -67,3 +69,9 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa return dashboardService, dashboardStore } + +func SetupOrgService(tb testing.TB, sqlStore db.DB, cfg *setting.Cfg) (org.Service, error) { + tb.Helper() + quotaService := quotatest.New(false, nil) + return orgimpl.ProvideService(sqlStore, cfg, quotaService) +} From 62aaec14b6b7c052f3f902d9fba43d09671ce43f Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Mon, 3 Feb 2025 09:46:34 -0500 Subject: [PATCH 305/894] LBAC for datasources: Enabled by default - expression "true" (#99971) * add expression "true" * update gen json --- pkg/services/featuremgmt/registry.go | 1 + pkg/services/featuremgmt/toggles_gen.json | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a99c4ab251e..ffec9103ac7 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -772,6 +772,7 @@ var ( FrontendOnly: false, AllowSelfServe: true, Owner: identityAccessTeam, + Expression: "true", }, { Name: "cachingOptimizeSerializationMemoryUsage", diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 934a3078871..d1a6a295a3d 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3722,17 +3722,18 @@ { "metadata": { "name": "teamHttpHeaders", - "resourceVersion": "1726836253132", + "resourceVersion": "1738590709387", "creationTimestamp": "2023-10-17T10:23:54Z", "annotations": { - "grafana.app/updatedTimestamp": "2024-09-20 12:44:13.132845 +0000 UTC" + "grafana.app/updatedTimestamp": "2025-02-03 13:51:49.3871 +0000 UTC" } }, "spec": { "description": "Enables LBAC for datasources to apply LogQL filtering of logs to the client requests for users in teams", "stage": "preview", "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true + "allowSelfServe": true, + "expression": "true" } }, { From d96c1169c25d2c4f6740ac7827101ade44a597cb Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:21:38 +0200 Subject: [PATCH 306/894] DashboardLayouts: Multi-select elements (#99257) * wip * refactor to map Co-authored-by: Sergej-Vlasov * refactor + allow selecting any kind of elements * rename class * refactor + tests * cr changes * fix deselection on shift clicking multiselected objects * i18n * fix * move logic to elementSelection * lint fix * unselecting last multiselected item should reopen dashboard options --------- Co-authored-by: Sergej-Vlasov --- .../edit-pane/DashboardEditPane.tsx | 66 +++++-- .../edit-pane/DashboardEditPaneSplitter.tsx | 8 +- .../edit-pane/ElementEditPane.tsx | 6 +- .../edit-pane/ElementSelection.test.ts | 178 +++++++++++++++++ .../edit-pane/ElementSelection.ts | 184 ++++++++++++++++++ .../MultiSelectedObjectsEditableElement.tsx | 40 ++++ .../MultiSelectedVizPanelsEditableElement.tsx | 55 ++++++ .../edit-pane/VizPanelEditableElement.tsx | 4 +- .../edit-pane/useEditableElement.ts | 30 +-- .../MultiSelectedRowItemsElement.tsx | 92 +++++++++ .../scene/layout-rows/RowItem.tsx | 13 +- .../scene/layout-rows/RowsLayoutManager.tsx | 2 +- .../features/dashboard-scene/scene/types.ts | 38 +++- public/locales/en-US/grafana.json | 31 ++- public/locales/pseudo-LOCALE/grafana.json | 31 ++- 15 files changed, 715 insertions(+), 63 deletions(-) create mode 100644 public/app/features/dashboard-scene/edit-pane/ElementSelection.test.ts create mode 100644 public/app/features/dashboard-scene/edit-pane/ElementSelection.ts create mode 100644 public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx create mode 100644 public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-rows/MultiSelectedRowItemsElement.tsx diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index 6376687d480..381011a89ff 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -2,24 +2,18 @@ import { css } from '@emotion/css'; import { useEffect, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { - SceneObjectState, - SceneObjectBase, - SceneObject, - SceneObjectRef, - sceneGraph, - useSceneObjectState, -} from '@grafana/scenes'; +import { SceneObjectState, SceneObjectBase, SceneObject, sceneGraph, useSceneObjectState } from '@grafana/scenes'; import { ElementSelectionContextItem, ElementSelectionContextState, ToolbarButton, useStyles2 } from '@grafana/ui'; import { isInCloneChain } from '../utils/clone'; import { getDashboardSceneFor } from '../utils/utils'; import { ElementEditPane } from './ElementEditPane'; +import { ElementSelection } from './ElementSelection'; import { useEditableElement } from './useEditableElement'; export interface DashboardEditPaneState extends SceneObjectState { - selectedObject?: SceneObjectRef; + selection?: ElementSelection; selectionContext: ElementSelectionContextState; } @@ -42,7 +36,7 @@ export class DashboardEditPane extends SceneObjectBase { public disableSelection() { this.setState({ selectionContext: { ...this.state.selectionContext, selected: [], enabled: false }, - selectedObject: undefined, + selection: undefined, }); } @@ -64,17 +58,49 @@ export class DashboardEditPane extends SceneObjectBase { } public selectObject(obj: SceneObject, id: string, multi?: boolean) { - const currentSelection = this.state.selectedObject?.resolve(); - if (currentSelection === obj) { + if (!this.state.selection) { + return; + } + + const prevItem = this.state.selection.getFirstObject(); + if (prevItem === obj && !multi) { + this.clearSelection(); + return; + } + + if (multi && this.state.selection.hasValue(id)) { + this.removeMultiSelectedObject(id); + return; + } + + const { selection, contextItems: selected } = this.state.selection.getStateWithValue(id, obj, !!multi); + + this.setState({ + selection: new ElementSelection(selection), + selectionContext: { + ...this.state.selectionContext, + selected, + }, + }); + } + + private removeMultiSelectedObject(id: string) { + if (!this.state.selection) { + return; + } + + const { entries, contextItems: selected } = this.state.selection.getStateWithoutValueAt(id); + + if (entries.length === 0) { this.clearSelection(); return; } this.setState({ - selectedObject: obj.getRef(), + selection: new ElementSelection([...entries]), selectionContext: { ...this.state.selectionContext, - selected: [{ id }], + selected, }, }); } @@ -82,7 +108,7 @@ export class DashboardEditPane extends SceneObjectBase { public clearSelection() { const dashboard = getDashboardSceneFor(this); this.setState({ - selectedObject: dashboard.getRef(), + selection: new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]), selectionContext: { ...this.state.selectionContext, selected: [], @@ -103,9 +129,11 @@ export interface Props { export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleCollapse }: Props) { // Activate the edit pane useEffect(() => { - if (!editPane.state.selectedObject) { + if (!editPane.state.selection) { const dashboard = getDashboardSceneFor(editPane); - editPane.setState({ selectedObject: dashboard.getRef() }); + editPane.setState({ + selection: new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]), + }); } editPane.enableSelection(); @@ -115,10 +143,10 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla }; }, [editPane]); - const { selectedObject } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); + const { selection } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); const styles = useStyles2(getStyles); const paneRef = useRef(null); - const editableElement = useEditableElement(selectedObject?.resolve()); + const editableElement = useEditableElement(selection); if (!editableElement) { return null; diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx index c45c47be262..b558a1e2abb 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx @@ -76,7 +76,13 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
editPane.clearSelection()} + onPointerDown={(evt) => { + if (evt.shiftKey) { + return; + } + + editPane.clearSelection(); + }} >
{controls}
diff --git a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx index ff2ef751086..b60b48fd549 100644 --- a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx @@ -4,14 +4,14 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Stack, useStyles2 } from '@grafana/ui'; import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory'; -import { EditableDashboardElement } from '../scene/types'; +import { EditableDashboardElement, MultiSelectedEditableDashboardElement } from '../scene/types'; export interface Props { - element: EditableDashboardElement; + element: EditableDashboardElement | MultiSelectedEditableDashboardElement; } export function ElementEditPane({ element }: Props) { - const categories = element.useEditPaneOptions(); + const categories = element.useEditPaneOptions ? element.useEditPaneOptions() : []; const styles = useStyles2(getStyles); return ( diff --git a/public/app/features/dashboard-scene/edit-pane/ElementSelection.test.ts b/public/app/features/dashboard-scene/edit-pane/ElementSelection.test.ts new file mode 100644 index 00000000000..f76e7ecc23a --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/ElementSelection.test.ts @@ -0,0 +1,178 @@ +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; + +import { DashboardEditableElement } from './DashboardEditableElement'; +import { ElementSelection } from './ElementSelection'; +import { MultiSelectedObjectsEditableElement } from './MultiSelectedObjectsEditableElement'; +import { MultiSelectedVizPanelsEditableElement } from './MultiSelectedVizPanelsEditableElement'; +import { VizPanelEditableElement } from './VizPanelEditableElement'; + +let panel1: VizPanel, panel2: VizPanel, scene: DashboardScene; + +describe('ElementSelection', () => { + beforeAll(() => { + const testScene = buildScene(); + + panel1 = testScene.panel1; + panel2 = testScene.panel2; + scene = testScene.scene; + }); + + it('returns a single object when only one is selected', () => { + const selection = new ElementSelection([['id1', panel1.getRef()]]); + + expect(selection.isMultiSelection).toBe(false); + expect(selection.getSelection()).toBe(panel1); + }); + + it('returns multiple objects when multiple are selected', () => { + const selection = new ElementSelection([ + ['id1', panel1.getRef()], + ['id2', panel2.getRef()], + ]); + + expect(selection.isMultiSelection).toBe(true); + expect(selection.getSelection()).toEqual([panel1, panel2]); + }); + + it('delete element', () => { + const selection = new ElementSelection([ + ['id1', panel1.getRef()], + ['id2', panel2.getRef()], + ]); + + selection.removeValue('id1'); + expect(selection.isMultiSelection).toBe(false); + expect(selection.getSelection()).toEqual(panel2); + }); + + it('returns entries', () => { + const ref1 = panel1.getRef(); + const ref2 = panel2.getRef(); + + const selection = new ElementSelection([ + ['id1', ref1], + ['id2', ref2], + ]); + + expect(selection.isMultiSelection).toBe(true); + expect(selection.getSelectionEntries()).toEqual([ + ['id1', ref1], + ['id2', ref2], + ]); + }); + + it('returns the first selected object through getFirstObject', () => { + const selection = new ElementSelection([ + ['id1', panel1.getRef()], + ['id2', panel2.getRef()], + ]); + + expect(selection.isMultiSelection).toBe(true); + expect(selection.getFirstObject()).toBe(panel1); + }); + + it('creates correct element type for single selection', () => { + const vizSelection = new ElementSelection([['id1', panel1.getRef()]]); + expect(vizSelection.createSelectionElement()).toBeInstanceOf(VizPanelEditableElement); + + const dashboardSelection = new ElementSelection([['id1', scene.getRef()]]); + expect(dashboardSelection.createSelectionElement()).toBeInstanceOf(DashboardEditableElement); + }); + + it('creates correct element type for multi-selection of same type', () => { + const selection = new ElementSelection([ + ['id1', panel1.getRef()], + ['id2', panel2.getRef()], + ]); + + expect(selection.createSelectionElement()).toBeInstanceOf(MultiSelectedVizPanelsEditableElement); + }); + + it('creates MultiSelectedObjectsEditableElement for selection of different object types', () => { + const selection = new ElementSelection([ + ['id1', panel1.getRef()], + ['id2', scene.getRef()], + ]); + + expect(selection.createSelectionElement()).toBeInstanceOf(MultiSelectedObjectsEditableElement); + }); + + it('handles empty selection correctly', () => { + const selection = new ElementSelection([]); + expect(selection.getSelection()).toBeUndefined(); + expect(selection.getFirstObject()).toBeUndefined(); + expect(selection.createSelectionElement()).toBeUndefined(); + }); + + it('returns the entries with the specified value removed', () => { + const selection = new ElementSelection([ + ['id1', panel1.getRef()], + ['id2', panel2.getRef()], + ['id3', scene.getRef()], + ]); + + const { entries, contextItems } = selection.getStateWithoutValueAt('id2'); + expect(entries).toEqual([ + ['id1', panel1.getRef()], + ['id3', scene.getRef()], + ]); + expect(contextItems).toEqual([{ id: 'id1' }, { id: 'id3' }]); + }); + + it('returns the entries with the specified value added in a multi-select scenario', () => { + const selection = new ElementSelection([ + ['id1', panel1.getRef()], + ['id2', panel2.getRef()], + ]); + + const { selection: entries, contextItems } = selection.getStateWithValue('id3', scene, true); + + expect(entries).toEqual([ + ['id3', panel1.getRef()], + ['id1', panel2.getRef()], + ['id2', scene.getRef()], + ]); + expect(contextItems).toEqual([{ id: 'id3' }, { id: 'id1' }, { id: 'id2' }]); + }); + + it('returns the entries with just the specified value added in a non multi-select scenario', () => { + const selection = new ElementSelection([ + ['id1', panel1.getRef()], + ['id2', panel2.getRef()], + ]); + + const { selection: entries, contextItems } = selection.getStateWithValue('id3', scene, false); + + expect(entries).toEqual([['id3', scene.getRef()]]); + expect(contextItems).toEqual([{ id: 'id3' }]); + }); +}); + +function buildScene() { + const panel1 = new VizPanel({ + title: 'Panel A', + // pluginId: 'text', + key: 'panel-12', + }); + + const panel2 = new VizPanel({ + title: 'Panel B', + // pluginId: 'text', + key: 'panel-13', + }); + + const scene = new DashboardScene({ + title: 'hello', + uid: 'dash-1', + meta: { + canEdit: true, + }, + $timeRange: new SceneTimeRange({}), + body: DefaultGridLayoutManager.fromVizPanels([panel1, panel2]), + }); + + return { panel1, panel2, scene }; +} diff --git a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts new file mode 100644 index 00000000000..29f107a2d84 --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts @@ -0,0 +1,184 @@ +import { SceneObject, SceneObjectRef, VizPanel } from '@grafana/scenes'; +import { ElementSelectionContextItem } from '@grafana/ui'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { + EditableDashboardElement, + isBulkActionElement, + isEditableDashboardElement, + MultiSelectedEditableDashboardElement, +} from '../scene/types'; + +import { DashboardEditableElement } from './DashboardEditableElement'; +import { MultiSelectedObjectsEditableElement } from './MultiSelectedObjectsEditableElement'; +import { MultiSelectedVizPanelsEditableElement } from './MultiSelectedVizPanelsEditableElement'; +import { VizPanelEditableElement } from './VizPanelEditableElement'; + +export class ElementSelection { + private selectedObjects?: Map>; + private sameType?: boolean; + + private _isMultiSelection: boolean; + + constructor(values: Array<[string, SceneObjectRef]>) { + this.selectedObjects = new Map(values); + this._isMultiSelection = values.length > 1; + + if (this.isMultiSelection) { + this.sameType = this.checkSameType(); + } + } + + private checkSameType() { + const values = this.selectedObjects?.values(); + const firstType = values?.next().value?.resolve()?.constructor.name; + + if (!firstType) { + return false; + } + + for (let obj of values ?? []) { + if (obj.resolve()?.constructor.name !== firstType) { + return false; + } + } + + return true; + } + + public hasValue(id: string) { + return this.selectedObjects?.has(id); + } + + public removeValue(id: string) { + this.selectedObjects?.delete(id); + + if (this.selectedObjects && this.selectedObjects.size < 2) { + this.sameType = undefined; + this._isMultiSelection = false; + } + } + + public getStateWithValue( + id: string, + obj: SceneObject, + isMulti: boolean + ): { selection: Array<[string, SceneObjectRef]>; contextItems: ElementSelectionContextItem[] } { + const ref = obj.getRef(); + let contextItems = [{ id }]; + let selection: Array<[string, SceneObjectRef]> = [[id, ref]]; + + const entries = this.getSelectionEntries() ?? []; + const items = entries.map(([key]) => ({ id: key })); + + if (isMulti) { + selection = [[id, ref], ...entries]; + contextItems = [{ id }, ...items]; + } + + return { selection, contextItems }; + } + + public getStateWithoutValueAt(id: string): { + entries: Array<[string, SceneObjectRef]>; + contextItems: ElementSelectionContextItem[]; + } { + this.removeValue(id); + const entries = this.getSelectionEntries() ?? []; + const contextItems = entries.map(([key]) => ({ id: key })); + + return { entries, contextItems }; + } + + public getSelection(): SceneObject | SceneObject[] | undefined { + if (this.isMultiSelection) { + return this.getSceneObjects(); + } + + return this.getFirstObject(); + } + + public getSelectionEntries(): Array<[string, SceneObjectRef]> { + return Array.from(this.selectedObjects?.entries() ?? []); + } + + public getFirstObject(): SceneObject | undefined { + return this.selectedObjects?.values().next().value?.resolve(); + } + + public get isMultiSelection(): boolean { + return this._isMultiSelection; + } + + private getSceneObjects(): SceneObject[] { + return Array.from(this.selectedObjects?.values() ?? []).map((obj) => obj.resolve()); + } + + public createSelectionElement() { + if (this.isMultiSelection) { + return this.createMultiSelectedElement(); + } + + return this.createSingleSelectedElement(); + } + + private createSingleSelectedElement(): EditableDashboardElement | undefined { + const sceneObj = this.selectedObjects?.values().next().value?.resolve(); + + if (!sceneObj) { + return undefined; + } + + if (isEditableDashboardElement(sceneObj)) { + return sceneObj; + } + + if (sceneObj instanceof VizPanel) { + return new VizPanelEditableElement(sceneObj); + } + + if (sceneObj instanceof DashboardScene) { + return new DashboardEditableElement(sceneObj); + } + + return undefined; + } + + private createMultiSelectedElement(): MultiSelectedEditableDashboardElement | undefined { + if (!this.isMultiSelection) { + return; + } + + const sceneObjects = this.getSceneObjects(); + + if (this.sameType) { + const firstObj = this.selectedObjects?.values().next().value?.resolve(); + + if (firstObj instanceof VizPanel) { + return new MultiSelectedVizPanelsEditableElement(sceneObjects); + } + + if (isEditableDashboardElement(firstObj!)) { + return firstObj.createMultiSelectedElement?.(sceneObjects); + } + } + + const bulkActionElements = []; + for (const sceneObject of sceneObjects) { + if (sceneObject instanceof VizPanel) { + const editableElement = new VizPanelEditableElement(sceneObject); + bulkActionElements.push(editableElement); + } + + if (isBulkActionElement(sceneObject)) { + bulkActionElements.push(sceneObject); + } + } + + if (bulkActionElements.length) { + return new MultiSelectedObjectsEditableElement(bulkActionElements); + } + + return undefined; + } +} diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx new file mode 100644 index 00000000000..574de4164c1 --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx @@ -0,0 +1,40 @@ +import { ReactNode } from 'react'; + +import { Stack, Text, Button } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; + +import { BulkActionElement, MultiSelectedEditableDashboardElement } from '../scene/types'; + +export class MultiSelectedObjectsEditableElement implements MultiSelectedEditableDashboardElement { + public isMultiSelectedEditableDashboardElement: true = true; + private items?: BulkActionElement[]; + + constructor(items: BulkActionElement[]) { + this.items = items; + } + + public onDelete = () => { + for (const item of this.items || []) { + item.onDelete(); + } + }; + + public getTypeName(): string { + return 'Objects'; + } + + renderActions(): ReactNode { + return ( + + + No. of objects selected: + {this.items?.length} + + +
From bb15f24dcdda2970eccc5e2da376181930046c1c Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Tue, 4 Feb 2025 14:56:17 +0000 Subject: [PATCH 336/894] Alerting: Update design of rule details tab and add `updated by` (#99895) --- .betterer.results | 21 +- .../ClipboardButton/ClipboardButton.tsx | 7 +- .../unified/components/InfoPausedRule.tsx | 7 +- .../unified/components/common/DetailText.tsx | 66 +++++ .../rule-editor/GrafanaEvaluationBehavior.tsx | 10 +- .../rule-viewer/RuleViewer.test.tsx | 14 +- .../components/rule-viewer/tabs/Details.tsx | 267 +++++++++++------- public/app/types/unified-alerting-dto.ts | 5 + public/locales/en-US/grafana.json | 19 ++ public/locales/pseudo-LOCALE/grafana.json | 19 ++ 10 files changed, 295 insertions(+), 140 deletions(-) create mode 100644 public/app/features/alerting/unified/components/common/DetailText.tsx diff --git a/.betterer.results b/.betterer.results index 06a027a4207..53d62e11c3f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1637,10 +1637,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], - "public/app/features/alerting/unified/components/InfoPausedRule.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"] - ], "public/app/features/alerting/unified/components/InvalidIntervalWarning.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"], @@ -2238,9 +2234,7 @@ exports[`better eslint`] = { [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"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "13"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "11"] ], "public/app/features/alerting/unified/components/rule-editor/GrafanaFolderAndLabelsStep.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] @@ -2486,19 +2480,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] ], - "public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "10"] - ], "public/app/features/alerting/unified/components/rule-viewer/tabs/Query.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/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx index 8f56a9a36a0..bfc7b94b793 100644 --- a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx +++ b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans } from '../../../src/utils/i18n'; +import { t } from '../../../src/utils/i18n'; import { useStyles2 } from '../../themes'; import { Button, ButtonProps } from '../Button'; import { Icon } from '../Icon/Icon'; @@ -60,11 +60,12 @@ export function ClipboardButton({ } }, [getText, onClipboardCopy, onClipboardError]); + const copiedText = t('clipboard-button.inline-toast.success', 'Copied'); return ( <> {showCopySuccess && ( - Copied + {copiedText} )} @@ -72,7 +73,7 @@ export function ClipboardButton({ onClick={copyTextCallback} icon={icon} variant={showCopySuccess ? 'success' : variant} - aria-label={showCopySuccess ? 'Copied' : undefined} + aria-label={showCopySuccess ? copiedText : undefined} {...buttonProps} className={cx(styles.button, showCopySuccess && styles.successButton, buttonProps.className)} ref={buttonRef} diff --git a/public/app/features/alerting/unified/components/InfoPausedRule.tsx b/public/app/features/alerting/unified/components/InfoPausedRule.tsx index 52df56f1289..39f435dbccd 100644 --- a/public/app/features/alerting/unified/components/InfoPausedRule.tsx +++ b/public/app/features/alerting/unified/components/InfoPausedRule.tsx @@ -1,9 +1,12 @@ import { Alert } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; const InfoPausedRule = () => { return ( - - Notifications for this rule will not fire and no alert instances will be created until the rule is un-paused. + + + Notifications for this rule will not fire and no alert instances will be created until the rule is un-paused. + ); }; diff --git a/public/app/features/alerting/unified/components/common/DetailText.tsx b/public/app/features/alerting/unified/components/common/DetailText.tsx new file mode 100644 index 00000000000..0e20671985d --- /dev/null +++ b/public/app/features/alerting/unified/components/common/DetailText.tsx @@ -0,0 +1,66 @@ +import { Box, ClipboardButton, Stack, Text, Tooltip } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; + +import ConditionalWrap from '../ConditionalWrap'; + +type DetailTextProps = { + id: string; + label: string; + value: string | JSX.Element | null; + /** Should the value be displayed using monospace font family? */ + monospace?: boolean; + /** Optional string to display in a tooltip on hover of the value */ + tooltipValue?: string; +} & ConditionalProps; + +type ConditionalProps = + // Require either both copy props or neither + | { + /** Should we show a button for copying the value to clipboard? */ + showCopyButton: boolean; + /** + * Value to use for copying to clipboard, when enabled. + * Needed as the value could be an element + */ + copyValue: string; + } + | { showCopyButton?: never; copyValue?: never }; + +export const DetailText = ({ + id, + label, + value, + monospace, + showCopyButton, + copyValue, + tooltipValue, +}: DetailTextProps) => { + const copyToClipboardLabel = t('alerting.copy-to-clipboard', 'Copy "{{label}}" to clipboard', { label }); + return ( + + + + {label} + + + {children}} + > + {value} + + {showCopyButton && ( + copyValue} + /> + )} + + + + ); +}; diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx index 6104c3b2fa6..5178df5a685 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -363,7 +363,10 @@ export function GrafanaEvaluationBehaviorStep({ {showErrorHandling && ( <> - + ( - + ( byRole('listitem', { name: `${key}: ${value}` }), }, details: { - pendingPeriod: byText(/Pending period/i), + pendingPeriod: byLabelText(/Pending period/i), }, actions: { edit: byRole('link', { name: 'Edit' }), @@ -107,6 +107,10 @@ const dataSources = { }; describe('RuleViewer', () => { + beforeEach(() => { + setupDataSources(...Object.values(dataSources)); + }); + describe('Grafana managed alert rule', () => { const mockRule = getGrafanaRule( { @@ -211,10 +215,6 @@ describe('RuleViewer', () => { ]); }); - beforeEach(() => { - setupDataSources(...Object.values(dataSources)); - }); - it('should render a data source managed alert rule', () => { renderRuleViewer(mockRule, mockRuleIdentifier); @@ -291,7 +291,7 @@ describe('RuleViewer', () => { // One summary is rendered by the Title component, and the other by the DetailsTab component expect(ELEMENTS.metadata.summary(mockRule.annotations[Annotation.summary]).getAll()).toHaveLength(2); - expect(within(ELEMENTS.details.pendingPeriod.get()).getByText(/15m/i)).toBeInTheDocument(); + expect(ELEMENTS.details.pendingPeriod.get()).toHaveTextContent(/15m/i); }); }); }); diff --git a/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx index 9d9397f8839..63d3933a4a8 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx @@ -1,19 +1,22 @@ import { css } from '@emotion/css'; import { formatDistanceToNowStrict } from 'date-fns'; -import { useCallback } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; -import { ClipboardButton, Stack, Text, TextLink, useStyles2 } from '@grafana/ui'; +import { GrafanaTheme2, dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data'; +import { Icon, Stack, Text, TextLink, useStyles2 } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; import { CombinedRule } from 'app/types/unified-alerting'; import { usePendingPeriod } from '../../../hooks/rules/usePendingPeriod'; -import { getAnnotations, isGrafanaRecordingRule, isGrafanaRulerRule, isRecordingRulerRule } from '../../../utils/rules'; -import { MetaText } from '../../MetaText'; +import { + getAnnotations, + isGrafanaAlertingRule, + isGrafanaRecordingRule, + isGrafanaRulerRule, + isRecordingRulerRule, +} from '../../../utils/rules'; +import { isNullDate } from '../../../utils/time'; import { Tokenize } from '../../Tokenize'; - -interface DetailsProps { - rule: CombinedRule; -} +import { DetailText } from '../../common/DetailText'; enum RuleType { GrafanaManagedAlertRule = 'Grafana-managed alert rule', @@ -22,7 +25,22 @@ enum RuleType { CloudRecordingRule = 'Cloud recording rule', } -const Details = ({ rule }: DetailsProps) => { +const DetailGroup = ({ title, children }: { title: string; children: React.ReactNode }) => { + return ( + + {title} + + {children} + + + ); +}; + +interface DetailsProps { + rule: CombinedRule; +} + +export const Details = ({ rule }: DetailsProps) => { const styles = useStyles2(getStyles); let ruleType: RuleType; @@ -43,103 +61,136 @@ const Details = ({ rule }: DetailsProps) => { const evaluationDuration = rule.promRule?.evaluationTime; const evaluationTimestamp = rule.promRule?.lastEvaluation; - const copyRuleUID = useCallback(() => { - if (isGrafanaRulerRule(rule.rulerRule)) { - return rule.rulerRule.grafana_alert.uid; - } else { - return ''; - } - }, [rule.rulerRule]); - const annotations = getAnnotations(rule); const hasEvaluationDuration = Number.isFinite(evaluationDuration); - return ( - -
- {/* type and identifier (optional) */} - - Rule type - {ruleType} - - - {isGrafanaRulerRule(rule.rulerRule) && ( - <> - Rule Identifier - - - {rule.rulerRule.grafana_alert.uid} - - - - - )} - + const lastUpdatedBy = (() => { + if (!isGrafanaRulerRule(rule.rulerRule)) { + return null; + } - {/* evaluation duration and pending period */} - - {hasEvaluationDuration && ( - <> - Last evaluation - {evaluationTimestamp && evaluationDuration ? ( - - {formatDistanceToNowStrict(new Date(evaluationTimestamp))} ago, took{' '} - {evaluationDuration}ms - - ) : null} - - )} - - - {pendingPeriod && ( - <> - Pending period - {pendingPeriod} - - )} - + return rule.rulerRule.grafana_alert.updated_by?.name || `User ID: ${rule.rulerRule.grafana_alert.updated_by?.uid}`; + })(); - {/* nodata and execution error state mapping */} - {isGrafanaRulerRule(rule.rulerRule) && - // grafana recording rules don't have these fields - rule.rulerRule.grafana_alert.no_data_state && - rule.rulerRule.grafana_alert.exec_err_state && ( - <> - - Alert state if no data or all values are null - {rule.rulerRule.grafana_alert.no_data_state} - - - Alert state if execution error or timeout - {rule.rulerRule.grafana_alert.exec_err_state} - - - )} -
- - {/* annotations go here */} - {annotations && ( - <> - Annotations - {Object.keys(annotations).length === 0 ? ( - - No annotations - - ) : ( -
- {Object.entries(annotations).map(([name, value]) => ( - - {name} - - - ))} -
- )} - - )} + const updated = isGrafanaRulerRule(rule.rulerRule) ? rule.rulerRule.grafana_alert.updated : undefined; + const isPaused = isGrafanaAlertingRule(rule.rulerRule) && rule.rulerRule.grafana_alert.is_paused; + const pausedIcon = ( + + + + + + Alert evaluation currently paused + ); + return ( +
+ + + {isGrafanaRulerRule(rule.rulerRule) && ( + <> + + + {updated && ( + + )} + + )} + + + + {isPaused ? ( + pausedIcon + ) : ( + <> + {hasEvaluationDuration && evaluationTimestamp && ( + + )} + {hasEvaluationDuration && ( + + )} + + )} + + {pendingPeriod && ( + + )} + + + {isGrafanaRulerRule(rule.rulerRule) && + // grafana recording rules don't have these fields + rule.rulerRule.grafana_alert.no_data_state && + rule.rulerRule.grafana_alert.exec_err_state && ( + + {hasEvaluationDuration && ( + + )} + {pendingPeriod && ( + + )} + + )} + + {annotations && ( + + {Object.keys(annotations).length === 0 ? ( +
+ + No annotations + +
+ ) : ( + Object.entries(annotations).map(([name, value]) => { + const id = `annotation-${name.replace(/\s/g, '-')}`; + return } />; + }) + )} +
+ )} +
+ ); }; interface AnnotationValueProps { @@ -152,7 +203,7 @@ export function AnnotationValue({ value }: AnnotationValueProps) { if (needsExternalLink) { return ( - + {value} ); @@ -162,12 +213,16 @@ export function AnnotationValue({ value }: AnnotationValueProps) { } const getStyles = (theme: GrafanaTheme2) => ({ - metadataWrapper: css({ + metadata: css({ display: 'grid', - gridTemplateColumns: 'auto auto', - rowGap: theme.spacing(3), - columnGap: theme.spacing(12), + gap: theme.spacing(4), + gridTemplateColumns: '1fr 1fr 1fr', + + [theme.breakpoints.down('lg')]: { + gridTemplateColumns: '1fr 1fr', + }, + [theme.breakpoints.down('sm')]: { + gridTemplateColumns: '1fr', + }, }), }); - -export { Details }; diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index 1cc55fe038e..302c6a4d0ba 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -265,6 +265,11 @@ export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition { namespace_uid: string; rule_group: string; provenance?: string; + updated_by?: { + uid: string; + name?: string; + }; + updated?: string; } export interface RulerGrafanaRuleDTO { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f0211561164..f97f3beb611 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -174,6 +174,24 @@ } }, "alerting": { + "alert": { + "alert-state": "Alert state", + "annotations": "Annotations", + "evaluation": "Evaluation", + "evaluation-paused": "Alert evaluation currently paused", + "evaluation-paused-description": "Notifications for this rule will not fire and no alert instances will be created until the rule is un-paused.", + "last-evaluated": "Last evaluated", + "last-evaluation-duration": "Last evaluation duration", + "last-updated-at": "Last updated at", + "last-updated-by": "Last updated by", + "no-annotations": "No annotations", + "pending-period": "Pending period", + "rule": "Rule", + "rule-identifier": "Rule identifier", + "rule-type": "Rule type", + "state-error-timeout": "Alert state if execution error or timeout", + "state-no-data": "Alert state if no data or all values are null" + }, "alert-recording-rule-form": { "evaluation-behaviour": { "description": { @@ -283,6 +301,7 @@ "contactPointFilter": { "label": "Contact point" }, + "copy-to-clipboard": "Copy \"{{label}}\" to clipboard", "export": { "subtitle": { "formats": "Select the format and download the file or copy the contents to clipboard", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 05e75615ed4..76b82980d04 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -174,6 +174,24 @@ } }, "alerting": { + "alert": { + "alert-state": "Åľęřŧ şŧäŧę", + "annotations": "Åʼnʼnőŧäŧįőʼnş", + "evaluation": "Ēväľūäŧįőʼn", + "evaluation-paused": "Åľęřŧ ęväľūäŧįőʼn čūřřęʼnŧľy päūşęđ", + "evaluation-paused-description": "Ńőŧįƒįčäŧįőʼnş ƒőř ŧĥįş řūľę ŵįľľ ʼnőŧ ƒįřę äʼnđ ʼnő äľęřŧ įʼnşŧäʼnčęş ŵįľľ þę čřęäŧęđ ūʼnŧįľ ŧĥę řūľę įş ūʼn-päūşęđ.", + "last-evaluated": "Ŀäşŧ ęväľūäŧęđ", + "last-evaluation-duration": "Ŀäşŧ ęväľūäŧįőʼn đūřäŧįőʼn", + "last-updated-at": "Ŀäşŧ ūpđäŧęđ äŧ", + "last-updated-by": "Ŀäşŧ ūpđäŧęđ þy", + "no-annotations": "Ńő äʼnʼnőŧäŧįőʼnş", + "pending-period": "Pęʼnđįʼnģ pęřįőđ", + "rule": "Ŗūľę", + "rule-identifier": "Ŗūľę įđęʼnŧįƒįęř", + "rule-type": "Ŗūľę ŧypę", + "state-error-timeout": "Åľęřŧ şŧäŧę įƒ ęχęčūŧįőʼn ęřřőř őř ŧįmęőūŧ", + "state-no-data": "Åľęřŧ şŧäŧę įƒ ʼnő đäŧä őř äľľ väľūęş äřę ʼnūľľ" + }, "alert-recording-rule-form": { "evaluation-behaviour": { "description": { @@ -283,6 +301,7 @@ "contactPointFilter": { "label": "Cőʼnŧäčŧ pőįʼnŧ" }, + "copy-to-clipboard": "Cőpy \"{{label}}\" ŧő čľįpþőäřđ", "export": { "subtitle": { "formats": "Ŝęľęčŧ ŧĥę ƒőřmäŧ äʼnđ đőŵʼnľőäđ ŧĥę ƒįľę őř čőpy ŧĥę čőʼnŧęʼnŧş ŧő čľįpþőäřđ", From b16e2904448e12f388f2bea07522b29fc8120d76 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:31:24 +0100 Subject: [PATCH 337/894] Auth: Remove feature toggle `authAPIAccessTokenAuth` (#100055) Remove feature toggle --- packages/grafana-data/src/types/featureToggles.gen.ts | 1 - pkg/services/authn/authnimpl/registration.go | 2 +- 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 ++- 6 files changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 71fa78c0d67..1cf9218c71f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -165,7 +165,6 @@ export interface FeatureToggles { kubernetesAggregator?: boolean; expressionParser?: boolean; groupByVariable?: boolean; - authAPIAccessTokenAuth?: boolean; scopeFilters?: boolean; ssoSettingsSAML?: boolean; oauthRequireSubClaim?: boolean; diff --git a/pkg/services/authn/authnimpl/registration.go b/pkg/services/authn/authnimpl/registration.go index 04e074040aa..593a0d823fc 100644 --- a/pkg/services/authn/authnimpl/registration.go +++ b/pkg/services/authn/authnimpl/registration.go @@ -115,7 +115,7 @@ func ProvideRegistration( authnSvc.RegisterClient(clients.ProvideJWT(jwtService, cfg)) } - if cfg.ExtJWTAuth.Enabled && features.IsEnabledGlobally(featuremgmt.FlagAuthAPIAccessTokenAuth) { + if cfg.ExtJWTAuth.Enabled { authnSvc.RegisterClient(clients.ProvideExtendedJWT(cfg)) } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3e737bb1f45..e1ff68d42bc 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1109,14 +1109,6 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, - { - Name: "authAPIAccessTokenAuth", - Description: "Enables the use of Auth API access tokens for authentication", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, - }, { Name: "scopeFilters", Description: "Enables the use of scope filters in Grafana", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 3b653f76b4e..9eab6e5ed1f 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -146,7 +146,6 @@ tlsMemcached,GA,@grafana/grafana-operator-experience-squad,false,false,false kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false expressionParser,experimental,@grafana/grafana-app-platform-squad,false,true,false groupByVariable,experimental,@grafana/dashboards-squad,false,false,false -authAPIAccessTokenAuth,experimental,@grafana/identity-access-team,false,false,false scopeFilters,experimental,@grafana/dashboards-squad,false,false,false ssoSettingsSAML,preview,@grafana/identity-access-team,false,false,false oauthRequireSubClaim,experimental,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 3a5db419907..4e293dc9a47 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -595,10 +595,6 @@ const ( // Enable groupBy variable support in scenes dashboards FlagGroupByVariable = "groupByVariable" - // FlagAuthAPIAccessTokenAuth - // Enables the use of Auth API access tokens for authentication - FlagAuthAPIAccessTokenAuth = "authAPIAccessTokenAuth" - // FlagScopeFilters // Enables the use of scope filters in Grafana FlagScopeFilters = "scopeFilters" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index e39d9ef150e..ba79b481cba 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -547,7 +547,8 @@ "metadata": { "name": "authAPIAccessTokenAuth", "resourceVersion": "1718727528075", - "creationTimestamp": "2024-04-02T15:45:15Z" + "creationTimestamp": "2024-04-02T15:45:15Z", + "deletionTimestamp": "2025-02-04T14:58:33Z" }, "spec": { "description": "Enables the use of Auth API access tokens for authentication", From e74cf72d9902f8451f4f8e78fe36cf82cce71d53 Mon Sep 17 00:00:00 2001 From: Christopher Lord Date: Tue, 4 Feb 2025 09:54:27 -0700 Subject: [PATCH 338/894] Plugin Metrics: Eliminate data race in plugin metrics middleware (#99396) fix: eliminate data race in plugin metrics middleware A data race was detected when multiple goroutines accessed the `MetricsMiddleware` simultaneously. The race occurred because a single `MetricsMiddleware` instance was being shared across goroutines while its `BaseHandler` field was being modified during middleware chain setup. Fix by creating a new `MetricsMiddleware` instance for each middleware chain, while safely sharing the thread-safe Prometheus metrics and plugin registry. This maintains proper metrics collection while eliminating the mutable shared state that caused the race condition. Original error was detected here: ``` WARNING: DATA RACE Read at 0x00c0039c0790 by goroutine 4486: github.com/grafana/grafana-plugin-sdk-go/backend.(*ErrorSourceMiddleware).CallResource() /Users/clord/src/grafana/irm-devstack/.devenv/state/go/pkg/mod/github.com/grafana/grafana-plugin-sdk-go@v0.261.0/backend/error_source_middleware.go:93 +0x40 github.com/grafana/grafana-plugin-sdk-go/backend.BaseHandler.CallResource() ... ``` --- .../clientmiddleware/metrics_middleware.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go index a75f7a43889..d38ce28724f 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go @@ -75,10 +75,13 @@ func newMetricsMiddleware(promRegisterer prometheus.Registerer, pluginRegistry r // NewMetricsMiddleware returns a new MetricsMiddleware. func NewMetricsMiddleware(promRegisterer prometheus.Registerer, pluginRegistry registry.Service) backend.HandlerMiddleware { - imw := newMetricsMiddleware(promRegisterer, pluginRegistry) + metrics := newMetricsMiddleware(promRegisterer, pluginRegistry) return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { - imw.BaseHandler = backend.NewBaseHandler(next) - return imw + return &MetricsMiddleware{ + BaseHandler: backend.NewBaseHandler(next), + pluginMetrics: metrics.pluginMetrics, + pluginRegistry: metrics.pluginRegistry, + } }) } From 3fd1b67381673cd50fc4105644ce39cb07a6c60a Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Tue, 4 Feb 2025 12:11:08 -0500 Subject: [PATCH 339/894] Dashboards: Apply schemaVersion migration in v2 conversion (#99973) --- pkg/apis/dashboard/v1alpha1/conversion.go | 10 ++++----- .../dashboard/v1alpha1/conversion_test.go | 4 +++- pkg/apis/dashboard/v2alpha1/conversion.go | 21 ++++++++++++------- .../dashboard/v2alpha1/conversion_test.go | 4 +++- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/pkg/apis/dashboard/v1alpha1/conversion.go b/pkg/apis/dashboard/v1alpha1/conversion.go index 8e21a1c62c5..27bb5313336 100644 --- a/pkg/apis/dashboard/v1alpha1/conversion.go +++ b/pkg/apis/dashboard/v1alpha1/conversion.go @@ -12,25 +12,23 @@ import ( ) func Convert_v0alpha1_Unstructured_To_v1alpha1_DashboardSpec(in *common.Unstructured, out *DashboardSpec, s conversion.Scope) error { - err := migration.Migrate(in.Object, schemaversion.LATEST_VERSION) + out.Unstructured = *in + err := migration.Migrate(out.Unstructured.Object, schemaversion.LATEST_VERSION) if err != nil { minErr := &schemaversion.MinimumVersionError{} if errors.As(err, &minErr) { - in.Object["__migrationError"] = err.Error() + out.Unstructured.Object["__migrationError"] = err.Error() } else { return err } } - out.Unstructured = *in - - t, ok := in.Object["title"].(string) + t, ok := out.Unstructured.Object["title"].(string) if !ok { klog.V(5).Infof("unstructured dashboard title field is not a string %v", t) return nil // skip setting the title if it's not a string in the unstructured object } out.Title = t - return nil } diff --git a/pkg/apis/dashboard/v1alpha1/conversion_test.go b/pkg/apis/dashboard/v1alpha1/conversion_test.go index dfca8a320ea..db91af59698 100644 --- a/pkg/apis/dashboard/v1alpha1/conversion_test.go +++ b/pkg/apis/dashboard/v1alpha1/conversion_test.go @@ -27,6 +27,7 @@ func TestConvertDashboardVersions(t *testing.T) { } ] }, + "refresh": true, "description": "", "editable": true, "fiscalYearStartMonth": 0, @@ -35,7 +36,7 @@ func TestConvertDashboardVersions(t *testing.T) { "links": [], "panels": [], "preload": false, - "schemaVersion": 40, + "schemaVersion": 39, "tags": [], "templating": { "list": [] @@ -56,6 +57,7 @@ func TestConvertDashboardVersions(t *testing.T) { require.NoError(t, err) require.Equal(t, result.Title, "New dashboard") require.Equal(t, result.Unstructured, object) + require.Equal(t, result.Unstructured.Object["refresh"], "", "schemaVersion migration not applied. refresh should be an empty string") // now convert back & ensure it is the same object2 := common.Unstructured{} diff --git a/pkg/apis/dashboard/v2alpha1/conversion.go b/pkg/apis/dashboard/v2alpha1/conversion.go index 22db120c0e5..7dcb7487b59 100644 --- a/pkg/apis/dashboard/v2alpha1/conversion.go +++ b/pkg/apis/dashboard/v2alpha1/conversion.go @@ -1,27 +1,34 @@ package v2alpha1 import ( + "errors" + conversion "k8s.io/apimachinery/pkg/conversion" klog "k8s.io/klog/v2" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/apis/dashboard/migration" + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" ) func Convert_v0alpha1_Unstructured_To_v2alpha1_DashboardSpec(in *common.Unstructured, out *DashboardSpec, s conversion.Scope) error { out.Unstructured = *in - - t, ok := in.Object["title"] - if !ok { - return nil // skip setting the title if it's not in the unstructured object + err := migration.Migrate(out.Unstructured.Object, schemaversion.LATEST_VERSION) + if err != nil { + minErr := &schemaversion.MinimumVersionError{} + if errors.As(err, &minErr) { + out.Unstructured.Object["__migrationError"] = err.Error() + } else { + return err + } } - title, ok := t.(string) + t, ok := out.Unstructured.Object["title"].(string) if !ok { klog.V(5).Infof("unstructured dashboard title field is not a string %v", t) return nil // skip setting the title if it's not a string in the unstructured object } - out.Title = title - + out.Title = t return nil } diff --git a/pkg/apis/dashboard/v2alpha1/conversion_test.go b/pkg/apis/dashboard/v2alpha1/conversion_test.go index 143fdcffcef..fdd337c822b 100644 --- a/pkg/apis/dashboard/v2alpha1/conversion_test.go +++ b/pkg/apis/dashboard/v2alpha1/conversion_test.go @@ -27,6 +27,7 @@ func TestConvertDashboardVersions(t *testing.T) { } ] }, + "refresh": true, "description": "", "editable": true, "fiscalYearStartMonth": 0, @@ -35,7 +36,7 @@ func TestConvertDashboardVersions(t *testing.T) { "links": [], "panels": [], "preload": false, - "schemaVersion": 40, + "schemaVersion": 39, "tags": [], "templating": { "list": [] @@ -56,6 +57,7 @@ func TestConvertDashboardVersions(t *testing.T) { require.NoError(t, err) require.Equal(t, result.Title, "New dashboard") require.Equal(t, result.Unstructured, object) + require.Equal(t, result.Unstructured.Object["refresh"], "", "schemaVersion migration not applied. refresh should be an empty string") // now convert back & ensure it is the same object2 := common.Unstructured{} From 87bb7c394744839570f5d0d0695d6edef2617114 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 4 Feb 2025 18:32:08 +0100 Subject: [PATCH 340/894] Explore: Fix casing for `exploreHideLogsDownload` setting (#100081) --- pkg/api/dtos/frontend_settings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 30a80ea8b71..a9581366b13 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -212,7 +212,7 @@ type FrontendSettingsDTO struct { CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled"` EnableFrontendSandboxForPlugins []string `json:"enableFrontendSandboxForPlugins"` ExploreDefaultTimeOffset string `json:"exploreDefaultTimeOffset"` - ExploreHideLogsDownload bool `json:"ExploreHideLogsDownload"` + ExploreHideLogsDownload bool `json:"exploreHideLogsDownload"` Auth FrontendSettingsAuthDTO `json:"auth"` From ff926c5ac5daa760378ce233d7d9f876f56727b5 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 4 Feb 2025 17:40:17 +0000 Subject: [PATCH 341/894] Logs Panel: Base elements for the new visualization (#99084) * Create base components * Create measurement service * Add container for list * Use measurement to render virtualized log lines * Match rendered styles in 2d context for measuring * Improve virtualization initialization and handle resize * Introduce log line processing * Virtualization: fix measurement of lines with line endings * Virtualization: include scrollbar width in calculation * Remove logs * Virtualization: optimize text measurement * Add support for forceEscape * Log line: properly style wrapped/unwrapped lines * Virtualization: handle possible overflows * Improve overflow handling * LogList: remove scroll position ref * Remove logs * Remove log * Add top/bottom navigation buttons * Add timestamp to pre-processing * Add showtime support * Fix imports * Chore: simplify dedup * Show level * Refactor measurement and measure level and timestamp * Virtualization: skip unnecessary measurements * Improve measurements to minimize overflow chance * Introduce logline colors * Update palette * Remove pretiffying * Add comment * Remove unused variable * Add color for info level * Fix dependencies * Refactor overflow to account for smaller estimations * Debounce resizing * Fix imports * Further optimize height calculation * Remove outline * Unused import * Use less under/overflow method * Respond to height changes * Refactor size adjustment to account for layout changes * Add Logs Panel support * Add margin bottom to log lines * Remove unused option * LogList: container div should never be null Bad API design * Log List: make app not undefined and update containerElement usages * New Logs Panel: Create as new visualization (#99427) * Logs Panel: clean up old panel * Logs Panel New: create as new visualization * Plugin: mark as alpha * Logs panel new: hold container in a state variable * Logs panel: fix no data state * Create newLogsPanel feature flag * Logs: use new feature flag * Prettier * Add new panel to code owners * Logs Navigation: add translations * Address betterer issues * Fix import * Extract translations * Update virtualization.ts * Virtualization: add DOM fallback for text measurement * Run gen-cue * plugins_integration_test: add logs-new to expected plugins --- .betterer.results | 4 - .github/CODEOWNERS | 1 + .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + .../panelcfg/x/LogsNewPanelCfg_types.gen.ts | 22 ++ pkg/registry/schemas/composable_kind.go | 10 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 13 + .../plugins_integration_test.go | 1 + public/app/features/explore/Logs/Logs.tsx | 64 ++++- .../features/explore/Logs/LogsNavigation.tsx | 37 ++- .../logs/components/panel/LogLine.tsx | 116 +++++++++ .../logs/components/panel/LogList.tsx | 135 +++++++++++ .../logs/components/panel/processing.ts | 66 +++++ .../logs/components/panel/virtualization.ts | 229 ++++++++++++++++++ public/app/features/logs/logsModel.ts | 4 +- .../app/features/plugins/built_in_plugins.ts | 3 + .../app/plugins/panel/logs-new/LogsPanel.tsx | 91 +++++++ .../panel/logs-new/img/icn-logs-panel.svg | 1 + public/app/plugins/panel/logs-new/module.tsx | 73 ++++++ .../app/plugins/panel/logs-new/panelcfg.cue | 40 +++ .../plugins/panel/logs-new/panelcfg.gen.ts | 20 ++ public/app/plugins/panel/logs-new/plugin.json | 17 ++ .../app/plugins/panel/logs-new/suggestions.ts | 33 +++ public/locales/en-US/grafana.json | 7 + public/locales/pseudo-LOCALE/grafana.json | 7 + 28 files changed, 995 insertions(+), 13 deletions(-) create mode 100644 packages/grafana-schema/src/raw/composable/logs(new)/panelcfg/x/LogsNewPanelCfg_types.gen.ts create mode 100644 public/app/features/logs/components/panel/LogLine.tsx create mode 100644 public/app/features/logs/components/panel/LogList.tsx create mode 100644 public/app/features/logs/components/panel/processing.ts create mode 100644 public/app/features/logs/components/panel/virtualization.ts create mode 100644 public/app/plugins/panel/logs-new/LogsPanel.tsx create mode 100644 public/app/plugins/panel/logs-new/img/icn-logs-panel.svg create mode 100644 public/app/plugins/panel/logs-new/module.tsx create mode 100644 public/app/plugins/panel/logs-new/panelcfg.cue create mode 100644 public/app/plugins/panel/logs-new/panelcfg.gen.ts create mode 100644 public/app/plugins/panel/logs-new/plugin.json create mode 100644 public/app/plugins/panel/logs-new/suggestions.ts diff --git a/.betterer.results b/.betterer.results index 53d62e11c3f..c9f78d230b0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4668,10 +4668,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] ], - "public/app/features/explore/Logs/LogsNavigation.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"] - ], "public/app/features/explore/Logs/LogsSamplePanel.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/.github/CODEOWNERS b/.github/CODEOWNERS index d8d54103584..645625d3d19 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -535,6 +535,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/plugins/panel/heatmap/ @grafana/dataviz-squad /public/app/plugins/panel/histogram/ @grafana/dataviz-squad /public/app/plugins/panel/logs/ @grafana/observability-logs +/public/app/plugins/panel/logs-new/ @grafana/observability-logs /public/app/plugins/panel/nodeGraph/ @grafana/observability-traces-and-profiling @grafana/app-o11y-visualizations /public/app/plugins/panel/traces/ @grafana/observability-traces-and-profiling /public/app/plugins/panel/flamegraph/ @grafana/observability-traces-and-profiling 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 030c81df2ed..9cd7fe7e1a7 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -232,6 +232,7 @@ Experimental features might be changed or removed without prior notice. | `grafanaAdvisor` | Enables Advisor app | | `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing | | `datasourceConnectionsTab` | Shows defined connections for a data source in the plugins detail page | +| `newLogsPanel` | Enables the new logs panel in Explore | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 1cf9218c71f..452659bcf74 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -255,4 +255,5 @@ export interface FeatureToggles { fetchRulesUsingPost?: boolean; alertingAlertmanagerExtraDedupStage?: boolean; alertingAlertmanagerExtraDedupStageStopPipeline?: boolean; + newLogsPanel?: boolean; } diff --git a/packages/grafana-schema/src/raw/composable/logs(new)/panelcfg/x/LogsNewPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logs(new)/panelcfg/x/LogsNewPanelCfg_types.gen.ts new file mode 100644 index 00000000000..91589b9cea1 --- /dev/null +++ b/packages/grafana-schema/src/raw/composable/logs(new)/panelcfg/x/LogsNewPanelCfg_types.gen.ts @@ -0,0 +1,22 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// public/app/plugins/gen.go +// Using jennies: +// TSTypesJenny +// PluginTsTypesJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +import * as common from '@grafana/schema'; + +export const pluginVersion = "11.6.0-pre"; + +export interface Options { + dedupStrategy: common.LogsDedupStrategy; + enableInfiniteScrolling?: boolean; + enableLogDetails: boolean; + showTime: boolean; + sortOrder: common.LogsSortOrder; + wrapLogMessage: boolean; +} diff --git a/pkg/registry/schemas/composable_kind.go b/pkg/registry/schemas/composable_kind.go index e17a71e15a9..8d6126e8d08 100644 --- a/pkg/registry/schemas/composable_kind.go +++ b/pkg/registry/schemas/composable_kind.go @@ -248,6 +248,16 @@ func GetComposableKinds() ([]ComposableKind, error) { CueFile: logsCue, }) + logsnewCue, err := loadCueFileWithCommon(root, filepath.Join(root, "./public/app/plugins/panel/logs-new/panelcfg.cue")) + if err != nil { + return nil, err + } + kinds = append(kinds, ComposableKind{ + Name: "logsnew", + Filename: "panelcfg.cue", + CueFile: logsnewCue, + }) + newsCue, err := loadCueFileWithCommon(root, filepath.Join(root, "./public/app/plugins/panel/news/panelcfg.cue")) if err != nil { return nil, err diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index e1ff68d42bc..f0def056b77 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1777,6 +1777,13 @@ var ( HideFromDocs: true, RequiresRestart: true, }, + { + Name: "newLogsPanel", + Description: "Enables the new logs panel in Explore", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 9eab6e5ed1f..ad6203ebdc7 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -236,3 +236,4 @@ datasourceConnectionsTab,experimental,@grafana/plugins-platform-backend,false,fa fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false alertingAlertmanagerExtraDedupStage,experimental,@grafana/alerting-squad,false,true,false alertingAlertmanagerExtraDedupStageStopPipeline,experimental,@grafana/alerting-squad,false,true,false +newLogsPanel,experimental,@grafana/observability-logs,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 4e293dc9a47..8c36acbcc51 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -954,4 +954,8 @@ const ( // FlagAlertingAlertmanagerExtraDedupStageStopPipeline // works together with alertingAlertmanagerExtraDedupStage, if enabled, it will stop the pipeline if the timestamps are not matching. Otherwise, it will emit a warning FlagAlertingAlertmanagerExtraDedupStageStopPipeline = "alertingAlertmanagerExtraDedupStageStopPipeline" + + // FlagNewLogsPanel + // Enables the new logs panel in Explore + FlagNewLogsPanel = "newLogsPanel" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index ba79b481cba..e1673e7c8ab 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2707,6 +2707,19 @@ "frontend": true } }, + { + "metadata": { + "name": "newLogsPanel", + "resourceVersion": "1738344859933", + "creationTimestamp": "2025-01-31T17:34:19Z" + }, + "spec": { + "description": "Enables the new logs panel in Explore", + "stage": "experimental", + "codeowner": "@grafana/observability-logs", + "frontend": true + } + }, { "metadata": { "name": "newPDFRendering", diff --git a/pkg/services/pluginsintegration/plugins_integration_test.go b/pkg/services/pluginsintegration/plugins_integration_test.go index 7d5bf5f8978..ecf452229ed 100644 --- a/pkg/services/pluginsintegration/plugins_integration_test.go +++ b/pkg/services/pluginsintegration/plugins_integration_test.go @@ -150,6 +150,7 @@ func verifyCorePluginCatalogue(t *testing.T, ctx context.Context, ps *pluginstor "histogram": {}, "live": {}, "logs": {}, + "logs-new": {}, "candlestick": {}, "news": {}, "nodeGraph": {}, diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 2ed3584119d..fb05d6bee91 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -55,6 +55,8 @@ import { createAndCopyShortLink, getLogsPermalinkRange } from 'app/core/utils/sh import { InfiniteScroll } from 'app/features/logs/components/InfiniteScroll'; import { LogRows } from 'app/features/logs/components/LogRows'; import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal'; +import { LogList } from 'app/features/logs/components/panel/LogList'; +import { ScrollToLogsEvent } from 'app/features/logs/components/panel/virtualization'; import { LogLevelColor, dedupLogRows, filterLogLevels } from 'app/features/logs/logsModel'; import { getLogLevel, getLogLevelFromKey, getLogLevelInfo } from 'app/features/logs/utils'; import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen'; @@ -709,7 +711,13 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { ); const scrollToTopLogs = useCallback(() => { - if (config.featureToggles.logsInfiniteScrolling) { + if (config.featureToggles.newLogsPanel) { + eventBus.publish( + new ScrollToLogsEvent({ + scrollTo: 'top', + }) + ); + } else if (config.featureToggles.logsInfiniteScrolling) { if (logsContainerRef.current) { logsContainerRef.current.scroll({ behavior: 'auto', @@ -718,7 +726,25 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { } } topLogsRef.current?.scrollIntoView(); - }, [logsContainerRef, topLogsRef]); + }, [eventBus]); + + const scrollToBottomLogs = useCallback(() => { + if (config.featureToggles.newLogsPanel) { + eventBus.publish( + new ScrollToLogsEvent({ + scrollTo: 'bottom', + }) + ); + } else if (config.featureToggles.logsInfiniteScrolling) { + if (logsContainerRef.current) { + logsContainerRef.current.scroll({ + behavior: 'auto', + top: logsContainerRef.current.scrollHeight, + }); + } + } + topLogsRef.current?.scrollTo(0, topLogsRef.current.scrollHeight); + }, [eventBus]); const onPinToContentOutlineClick = useCallback( (row: LogRowModel, allowUnPin = true) => { @@ -968,7 +994,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { />
)} - {visualisationType === 'logs' && hasData && ( + {visualisationType === 'logs' && hasData && !config.featureToggles.newLogsPanel && ( <>
= (props: Props) => { /> )} + {visualisationType === 'logs' && config.featureToggles.newLogsPanel && ( + <> +
+ {logsContainerRef.current && ( + + )} +
+ + + )} {!loading && !hasData && !scanning && (
diff --git a/public/app/features/explore/Logs/LogsNavigation.tsx b/public/app/features/explore/Logs/LogsNavigation.tsx index 945c679bcd0..5c312fa7b37 100644 --- a/public/app/features/explore/Logs/LogsNavigation.tsx +++ b/public/app/features/explore/Logs/LogsNavigation.tsx @@ -7,6 +7,7 @@ import { config, reportInteraction } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { Button, Icon, Spinner, useTheme2 } from '@grafana/ui'; import { TOP_BAR_LEVEL_HEIGHT } from 'app/core/components/AppChrome/types'; +import { t, Trans } from 'app/core/internationalization'; import { LogsNavigationPages } from './LogsNavigationPages'; @@ -19,6 +20,7 @@ type Props = { logsSortOrder?: LogsSortOrder | null; onChangeTime: (range: AbsoluteTimeRange) => void; scrollToTopLogs: () => void; + scrollToBottomLogs?: () => void; addResultsToCache: () => void; clearCache: () => void; }; @@ -35,6 +37,7 @@ function LogsNavigation({ loading, onChangeTime, scrollToTopLogs, + scrollToBottomLogs, visibleRange, queries, clearCache, @@ -126,7 +129,7 @@ function LogsNavigation({ >
{loading ? : } - Older logs + Older logs
); @@ -156,7 +159,9 @@ function LogsNavigation({
{loading && } {onFirstPage || loading ? null : } - {onFirstPage ? 'Start of range' : 'Newer logs'} + {onFirstPage + ? t('logs.logs-navigation.start-of-range', 'Start of range') + : t('logs.logs-navigation.newer-logs', 'Newer logs')}
); @@ -178,6 +183,11 @@ function LogsNavigation({ scrollToTopLogs(); }, [scrollToTopLogs]); + const onScrollToBottomClick = useCallback(() => { + reportInteraction('grafana_explore_logs_scroll_bottom_clicked'); + scrollToBottomLogs?.(); + }, [scrollToBottomLogs]); + return (
{!config.featureToggles.logsInfiniteScrolling && ( @@ -194,12 +204,23 @@ function LogsNavigation({ {oldestLogsFirst ? newerLogsButton : olderLogsButton} )} + {scrollToBottomLogs && ( + + )} @@ -244,6 +265,16 @@ const getStyles = (theme: GrafanaTheme2, oldestLogsFirst: boolean) => { height: '100%', whiteSpace: 'normal', }), + scrollToBottomButton: css({ + width: '40px', + height: '40px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + position: 'absolute', + top: 0, + }), scrollToTopButton: css({ width: '40px', height: '40px', diff --git a/public/app/features/logs/components/panel/LogLine.tsx b/public/app/features/logs/components/panel/LogLine.tsx new file mode 100644 index 00000000000..02b2b464dff --- /dev/null +++ b/public/app/features/logs/components/panel/LogLine.tsx @@ -0,0 +1,116 @@ +import { css } from '@emotion/css'; +import { CSSProperties, useEffect, useRef } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { useTheme2 } from '@grafana/ui'; + +import { ProcessedLogModel } from './processing'; +import { hasUnderOrOverflow } from './virtualization'; + +interface Props { + index: number; + log: ProcessedLogModel; + showTime: boolean; + style: CSSProperties; + onOverflow?: (index: number, id: string, height: number) => void; + wrapLogMessage: boolean; +} + +export const LogLine = ({ index, log, style, onOverflow, showTime, wrapLogMessage }: Props) => { + const theme = useTheme2(); + const styles = getStyles(theme); + const logLineRef = useRef(null); + + useEffect(() => { + if (!onOverflow || !logLineRef.current) { + return; + } + const calculatedHeight = typeof style.height === 'number' ? style.height : undefined; + const actualHeight = hasUnderOrOverflow(logLineRef.current, calculatedHeight); + if (actualHeight) { + onOverflow(index, log.uid, actualHeight); + } + }, [index, log.uid, onOverflow, style.height]); + + return ( +
+
+ {showTime && {log.timestamp}} + {log.logLevel && {log.logLevel}} + {log.body} +
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + const colors = { + critical: '#B877D9', + error: '#FF5286', + warning: '#FBAD37', + debug: '#6CCF8E', + trace: '#6ed0e0', + info: '#6E9FFF', + }; + + return { + logLine: css({ + color: theme.colors.text.primary, + fontFamily: theme.typography.fontFamilyMonospace, + fontSize: theme.typography.fontSize, + wordBreak: 'break-all', + '&:hover': { + opacity: 0.9, + }, + }), + timestamp: css({ + color: theme.colors.text.secondary, + display: 'inline-block', + marginRight: theme.spacing(1), + '&.level-critical': { + color: colors.critical, + }, + '&.level-error': { + color: colors.error, + }, + '&.level-warning': { + color: colors.warning, + }, + '&.level-debug': { + color: colors.debug, + }, + }), + level: css({ + color: theme.colors.text.secondary, + fontWeight: theme.typography.fontWeightBold, + display: 'inline-block', + marginRight: theme.spacing(1), + '&.level-critical': { + color: colors.critical, + }, + '&.level-error': { + color: colors.error, + }, + '&.level-warning': { + color: colors.warning, + }, + '&.level-info': { + color: colors.info, + }, + '&.level-debug': { + color: colors.debug, + }, + }), + overflows: css({ + outline: 'solid 1px red', + }), + unwrappedLogLine: css({ + whiteSpace: 'pre', + paddingBottom: theme.spacing(0.5), + }), + wrappedLogLine: css({ + whiteSpace: 'pre-wrap', + paddingBottom: theme.spacing(0.5), + }), + }; +}; diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx new file mode 100644 index 00000000000..8c08e96bfef --- /dev/null +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -0,0 +1,135 @@ +import { debounce } from 'lodash'; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { ListChildComponentProps, VariableSizeList } from 'react-window'; + +import { CoreApp, EventBus, LogRowModel, LogsSortOrder } from '@grafana/data'; +import { useTheme2 } from '@grafana/ui'; + +import { LogLine } from './LogLine'; +import { preProcessLogs, ProcessedLogModel } from './processing'; +import { + getLogLineSize, + init as initVirtualization, + resetLogLineSizes, + ScrollToLogsEvent, + storeLogLineSize, +} from './virtualization'; + +interface Props { + app: CoreApp; + logs: LogRowModel[]; + containerElement: HTMLDivElement; + eventBus: EventBus; + forceEscape?: boolean; + showTime: boolean; + sortOrder: LogsSortOrder; + timeZone: string; + wrapLogMessage: boolean; +} + +export const LogList = ({ + app, + containerElement, + logs, + eventBus, + forceEscape = false, + showTime, + sortOrder, + timeZone, + wrapLogMessage, +}: Props) => { + const [processedLogs, setProcessedLogs] = useState([]); + const [listHeight, setListHeight] = useState( + app === CoreApp.Explore ? window.innerHeight * 0.75 : containerElement.clientHeight + ); + const theme = useTheme2(); + const listRef = useRef(null); + const widthRef = useRef(containerElement.clientWidth); + + useEffect(() => { + initVirtualization(theme); + }, [theme]); + + useEffect(() => { + const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) => { + if (e.payload.scrollTo === 'top') { + listRef.current?.scrollTo(0); + } else { + listRef.current?.scrollToItem(processedLogs.length - 1); + } + }); + return () => subscription.unsubscribe(); + }, [eventBus, processedLogs.length]); + + useEffect(() => { + setProcessedLogs(preProcessLogs(logs, { wrap: wrapLogMessage, escape: forceEscape, order: sortOrder, timeZone })); + listRef.current?.resetAfterIndex(0); + listRef.current?.scrollTo(0); + }, [forceEscape, logs, sortOrder, timeZone, wrapLogMessage]); + + useEffect(() => { + const handleResize = debounce(() => { + setListHeight(app === CoreApp.Explore ? window.innerHeight * 0.75 : containerElement.clientHeight); + }, 50); + window.addEventListener('resize', handleResize); + handleResize(); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [app, containerElement.clientHeight]); + + useLayoutEffect(() => { + if (widthRef.current === containerElement.clientWidth) { + return; + } + resetLogLineSizes(); + listRef.current?.resetAfterIndex(0); + widthRef.current = containerElement.clientWidth; + }); + + const handleOverflow = useCallback( + (index: number, id: string, height: number) => { + if (containerElement) { + storeLogLineSize(id, containerElement, height); + listRef.current?.resetAfterIndex(index); + } + }, + [containerElement] + ); + + const Renderer = useCallback( + ({ index, style }: ListChildComponentProps) => { + return ( + + ); + }, + [handleOverflow, processedLogs, showTime, wrapLogMessage] + ); + + if (!containerElement || listHeight == null) { + // Wait for container to be rendered + return null; + } + + return ( + processedLogs[index].uid} + layout="vertical" + ref={listRef} + style={{ overflowY: 'scroll' }} + width="100%" + > + {Renderer} + + ); +}; diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts new file mode 100644 index 00000000000..68986505ccc --- /dev/null +++ b/public/app/features/logs/components/panel/processing.ts @@ -0,0 +1,66 @@ +import { dateTimeFormat, LogRowModel, LogsSortOrder } from '@grafana/data'; + +import { escapeUnescapedString, sortLogRows } from '../../utils'; + +import { measureTextWidth } from './virtualization'; + +export interface ProcessedLogModel extends LogRowModel { + body: string; + timestamp: string; + dimensions: LogDimensions; +} + +export interface LogDimensions { + timestampWidth: number; + levelWidth: number; +} + +interface PreProcessOptions { + escape: boolean; + order: LogsSortOrder; + timeZone: string; + wrap: boolean; +} + +export const preProcessLogs = ( + logs: LogRowModel[], + { escape, order, timeZone, wrap }: PreProcessOptions +): ProcessedLogModel[] => { + const orderedLogs = sortLogRows(logs, order); + return orderedLogs.map((log) => preProcessLog(log, { wrap, escape, timeZone, expanded: false })); +}; + +interface PreProcessLogOptions { + escape: boolean; + expanded: boolean; // Not yet implemented + timeZone: string; + wrap: boolean; +} +const preProcessLog = ( + log: LogRowModel, + { escape, expanded, timeZone, wrap }: PreProcessLogOptions +): ProcessedLogModel => { + let body = log.entry; + const timestamp = dateTimeFormat(log.timeEpochMs, { + timeZone, + defaultWithMS: true, + }); + + if (escape && log.hasUnescapedContent) { + body = escapeUnescapedString(body); + } + // With wrapping disabled, we want to turn it into a single-line log entry unless the line is expanded + if (!wrap && !expanded) { + body = body.replace(/(\r\n|\n|\r)/g, ''); + } + + return { + ...log, + body, + timestamp, + dimensions: { + timestampWidth: measureTextWidth(timestamp), + levelWidth: measureTextWidth(log.logLevel), + }, + }; +}; diff --git a/public/app/features/logs/components/panel/virtualization.ts b/public/app/features/logs/components/panel/virtualization.ts new file mode 100644 index 00000000000..6c9bfbfbb30 --- /dev/null +++ b/public/app/features/logs/components/panel/virtualization.ts @@ -0,0 +1,229 @@ +import { BusEventWithPayload, GrafanaTheme2 } from '@grafana/data'; + +import { ProcessedLogModel } from './processing'; + +let ctx: CanvasRenderingContext2D | null = null; +let gridSize = 8; +let paddingBottom = gridSize * 0.5; +let lineHeight = 22; +let measurementMode: 'canvas' | 'dom' = 'canvas'; + +export function init(theme: GrafanaTheme2) { + const font = `${theme.typography.fontSize}px ${theme.typography.fontFamilyMonospace}`; + const letterSpacing = theme.typography.body.letterSpacing; + + initDOMmeasurement(font, letterSpacing); + initCanvasMeasurement(font, letterSpacing); + + gridSize = theme.spacing.gridSize; + paddingBottom = gridSize * 0.5; + lineHeight = theme.typography.fontSize * theme.typography.body.lineHeight; + + widthMap = new Map(); + resetLogLineSizes(); + + determineMeasurementMode(); + + return true; +} + +function determineMeasurementMode() { + if (!ctx) { + measurementMode = 'dom'; + return; + } + const canvasCharWidth = ctx.measureText('e').width; + const domCharWidth = measureTextWidthWithDOM('e'); + const diff = domCharWidth - canvasCharWidth; + if (diff >= 0.1) { + console.warn('Virtualized log list: falling back to DOM for measurement'); + measurementMode = 'dom'; + } +} + +function initCanvasMeasurement(font: string, letterSpacing: string | undefined) { + const canvas = document.createElement('canvas'); + ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + ctx.font = font; + ctx.fontKerning = 'normal'; + ctx.fontStretch = 'normal'; + ctx.fontVariantCaps = 'normal'; + ctx.textRendering = 'optimizeLegibility'; + if (letterSpacing) { + ctx.letterSpacing = letterSpacing; + } +} + +const span = document.createElement('span'); +function initDOMmeasurement(font: string, letterSpacing: string | undefined) { + span.style.font = font; + span.style.visibility = 'hidden'; + span.style.position = 'absolute'; + span.style.wordBreak = 'break-all'; + if (letterSpacing) { + span.style.letterSpacing = letterSpacing; + } +} + +let widthMap = new Map(); +export function measureTextWidth(text: string): number { + if (!ctx) { + throw new Error(`Measuring context canvas is not initialized. Call init() before.`); + } + const key = text.length; + + const storedWidth = widthMap.get(key); + if (storedWidth) { + return storedWidth; + } + + const width = measurementMode === 'canvas' ? ctx.measureText(text).width : measureTextWidthWithDOM(text); + widthMap.set(key, width); + + return width; +} + +function measureTextWidthWithDOM(text: string) { + span.textContent = text; + + document.body.appendChild(span); + const width = span.getBoundingClientRect().width; + document.body.removeChild(span); + + return width; +} + +export function measureTextHeight(text: string, maxWidth: number, beforeWidth = 0) { + let logLines = 0; + const charWidth = measureTextWidth('e'); + let logLineCharsLength = Math.round(maxWidth / charWidth); + const firstLineCharsLength = Math.floor((maxWidth - beforeWidth) / charWidth) - 2 * charWidth; + const textLines = text.split('\n'); + + // Skip unnecessary measurements + if (textLines.length === 1 && text.length < firstLineCharsLength) { + return { + lines: 1, + height: lineHeight + paddingBottom, + }; + } + + for (const textLine of textLines) { + for (let start = 0; start < textLine.length; ) { + let testLogLine: string; + let width = 0; + let delta = 0; + let availableWidth = maxWidth - beforeWidth; + do { + testLogLine = textLine.substring(start, start + logLineCharsLength - delta); + width = measureTextWidth(testLogLine); + delta += 1; + } while (width >= availableWidth); + if (beforeWidth) { + beforeWidth = 0; + } + logLines += 1; + start += testLogLine.length; + } + } + + const height = logLines * lineHeight + paddingBottom; + + return { + lines: logLines, + height, + }; +} + +interface DisplayOptions { + wrap: boolean; + showTime: boolean; +} + +export function getLogLineSize( + logs: ProcessedLogModel[], + container: HTMLDivElement | null, + { wrap, showTime }: DisplayOptions, + index: number +) { + if (!container) { + return 0; + } + if (!wrap) { + return lineHeight + paddingBottom; + } + const storedSize = retrieveLogLineSize(logs[index].uid, container); + if (storedSize) { + return storedSize; + } + const gap = gridSize; + let optionsWidth = 0; + if (showTime) { + optionsWidth += logs[index].dimensions.timestampWidth + gap; + } + if (logs[index].logLevel) { + optionsWidth += logs[index].dimensions.levelWidth + gap; + } + const { height } = measureTextHeight(logs[index].body, getLogContainerWidth(container), optionsWidth); + return height; +} + +export function hasUnderOrOverflow(element: HTMLDivElement, calculatedHeight?: number): number | null { + const height = calculatedHeight ?? element.clientHeight; + if (element.scrollHeight > height) { + return element.scrollHeight; + } + const child = element.firstChild; + if (child instanceof HTMLDivElement && child.clientHeight < height) { + return child.clientHeight; + } + return null; +} + +const scrollBarWidth = getScrollbarWidth(); + +export function getLogContainerWidth(container: HTMLDivElement) { + return container.clientWidth - scrollBarWidth; +} + +export function getScrollbarWidth() { + const hiddenDiv = document.createElement('div'); + + hiddenDiv.style.width = '100px'; + hiddenDiv.style.height = '100px'; + hiddenDiv.style.overflow = 'scroll'; + hiddenDiv.style.position = 'absolute'; + hiddenDiv.style.top = '-9999px'; + + document.body.appendChild(hiddenDiv); + const width = hiddenDiv.offsetWidth - hiddenDiv.clientWidth; + document.body.removeChild(hiddenDiv); + + return width; +} + +let logLineSizesMap = new Map(); +export function resetLogLineSizes() { + logLineSizesMap = new Map(); +} + +export function storeLogLineSize(id: string, container: HTMLDivElement, height: number) { + const key = `${id}_${getLogContainerWidth(container)}`; + logLineSizesMap.set(key, height); +} + +export function retrieveLogLineSize(id: string, container: HTMLDivElement) { + const key = `${id}_${getLogContainerWidth(container)}`; + return logLineSizesMap.get(key); +} + +export interface ScrollToLogsEventPayload { + scrollTo: 'top' | 'bottom'; +} + +export class ScrollToLogsEvent extends BusEventWithPayload { + static type = 'logs-panel-scroll-to'; +} diff --git a/public/app/features/logs/logsModel.ts b/public/app/features/logs/logsModel.ts index a298b7f0bb9..60aef4364bd 100644 --- a/public/app/features/logs/logsModel.ts +++ b/public/app/features/logs/logsModel.ts @@ -92,13 +92,11 @@ export function dedupLogRows(rows: LogRowModel[], strategy?: LogsDedupStrategy): } return rows.reduce((result: LogRowModel[], row: LogRowModel, index) => { - const rowCopy = { ...row }; const previous = result[result.length - 1]; if (index > 0 && isDuplicateRow(row, previous, strategy)) { previous.duplicates!++; } else { - rowCopy.duplicates = 0; - result.push(rowCopy); + result.push({ ...row, duplicates: 0 }); } return result; }, []); diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 1036baa2e89..9f8ac9cda32 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -45,6 +45,8 @@ const histogramPanel = async () => await import(/* webpackChunkName: "histogramPanel" */ 'app/plugins/panel/histogram/module'); const livePanel = async () => await import(/* webpackChunkName: "livePanel" */ 'app/plugins/panel/live/module'); const logsPanel = async () => await import(/* webpackChunkName: "logsPanel" */ 'app/plugins/panel/logs/module'); +const newLogsPanel = async () => + await import(/* webpackChunkName: "newLogsPanel" */ 'app/plugins/panel/logs-new/module'); const newsPanel = async () => await import(/* webpackChunkName: "newsPanel" */ 'app/plugins/panel/news/module'); const pieChartPanel = async () => await import(/* webpackChunkName: "pieChartPanel" */ 'app/plugins/panel/piechart/module'); @@ -116,6 +118,7 @@ const builtInPlugins: Record Promise {} + +export const LogsPanel = ({ + data, + timeZone, + fieldConfig, + options: { showTime, wrapLogMessage, sortOrder, dedupStrategy }, + id, +}: LogsPanelProps) => { + const isAscending = sortOrder === LogsSortOrder.Ascending; + const style = useStyles2(getStyles); + const [logsContainer, setLogsContainer] = useState(null); + const [panelData, setPanelData] = useState(data); + // Prevents the scroll position to change when new data from infinite scrolling is received + const keepScrollPositionRef = useRef(false); + const { eventBus } = usePanelContext(); + + const logs = useMemo(() => { + const logsModel = panelData + ? dataFrameToLogsModel(panelData.series, data.request?.intervalMs, undefined, data.request?.targets) + : null; + return logsModel ? dedupLogRows(logsModel.rows, dedupStrategy) : []; + }, [data.request?.intervalMs, data.request?.targets, dedupStrategy, panelData]); + + useEffect(() => { + setPanelData(data); + }, [data]); + + useLayoutEffect(() => { + if (keepScrollPositionRef.current) { + keepScrollPositionRef.current = false; + return; + } + /** + * In dashboards, users with newest logs at the bottom have the expectation of keeping the scroll at the bottom + * when new data is received. See https://github.com/grafana/grafana/pull/37634 + */ + if (data.request?.app === CoreApp.Dashboard || data.request?.app === CoreApp.PanelEditor) { + eventBus.publish( + new ScrollToLogsEvent({ + scrollTo: isAscending ? 'top' : 'bottom', + }) + ); + } + }, [data.request?.app, eventBus, isAscending, logs]); + + if (!logs.length) { + return ; + } + + return ( +
setLogsContainer(element)}> + {logs.length > 0 && logsContainer && ( + + )} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css({ + marginBottom: theme.spacing(1.5), + minHeight: '100%', + maxHeight: '100%', + display: 'flex', + flex: 1, + flexDirection: 'column', + }), +}); diff --git a/public/app/plugins/panel/logs-new/img/icn-logs-panel.svg b/public/app/plugins/panel/logs-new/img/icn-logs-panel.svg new file mode 100644 index 00000000000..046b59454ad --- /dev/null +++ b/public/app/plugins/panel/logs-new/img/icn-logs-panel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/app/plugins/panel/logs-new/module.tsx b/public/app/plugins/panel/logs-new/module.tsx new file mode 100644 index 00000000000..1e024cfcbd3 --- /dev/null +++ b/public/app/plugins/panel/logs-new/module.tsx @@ -0,0 +1,73 @@ +import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription } from '@grafana/data'; + +import { LogsPanel } from './LogsPanel'; +import { Options } from './panelcfg.gen'; +import { LogsPanelSuggestionsSupplier } from './suggestions'; + +export const plugin = new PanelPlugin(LogsPanel) + .setPanelOptions((builder) => { + builder + .addBooleanSwitch({ + path: 'showTime', + name: 'Time', + description: '', + defaultValue: false, + }) + .addBooleanSwitch({ + path: 'wrapLogMessage', + name: 'Wrap lines', + description: '', + defaultValue: false, + }) + .addBooleanSwitch({ + path: 'enableLogDetails', + name: 'Enable log details', + description: '', + defaultValue: true, + }) + .addBooleanSwitch({ + path: 'enableInfiniteScrolling', + name: 'Enable infinite scrolling', + description: 'Experimental. Request more results by scrolling to the bottom of the logs list.', + defaultValue: false, + }) + .addRadio({ + path: 'dedupStrategy', + name: 'Deduplication', + description: '', + settings: { + options: [ + { value: LogsDedupStrategy.none, label: 'None', description: LogsDedupDescription[LogsDedupStrategy.none] }, + { + value: LogsDedupStrategy.exact, + label: 'Exact', + description: LogsDedupDescription[LogsDedupStrategy.exact], + }, + { + value: LogsDedupStrategy.numbers, + label: 'Numbers', + description: LogsDedupDescription[LogsDedupStrategy.numbers], + }, + { + value: LogsDedupStrategy.signature, + label: 'Signature', + description: LogsDedupDescription[LogsDedupStrategy.signature], + }, + ], + }, + defaultValue: LogsDedupStrategy.none, + }) + .addRadio({ + path: 'sortOrder', + name: 'Order', + description: '', + settings: { + options: [ + { value: LogsSortOrder.Descending, label: 'Newest first' }, + { value: LogsSortOrder.Ascending, label: 'Oldest first' }, + ], + }, + defaultValue: LogsSortOrder.Descending, + }); + }) + .setSuggestionsSupplier(new LogsPanelSuggestionsSupplier()); diff --git a/public/app/plugins/panel/logs-new/panelcfg.cue b/public/app/plugins/panel/logs-new/panelcfg.cue new file mode 100644 index 00000000000..ff5d3351ef3 --- /dev/null +++ b/public/app/plugins/panel/logs-new/panelcfg.cue @@ -0,0 +1,40 @@ +// Copyright 2023 Grafana Labs +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package grafanaplugin + +import ( + "github.com/grafana/grafana/packages/grafana-schema/src/common" +) + +composableKinds: PanelCfg: { + maturity: "experimental" + + lineage: { + schemas: [{ + version: [0, 0] + schema: { + Options: { + showTime: bool + wrapLogMessage: bool + enableLogDetails: bool + sortOrder: common.LogsSortOrder + dedupStrategy: common.LogsDedupStrategy + enableInfiniteScrolling?: bool + } @cuetsy(kind="interface") + } + }] + lenses: [] + } +} diff --git a/public/app/plugins/panel/logs-new/panelcfg.gen.ts b/public/app/plugins/panel/logs-new/panelcfg.gen.ts new file mode 100644 index 00000000000..e8d58be30ce --- /dev/null +++ b/public/app/plugins/panel/logs-new/panelcfg.gen.ts @@ -0,0 +1,20 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// public/app/plugins/gen.go +// Using jennies: +// TSTypesJenny +// PluginTsTypesJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +import * as common from '@grafana/schema'; + +export interface Options { + dedupStrategy: common.LogsDedupStrategy; + enableInfiniteScrolling?: boolean; + enableLogDetails: boolean; + showTime: boolean; + sortOrder: common.LogsSortOrder; + wrapLogMessage: boolean; +} diff --git a/public/app/plugins/panel/logs-new/plugin.json b/public/app/plugins/panel/logs-new/plugin.json new file mode 100644 index 00000000000..54357e1cd97 --- /dev/null +++ b/public/app/plugins/panel/logs-new/plugin.json @@ -0,0 +1,17 @@ +{ + "type": "panel", + "name": "Logs (new)", + "id": "logs-new", + "state": "alpha", + + "info": { + "author": { + "name": "Grafana Labs", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-logs-panel.svg", + "large": "img/icn-logs-panel.svg" + } + } +} diff --git a/public/app/plugins/panel/logs-new/suggestions.ts b/public/app/plugins/panel/logs-new/suggestions.ts new file mode 100644 index 00000000000..5229b4fb7f0 --- /dev/null +++ b/public/app/plugins/panel/logs-new/suggestions.ts @@ -0,0 +1,33 @@ +import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; +import { SuggestionName } from 'app/types/suggestions'; + +import { Options } from './panelcfg.gen'; + +export class LogsPanelSuggestionsSupplier { + getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { + const list = builder.getListAppender({ + name: '', + pluginId: 'logs-new', + options: {}, + fieldConfig: { + defaults: { + custom: {}, + }, + overrides: [], + }, + }); + + const { dataSummary: ds } = builder; + + // Require a string & time field + if (!ds.hasData || !ds.hasTimeField || !ds.hasStringField) { + return; + } + + if (ds.preferredVisualisationType === 'logs') { + list.append({ name: SuggestionName.Logs, score: VisualizationSuggestionScore.Best }); + } else { + list.append({ name: SuggestionName.Logs }); + } + } +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f97f3beb611..a17c05d5399 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1911,6 +1911,13 @@ "shortcut": "alt+select to enable again" } }, + "logs-navigation": { + "newer-logs": "Newer logs", + "older-logs": "Older logs", + "scroll-bottom": "Scroll to bottom", + "scroll-top": "Scroll to top", + "start-of-range": "Start of range" + }, "popover-menu": { "copy": "Copy selection", "disable-menu": "Disable menu", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 76b82980d04..b0948db6816 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1911,6 +1911,13 @@ "shortcut": "äľŧ+şęľęčŧ ŧő ęʼnäþľę äģäįʼn" } }, + "logs-navigation": { + "newer-logs": "Ńęŵęř ľőģş", + "older-logs": "Øľđęř ľőģş", + "scroll-bottom": "Ŝčřőľľ ŧő þőŧŧőm", + "scroll-top": "Ŝčřőľľ ŧő ŧőp", + "start-of-range": "Ŝŧäřŧ őƒ řäʼnģę" + }, "popover-menu": { "copy": "Cőpy şęľęčŧįőʼn", "disable-menu": "Đįşäþľę męʼnū", From 6eac07c3a7efc5e0df52049d904713a4df96f6ac Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Tue, 4 Feb 2025 11:44:02 -0700 Subject: [PATCH 342/894] Image Renderer: Add support for SSL in plugin mode (#98009) --- conf/defaults.ini | 1 + .../setup-grafana/configure-grafana/_index.md | 2 ++ pkg/services/rendering/rendering.go | 17 +++++++++++++---- pkg/services/rendering/rendering_test.go | 7 +++++++ 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 859e0c65d84..9fa38434c95 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1741,6 +1741,7 @@ sas_token_expiration_days = # URL to a remote HTTP image renderer service, e.g. http://localhost:8081/render, will enable Grafana to render panels and dashboards to PNG-images using HTTP requests to an external service. server_url = # If the remote HTTP image renderer service runs on a different server than the Grafana server you may have to configure this to a URL where Grafana is reachable, e.g. http://grafana.domain/. +# The `callback_url` can also be configured to support usage of the image renderer running as a plugin with support for SSL / HTTPS. For example https://localhost:3000/. callback_url = # An auth token that will be sent to and verified by the renderer. The renderer will deny any request without an auth token matching the one configured on the renderer side. renderer_token = - diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 5c15ff2c0b8..1d50227704a 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -2352,6 +2352,8 @@ URL to a remote HTTP image renderer service, for example, `http://localhost:8081 If the remote HTTP image renderer service runs on a different server than the Grafana server you may have to configure this to a URL where Grafana is reachable, for example, http://grafana.domain/. +The `callback_url` can also be configured to support usage of the image renderer running as a plugin with support for SSL / HTTPS. For example https://localhost:3000/. + #### `concurrent_render_request_limit` Concurrent render request limit affects when the /render HTTP endpoint is used. Rendering many images at the same time can overload the server, diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index cb25268154f..744f5e09e18 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -81,14 +81,14 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, remot var domain string switch { - case cfg.RendererUrl != "": - // RendererCallbackUrl has already been passed, it won't generate an error. + case cfg.RendererCallbackUrl != "": u, err := url.Parse(cfg.RendererCallbackUrl) if err != nil { + logger.Warn("Image renderer callback url is not valid. " + + "Please provide a valid RendererCallbackUrl. " + + "Read more at https://grafana.com/docs/grafana/latest/administration/image_rendering/") return nil, err } - - sanitizeURL = getSanitizerURL(cfg.RendererUrl) domain = u.Hostname() case cfg.HTTPAddr != setting.DefaultHTTPAddr: domain = cfg.HTTPAddr @@ -96,6 +96,10 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, remot domain = "localhost" } + if cfg.RendererUrl != "" { + sanitizeURL = getSanitizerURL(cfg.RendererUrl) + } + var renderKeyProvider renderKeyProvider if features.IsEnabledGlobally(featuremgmt.FlagRenderAuthJWT) { renderKeyProvider = &jwtRenderKeyProvider{ @@ -429,6 +433,11 @@ func (rs *RenderingService) getGrafanaCallbackURL(path string) string { return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path) } + if rs.Cfg.RendererCallbackUrl != "" { + // &render=1 signals to the legacy redirect layer to + return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path) + } + protocol := rs.Cfg.Protocol switch protocol { case setting.HTTPScheme: diff --git a/pkg/services/rendering/rendering_test.go b/pkg/services/rendering/rendering_test.go index cdc484baf31..2bcd6463633 100644 --- a/pkg/services/rendering/rendering_test.go +++ b/pkg/services/rendering/rendering_test.go @@ -31,8 +31,15 @@ func TestGetUrl(t *testing.T) { require.Equal(t, rs.Cfg.RendererCallbackUrl+path+"&render=1", url) }) + t.Run("When callback url is configured and https should return domain of callback url plus path", func(t *testing.T) { + rs.Cfg.RendererCallbackUrl = "https://public-grafana.com/" + url := rs.getGrafanaCallbackURL(path) + require.Equal(t, rs.Cfg.RendererCallbackUrl+path+"&render=1", url) + }) + t.Run("When renderer url not configured", func(t *testing.T) { rs.Cfg.RendererUrl = "" + rs.Cfg.RendererCallbackUrl = "" rs.domain = "localhost" rs.Cfg.HTTPPort = "3000" From 68f1730461427dd6d3d5b077a7963897091225cf Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Tue, 4 Feb 2025 14:23:15 -0500 Subject: [PATCH 343/894] Alerting: set updated_by for system owned operations (#100068) --- pkg/services/ngalert/api/api_ruler.go | 21 +++++++++++++++---- pkg/services/ngalert/api/api_ruler_test.go | 18 ++++++++++++++++ pkg/services/ngalert/models/alert_rule.go | 5 +++++ .../ngalert/provisioning/alert_rules.go | 18 +++++++++++----- pkg/services/ngalert/store/alert_rule.go | 4 ++-- 5 files changed, 55 insertions(+), 11 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 215048e448c..1f122c04ad1 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -757,23 +757,36 @@ type userIDToUserInfoFn func(id *ngmodels.UserUID) *apimodels.UserInfo // getIdentityName returns name of either user or service account func (srv RulerSrv) resolveUserIdToNameFn(ctx context.Context) userIDToUserInfoFn { + cache := map[ngmodels.UserUID]*apimodels.UserInfo{ + ngmodels.AlertingUserUID: { + UID: string(ngmodels.AlertingUserUID), + }, + ngmodels.FileProvisioningUserUID: { + UID: string(ngmodels.FileProvisioningUserUID), + }, + } return func(id *ngmodels.UserUID) *apimodels.UserInfo { if id == nil { return nil } + if val, ok := cache[*id]; ok { + return val + } u, err := srv.userService.GetByUID(ctx, &user.GetUserByUIDQuery{ UID: string(*id), }) - var result string + var name string if err != nil { srv.log.FromContext(ctx).Warn("Failed to get user by uid. Defaulting to an empty name", "uid", id, "error", err) } if u != nil { - result = u.NameOrFallback() + name = u.NameOrFallback() } - return &apimodels.UserInfo{ + result := &apimodels.UserInfo{ UID: string(*id), - Name: result, + Name: name, } + cache[*id] = result + return result } } diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index dce8f9a801c..73cf2bb6300 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -404,6 +404,24 @@ func TestRouteGetRuleByUID(t *testing.T) { Name: "Test", }, }, + { + desc: "recognize system identifier (alerting)", + UpdatedBy: &models.AlertingUserUID, + User: nil, + UserServiceError: nil, + Expected: &apimodels.UserInfo{ + UID: string(models.AlertingUserUID), + }, + }, + { + desc: "recognize system identifier (provisioning)", + UpdatedBy: &models.FileProvisioningUserUID, + User: nil, + UserServiceError: nil, + Expected: &apimodels.UserInfo{ + UID: string(models.FileProvisioningUserUID), + }, + }, } for _, tc := range testcases { t.Run(tc.desc, func(t *testing.T) { diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 07a5eb86280..b7da813141f 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -47,6 +47,11 @@ var ( ErrNoPanel = errors.New("no panel") ) +var ( + FileProvisioningUserUID = UserUID("__provisioning__") + AlertingUserUID = UserUID("__alerting__") +) + // swagger:enum NoDataState type NoDataState string diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index 9b8cd429653..c31413b3bdc 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -232,7 +232,7 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, user ident } } err = service.xact.InTransaction(ctx, func(ctx context.Context) error { - ids, err := service.ruleStore.InsertAlertRules(ctx, models.NewUserUID(user), []models.AlertRule{ + ids, err := service.ruleStore.InsertAlertRules(ctx, userUidOrFallback(user), []models.AlertRule{ rule, }) if err != nil { @@ -359,7 +359,7 @@ func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, user ident } } - return service.ruleStore.UpdateAlertRules(ctx, models.NewUserUID(user), updateRules) + return service.ruleStore.UpdateAlertRules(ctx, userUidOrFallback(user), updateRules) }) } @@ -511,7 +511,7 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity New: *update.New, }) } - if err := service.ruleStore.UpdateAlertRules(ctx, models.NewUserUID(user), updates); err != nil { + if err := service.ruleStore.UpdateAlertRules(ctx, userUidOrFallback(user), updates); err != nil { return fmt.Errorf("failed to update alert rules: %w", err) } for _, update := range delta.Update { @@ -522,7 +522,7 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity } if len(delta.New) > 0 { - uids, err := service.ruleStore.InsertAlertRules(ctx, models.NewUserUID(user), withoutNilAlertRules(delta.New)) + uids, err := service.ruleStore.InsertAlertRules(ctx, userUidOrFallback(user), withoutNilAlertRules(delta.New)) if err != nil { return fmt.Errorf("failed to insert alert rules: %w", err) } @@ -618,7 +618,7 @@ func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, user ident return models.AlertRule{}, err } err = service.xact.InTransaction(ctx, func(ctx context.Context) error { - err := service.ruleStore.UpdateAlertRules(ctx, models.NewUserUID(user), []models.UpdateRule{ + err := service.ruleStore.UpdateAlertRules(ctx, userUidOrFallback(user), []models.UpdateRule{ { Existing: storedRule, New: rule, @@ -871,3 +871,11 @@ func (service *AlertRuleService) ensureNamespace(ctx context.Context, user ident return nil } + +func userUidOrFallback(user identity.Requester) *models.UserUID { + userUID := models.NewUserUID(user) + if user == nil { + return &models.FileProvisioningUserUID + } + return userUID +} diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index adcfe8450fa..fbc9e061385 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -988,7 +988,7 @@ func (st DBstore) RenameReceiverInNotificationSettings(ctx context.Context, orgI } // Provide empty user identifier to ensure it's clear that the rule update was made by the system // and not by the user who changed the receiver's title. - return result, nil, st.UpdateAlertRules(ctx, nil, updates) + return result, nil, st.UpdateAlertRules(ctx, &ngmodels.AlertingUserUID, updates) } // RenameTimeIntervalInNotificationSettings renames all rules that use old time interval name to the new name. @@ -1065,7 +1065,7 @@ func (st DBstore) RenameTimeIntervalInNotificationSettings( } // Provide empty user identifier to ensure it's clear that the rule update was made by the system // and not by the user who changed the receiver's title. - return result, nil, st.UpdateAlertRules(ctx, nil, updates) + return result, nil, st.UpdateAlertRules(ctx, &ngmodels.AlertingUserUID, updates) } func ruleConstraintViolationToErr(sess *db.Session, rule ngmodels.AlertRule, err error, logger log.Logger) error { From f9c4d3edce8e8ad827dc91b18415502802123cc3 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Tue, 4 Feb 2025 13:33:44 -0600 Subject: [PATCH 344/894] Unified Storage: Updates index latency logging (#100085) updates index latency logging --- pkg/storage/unified/resource/search.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index 6d03f97e6f2..f0d18960dc5 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -460,9 +460,9 @@ func (s *searchSupport) handleEvent(ctx context.Context, evt *WrittenEvent) { // record latency from when event was created to when it was indexed latencySeconds := float64(time.Now().UnixMicro()-evt.ResourceVersion) / 1e6 span.AddEvent("index latency", trace.WithAttributes(attribute.Float64("latency_seconds", latencySeconds))) - if latencySeconds > 5 { - s.log.Debug("high index latency object details", "resource", evt.Key.Resource, "latency_seconds", latencySeconds, "name", evt.Key.Name, "namespace", evt.Key.Namespace) - s.log.Warn("high index latency", "latency", latencySeconds) + s.log.Debug("indexed new object", "resource", evt.Key.Resource, "latency_seconds", latencySeconds, "name", evt.Key.Name, "namespace", evt.Key.Namespace, "rv", evt.ResourceVersion) + if latencySeconds > 1 { + s.log.Warn("high index latency object details", "resource", evt.Key.Resource, "latency_seconds", latencySeconds, "name", evt.Key.Name, "namespace", evt.Key.Namespace, "rv", evt.ResourceVersion) } if IndexMetrics != nil { IndexMetrics.IndexLatency.WithLabelValues(evt.Key.Resource).Observe(latencySeconds) From 7ebc81fbbfcd270ca077688c77a115ad0ecdcc44 Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Tue, 4 Feb 2025 15:18:30 -0600 Subject: [PATCH 345/894] Explore metrics: Fix otel bug (#100092) --- public/app/features/trails/otel/util.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/trails/otel/util.ts b/public/app/features/trails/otel/util.ts index cd96351c28b..166d089ff4e 100644 --- a/public/app/features/trails/otel/util.ts +++ b/public/app/features/trails/otel/util.ts @@ -522,6 +522,7 @@ export async function updateOtelData( resettingOtel: false, afterFirstOtelCheck: true, isUpdatingOtel: false, + nonPromotedOtelResources, }); } } From 17e21bff977fe970b2a7f63ad7f9cfb616d099ce Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:22:42 -0500 Subject: [PATCH 346/894] Dashboards: Fix title conversions (#100084) --- pkg/apis/dashboard/v1alpha1/conversion.go | 4 +++- pkg/apis/dashboard/v2alpha1/conversion.go | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/apis/dashboard/v1alpha1/conversion.go b/pkg/apis/dashboard/v1alpha1/conversion.go index 27bb5313336..358c482c4e3 100644 --- a/pkg/apis/dashboard/v1alpha1/conversion.go +++ b/pkg/apis/dashboard/v1alpha1/conversion.go @@ -34,6 +34,8 @@ func Convert_v0alpha1_Unstructured_To_v1alpha1_DashboardSpec(in *common.Unstruct func Convert_v1alpha1_DashboardSpec_To_v0alpha1_Unstructured(in *DashboardSpec, out *common.Unstructured, s conversion.Scope) error { *out = in.Unstructured - out.Object["title"] = in.Title + if in.Title != "" { + out.Object["title"] = in.Title + } return nil } diff --git a/pkg/apis/dashboard/v2alpha1/conversion.go b/pkg/apis/dashboard/v2alpha1/conversion.go index 7dcb7487b59..280cac67b14 100644 --- a/pkg/apis/dashboard/v2alpha1/conversion.go +++ b/pkg/apis/dashboard/v2alpha1/conversion.go @@ -34,6 +34,8 @@ func Convert_v0alpha1_Unstructured_To_v2alpha1_DashboardSpec(in *common.Unstruct func Convert_v2alpha1_DashboardSpec_To_v0alpha1_Unstructured(in *DashboardSpec, out *common.Unstructured, s conversion.Scope) error { *out = in.Unstructured - out.Object["title"] = in.Title + if in.Title != "" { + out.Object["title"] = in.Title + } return nil } From 5ad31ebe398e7dd65f31b076a205fc062d75d3c6 Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Wed, 5 Feb 2025 10:55:47 +0200 Subject: [PATCH 347/894] API keys: Migrate API keys to service accounts at startup (#96924) * migrate API keys to SA at startup * send metrics with api key migration stats * address feedback * run API keys migration in a server lock * update logging --- .../serviceaccounts/manager/service.go | 62 +++++++++++++++++++ pkg/services/serviceaccounts/manager/stats.go | 36 +++++++++++ 2 files changed, 98 insertions(+) diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index bfb3938b65c..2bb64c3646a 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" @@ -39,6 +40,8 @@ type ServiceAccountsService struct { log log.Logger backgroundLog log.Logger secretScanService secretscan.Checker + orgService org.Service + serverLock *serverlock.ServerLockService secretScanEnabled bool secretScanInterval time.Duration @@ -54,6 +57,7 @@ func ProvideServiceAccountsService( orgService org.Service, acService accesscontrol.Service, permissions accesscontrol.ServiceAccountPermissionsService, + serverLockService *serverlock.ServerLockService, ) (*ServiceAccountsService, error) { serviceAccountsStore := database.ProvideServiceAccountsStore( cfg, @@ -71,6 +75,8 @@ func ProvideServiceAccountsService( store: serviceAccountsStore, log: log.New("serviceaccounts"), backgroundLog: log.New("serviceaccounts.background"), + orgService: orgService, + serverLock: serverLockService, } if err := RegisterRoles(acService); err != nil { @@ -102,6 +108,17 @@ func (sa *ServiceAccountsService) Run(ctx context.Context) error { sa.log.Warn("Failed to get usage metrics", "error", err.Error()) } + err := sa.serverLock.LockAndExecute(ctx, "migrate API keys to service accounts", time.Minute*30, func(context.Context) { + err := sa.migrateAPIKeysForAllOrgs(ctx) + if err != nil { + sa.log.Warn("Failed to migrate API keys", "error", err.Error()) + } + }) + + if err != nil { + sa.log.Error("Failed to lock and execute the migration of API keys to service accounts", "error", err) + } + updateStatsTicker := time.NewTicker(metricsCollectionInterval) defer updateStatsTicker.Stop() @@ -299,6 +316,51 @@ func (sa *ServiceAccountsService) MigrateApiKeysToServiceAccounts(ctx context.Co return sa.store.MigrateApiKeysToServiceAccounts(ctx, orgID) } +func (sa *ServiceAccountsService) migrateAPIKeysForAllOrgs(ctx context.Context) error { + sa.log.Debug("Starting to migrate API keys to service accounts") + + total := 0 + migrated := 0 + failed := 0 + errorsTotal := 0 + + defer func() { + if total > 0 || errorsTotal > 0 { + sa.log.Info("API key migration finished", "total_keys", total, "successful_keys", migrated, "failed_keys", failed, "errors", errorsTotal) + } + setAPIKeyMigrationStats(total, migrated, failed) + }() + + orgs, err := sa.orgService.Search(ctx, &org.SearchOrgsQuery{}) + if err != nil { + return err + } + + for _, o := range orgs { + sa.log.Debug("Migrating API keys for an org", "orgId", o.ID) + + result, err := sa.store.MigrateApiKeysToServiceAccounts(ctx, o.ID) + if err != nil { + sa.log.Warn("Failed to migrate API keys", "error", err.Error(), "orgId", o.ID) + errorsTotal += 1 + continue + } + if result.Failed > 0 { + sa.log.Warn("Some API keys failed to be migrated", "total_keys", result.Total, "failed_keys", result.Failed, "orgId", o.ID) + } else if result.Total > 0 { + sa.log.Info("API key migration was successful", "orgId", o.ID, "total_keys", result.Total) + } else { + sa.log.Debug("No API keys found to migrate", "orgId", o.ID) + } + + total += result.Total + migrated += result.Migrated + failed += result.Failed + } + + return nil +} + func validOrgID(orgID int64) error { if orgID == 0 { return serviceaccounts.ErrServiceAccountInvalidOrgID.Errorf("invalid org ID 0 has been specified") diff --git a/pkg/services/serviceaccounts/manager/stats.go b/pkg/services/serviceaccounts/manager/stats.go index 521688ca66f..a882ad2db41 100644 --- a/pkg/services/serviceaccounts/manager/stats.go +++ b/pkg/services/serviceaccounts/manager/stats.go @@ -20,6 +20,15 @@ var ( // MStatTotalServiceAccountTokens is a metric gauge for total number of service account tokens MStatTotalServiceAccountTokens prometheus.Gauge + // MStatTotalMigratedAPIKeysToSATokens is a metric gauge for total number of API keys to be migrated to service account tokens + MStatTotalMigratedAPIKeysToSATokens prometheus.Gauge + + // MStatSuccessfullyMigratedAPIKeysToSATokens is a metric gauge for total number of successful migrations of API keys to service account tokens + MStatSuccessfullyMigratedAPIKeysToSATokens prometheus.Gauge + + // MStatFailedMigratedAPIKeysToSATokens is a metric gauge for total number of failed migrations of API keys to service account tokens + MStatFailedMigratedAPIKeysToSATokens prometheus.Gauge + Initialised bool = false ) @@ -42,10 +51,31 @@ func init() { Namespace: ExporterName, }) + MStatTotalMigratedAPIKeysToSATokens = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "stat_total_migrated_api_keys_to_sa_tokens", + Help: "total number of API keys to be migrated to service account tokens", + Namespace: ExporterName, + }) + + MStatSuccessfullyMigratedAPIKeysToSATokens = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "stat_successfully_migrated_api_keys_to_sa_tokens", + Help: "total number of successful migrations of API keys to service account tokens", + Namespace: ExporterName, + }) + + MStatFailedMigratedAPIKeysToSATokens = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "stat_failed_migrated_api_keys_to_sa_tokens", + Help: "total number of failed migrations of API keys to service account tokens", + Namespace: ExporterName, + }) + prometheus.MustRegister( MStatTotalServiceAccounts, MStatTotalServiceAccountTokens, MStatTotalServiceAccountsNoRole, + MStatTotalMigratedAPIKeysToSATokens, + MStatSuccessfullyMigratedAPIKeysToSATokens, + MStatFailedMigratedAPIKeysToSATokens, ) } @@ -81,3 +111,9 @@ func (sa *ServiceAccountsService) getUsageMetrics(ctx context.Context) (map[stri return stats, nil } + +func setAPIKeyMigrationStats(total, migrated, failed int) { + MStatTotalMigratedAPIKeysToSATokens.Set(float64(total)) + MStatSuccessfullyMigratedAPIKeysToSATokens.Set(float64(migrated)) + MStatFailedMigratedAPIKeysToSATokens.Set(float64(failed)) +} From bea62aa6150344439c30026b39253d6bf6da48e4 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 5 Feb 2025 10:58:19 +0200 Subject: [PATCH 348/894] Alerting: Update IRM copies in Configuration Tracker (#100069) * updated alerting configuration tracker with new IRM copies * Configuration tracker steps * copy * CI trigger --------- Co-authored-by: Sonia Aguilar --- .../gops/configuration-tracker/irmHooks.ts | 362 +++++++++++------- 1 file changed, 215 insertions(+), 147 deletions(-) diff --git a/public/app/features/gops/configuration-tracker/irmHooks.ts b/public/app/features/gops/configuration-tracker/irmHooks.ts index 1f02021aff6..76c738e80f3 100644 --- a/public/app/features/gops/configuration-tracker/irmHooks.ts +++ b/public/app/features/gops/configuration-tracker/irmHooks.ts @@ -6,6 +6,7 @@ import { useNotificationPolicyRoute } from 'app/features/alerting/unified/compon import { getIrmIfPresentOrIncidentPluginId, getIrmIfPresentOrOnCallPluginId, + getIsIrmPluginPresent, } from 'app/features/alerting/unified/utils/config'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { RelativeUrl, createRelativeUrl } from 'app/features/alerting/unified/utils/url'; @@ -125,175 +126,242 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { locationService.push(urlToGoWithIntegration); } + function getGrafanaAlertingConfigSteps(): SectionDtoStep[] { + let steps: SectionDtoStep[] = [ + { + title: 'Update default contact point', + description: 'Update the default contact point to a method other than the example email address.', + button: { + type: 'openLink', + urlLink: { + url: `/alerting/notifications`, + queryParams: { search: defaultContactpoint, alertmanager: 'grafana' }, + }, + label: 'Edit', + labelOnDone: 'View', + urlLinkOnDone: { + url: `/alerting/notifications`, + }, + }, + done: isContactPointReady(defaultContactpoint, contactPoints), + }, + ]; + + if (!getIsIrmPluginPresent()) { + steps = [ + ...steps, + { + title: 'Connect alerting to OnCall', + description: 'Create an OnCall integration for an alerting contact point.', + button: { + type: 'openLink', + urlLink: { + url: '/alerting/notifications/receivers/new', + }, + label: 'Connect', + urlLinkOnDone: { + url: '/alerting/notifications', + }, + labelOnDone: 'View', + }, + done: isOnCallContactPointReady(contactPoints), + }, + ]; + } + + steps = [ + ...steps, + { + title: 'Create alert rule', + description: 'Create an alert rule to monitor your system.', + button: { + type: 'openLink', + urlLink: { + url: '/alerting/new', + }, + label: 'Create', + urlLinkOnDone: { + url: '/alerting/list', + }, + labelOnDone: 'View', + }, + done: isCreateAlertRuleDone, + }, + { + title: 'Create SLO', + description: 'Create SLOs to monitor your service.', + button: { + type: 'openLink', + urlLink: { + url: '/a/grafana-slo-app/wizard/new', + }, + label: 'Create', + urlLinkOnDone: { + url: '/a/grafana-slo-app/manage-slos', + }, + labelOnDone: 'View', + }, + done: hasSlo, + }, + { + title: 'Enable SLO alerting', + description: 'Configure SLO alerting to receive notifications when your SLOs are breached.', + button: { + type: 'openLink', + urlLink: { + queryParams: { alertsEnabled: 'disabled' }, + url: '/a/grafana-slo-app/manage-slos', + }, + label: 'Enable', + urlLinkOnDone: { + queryParams: { alertsEnabled: 'enabled' }, + url: '/a/grafana-slo-app/manage-slos', + }, + labelOnDone: 'View', + }, + done: hasSloWithAlert, + }, + ]; + + return steps; + } + const essentialContent: SectionsDto = { sections: [ { title: 'Detect', description: 'Configure Grafana Alerting', - steps: [ - { - title: 'Update default contact point', - description: 'Update the default contact point to a method other than the example email address.', - button: { - type: 'openLink', - urlLink: { - url: `/alerting/notifications`, - queryParams: { search: defaultContactpoint, alertmanager: 'grafana' }, - }, - label: 'Edit', - labelOnDone: 'View', - urlLinkOnDone: { - url: `/alerting/notifications`, - }, - }, - done: isContactPointReady(defaultContactpoint, contactPoints), - }, - { - title: 'Connect alerting to OnCall', - description: 'Create an OnCall integration for an alerting contact point.', - button: { - type: 'openLink', - urlLink: { - url: '/alerting/notifications/receivers/new', - }, - label: 'Connect', - urlLinkOnDone: { - url: '/alerting/notifications', - }, - labelOnDone: 'View', - }, - done: isOnCallContactPointReady(contactPoints), - }, - { - title: 'Create alert rule', - description: 'Create an alert rule to monitor your system.', - button: { - type: 'openLink', - urlLink: { - url: '/alerting/new', - }, - label: 'Create', - urlLinkOnDone: { - url: '/alerting/list', - }, - labelOnDone: 'View', - }, - done: isCreateAlertRuleDone, - }, - { - title: 'Create your first SLO', - description: 'Create SLOs to monitor your service.', - button: { - type: 'openLink', - urlLink: { - url: '/a/grafana-slo-app/wizard/new', - }, - label: 'Create', - urlLinkOnDone: { - url: '/a/grafana-slo-app/manage-slos', - }, - labelOnDone: 'View', - }, - done: hasSlo, - }, - { - title: 'Enable SLO alerting', - description: 'Configure SLO alerting to receive notifications when your SLOs are breached.', - button: { - type: 'openLink', - urlLink: { - queryParams: { alertsEnabled: 'disabled' }, - url: '/a/grafana-slo-app/manage-slos', - }, - label: 'Enable', - urlLinkOnDone: { - queryParams: { alertsEnabled: 'enabled' }, - url: '/a/grafana-slo-app/manage-slos', - }, - labelOnDone: 'View', - }, - done: hasSloWithAlert, - }, - ], + steps: getGrafanaAlertingConfigSteps(), }, { title: 'Respond', description: 'Configure OnCall and Incident', - steps: [ - { - title: 'Initialize Incident plugin', - description: 'Initialize the Incident plugin to declare and manage incidents.', - button: { - type: 'openLink', - urlLink: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}/walkthrough/generate-key`, + steps: getIsIrmPluginPresent() + ? [ + { + title: 'Connect alerting to IRM', + description: 'Create an IRM integration for an alerting contact point.', + button: { + type: 'openLink', + urlLink: { + url: '/alerting/notifications/receivers/new', + }, + label: 'Connect', + urlLinkOnDone: { + url: '/alerting/notifications', + }, + labelOnDone: 'View', + }, + done: isOnCallContactPointReady(contactPoints), }, - label: 'Initialize', - urlLinkOnDone: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}`, + { + title: 'Connect IRM to your Slack workspace', + description: + 'Receive alerts and oncall notifications, or automatically create an incident channel and manage incidents directly within your chat environment.', + button: { + type: 'openLink', + urlLink: { + url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/grate.slack`, + }, + label: 'Connect', + urlLinkOnDone: { + url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations`, + }, + }, + done: isChatOpsInstalled, }, - labelOnDone: 'View', - }, - done: isIncidentsInstalled, - }, - { - title: 'Connect your Messaging workspace to OnCall', - description: 'Receive alerts and oncall notifications within your chat environment.', - button: { - type: 'openLink', - urlLink: { - url: `/a/${getIrmIfPresentOrOnCallPluginId()}/settings`, - queryParams: { tab: 'ChatOps', chatOpsTab: 'Slack' }, + { + title: 'Add Slack notifications to IRM integrations', + description: 'Select ChatOps channels to route notifications', + button: { + type: 'openLink', + urlLink: { + url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, + }, + label: 'Add', + urlLinkOnDone: { + url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, + }, + labelOnDone: 'View', + }, + done: is_integration_chatops_connected, }, - label: 'Connect', - urlLinkOnDone: { - url: `/a/${getIrmIfPresentOrOnCallPluginId()}/settings`, - queryParams: { tab: 'ChatOps' }, + ] + : [ + { + title: 'Initialize Incident plugin', + description: 'Initialize the Incident plugin to declare and manage incidents.', + button: { + type: 'openLink', + urlLink: { + url: `/a/${getIrmIfPresentOrIncidentPluginId()}/walkthrough/generate-key`, + }, + label: 'Initialize', + urlLinkOnDone: { + url: `/a/${getIrmIfPresentOrIncidentPluginId()}`, + }, + labelOnDone: 'View', + }, + done: isIncidentsInstalled, }, - labelOnDone: 'View', - }, - done: is_chatops_connected, - }, - { - title: 'Connect your Messaging workspace to Incident', - description: - 'Automatically create an incident channel and manage incidents directly within your chat environment.', - button: { - type: 'openLink', - urlLink: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/grate.slack`, + { + title: 'Connect your Messaging workspace to OnCall', + description: 'Receive alerts and oncall notifications within your chat environment.', + button: { + type: 'openLink', + urlLink: { + url: `/a/${getIrmIfPresentOrOnCallPluginId()}/settings`, + queryParams: { tab: 'ChatOps', chatOpsTab: 'Slack' }, + }, + label: 'Connect', + urlLinkOnDone: { + url: `/a/${getIrmIfPresentOrOnCallPluginId()}/settings`, + queryParams: { tab: 'ChatOps' }, + }, + labelOnDone: 'View', + }, + done: is_chatops_connected, }, - label: 'Connect', - urlLinkOnDone: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations`, + { + title: 'Connect your Messaging workspace to Incident', + description: + 'Automatically create an incident channel and manage incidents directly within your chat environment.', + button: { + type: 'openLink', + urlLink: { + url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/grate.slack`, + }, + label: 'Connect', + urlLinkOnDone: { + url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations`, + }, + }, + done: isChatOpsInstalled, }, - }, - done: isChatOpsInstalled, - }, - { - title: 'Add Messaging workspace channel to OnCall Integration', - description: 'Select ChatOps channels to route notifications', - button: { - type: 'openLink', - urlLink: { - url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, + { + title: 'Add Messaging workspace channel to OnCall Integration', + description: 'Select ChatOps channels to route notifications', + button: { + type: 'openLink', + urlLink: { + url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, + }, + label: 'Add', + urlLinkOnDone: { + url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, + }, + labelOnDone: 'View', + }, + done: is_integration_chatops_connected, }, - label: 'Add', - urlLinkOnDone: { - url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, - }, - labelOnDone: 'View', - }, - done: is_integration_chatops_connected, - }, - ], + ], }, { title: 'Test your configuration', description: '', steps: [ { - title: 'Send OnCall demo alert via Alerting integration', + title: getIsIrmPluginPresent() ? 'Send test alert' : 'Send OnCall demo alert via Alerting integration', description: 'In the integration page, click Send demo alert, to review your notification', button: { type: 'dropDown', From f51571db5d35e9e6e8ff459b4663b1db3c08deb6 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Wed, 5 Feb 2025 11:08:41 +0200 Subject: [PATCH 349/894] Dashboards: Refactor types for dynamic dashboards (#100064) --- .betterer.results | 4 - .../edit-pane/DashboardEditPane.tsx | 4 +- .../edit-pane/DashboardEditableElement.tsx | 9 +- .../edit-pane/ElementEditPane.tsx | 5 +- .../edit-pane/ElementSelection.ts | 9 +- .../MultiSelectedObjectsEditableElement.tsx | 11 +- .../MultiSelectedVizPanelsEditableElement.tsx | 9 +- .../edit-pane/VizPanelEditableElement.tsx | 11 +- .../edit-pane/useEditableElement.ts | 3 +- .../panel-edit/PanelEditor.tsx | 2 +- .../panel-edit/getPanelFrameOptions.tsx | 2 +- .../dashboard-scene/scene/DashboardScene.tsx | 6 +- .../scene/DashboardSceneUrlSync.test.ts | 2 +- .../scene/DashboardSceneUrlSync.ts | 2 +- .../layout-default/DashboardGridItem.tsx | 10 +- .../DefaultGridLayoutManager.test.tsx | 14 +- .../DefaultGridLayoutManager.tsx | 26 +-- .../layout-default/RowRepeaterBehavior.ts | 2 +- .../ResponsiveGridItem.tsx | 9 +- .../ResponsiveGridLayoutManager.tsx | 30 +-- .../MultiSelectedRowItemsElement.tsx | 9 +- .../scene/layout-rows/RowItem.tsx | 12 +- .../layout-rows/RowItemRepeaterBehavior.ts | 2 +- .../scene/layout-rows/RowsLayoutManager.tsx | 26 +-- .../DashboardLayoutSelector.tsx | 8 +- .../scene/layouts-shared/layoutRegistry.ts | 8 +- .../scene/layouts-shared/utils.ts | 2 +- .../features/dashboard-scene/scene/types.ts | 207 ------------------ .../scene/types/BulkActionElement.ts | 8 + .../scene/types/DashboardLayoutItem.ts | 32 +++ .../scene/types/DashboardLayoutManager.ts | 82 +++++++ .../types/DashboardRepeatsProcessedEvent.ts | 10 + .../scene/types/EditableDashboardElement.ts | 40 ++++ .../scene/types/LayoutParent.ts | 14 ++ .../scene/types/LayoutRegistryItem.ts | 20 ++ .../MultiSelectedEditableDashboardElement.ts | 29 +++ .../transformSaveModelSchemaV2ToScene.test.ts | 2 +- .../dashboard-scene/solo/useSoloPanel.ts | 2 +- .../features/dashboard-scene/utils/utils.ts | 2 +- 39 files changed, 337 insertions(+), 348 deletions(-) delete mode 100644 public/app/features/dashboard-scene/scene/types.ts create mode 100644 public/app/features/dashboard-scene/scene/types/BulkActionElement.ts create mode 100644 public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts create mode 100644 public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts create mode 100644 public/app/features/dashboard-scene/scene/types/DashboardRepeatsProcessedEvent.ts create mode 100644 public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts create mode 100644 public/app/features/dashboard-scene/scene/types/LayoutParent.ts create mode 100644 public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts create mode 100644 public/app/features/dashboard-scene/scene/types/MultiSelectedEditableDashboardElement.ts diff --git a/.betterer.results b/.betterer.results index c9f78d230b0..629774cfa72 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3435,10 +3435,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/scene/UnlinkModal.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], - "public/app/features/dashboard-scene/scene/types.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], "public/app/features/dashboard-scene/serialization/angularMigration.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index 86ed2e16772..497b45bf109 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -176,7 +176,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla {openOverlay && ( - + )} @@ -185,7 +185,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla return (
- +
); } diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index f3814c2a7e1..1e458430481 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -6,10 +6,11 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { DashboardScene } from '../scene/DashboardScene'; import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector'; -import { EditableDashboardElement } from '../scene/types'; +import { EditableDashboardElement } from '../scene/types/EditableDashboardElement'; export class DashboardEditableElement implements EditableDashboardElement { - public isEditableDashboardElement: true = true; + public readonly isEditableDashboardElement = true; + public readonly typeName = 'Dashboard'; public constructor(private dashboard: DashboardScene) {} @@ -47,10 +48,6 @@ export class DashboardEditableElement implements EditableDashboardElement { return [dashboardOptions, layoutCategory]; } - - public getTypeName(): string { - return 'Dashboard'; - } } export function DashboardTitleInput({ dashboard }: { dashboard: DashboardScene }) { diff --git a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx index b60b48fd549..16adfe8004a 100644 --- a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx @@ -4,7 +4,8 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Stack, useStyles2 } from '@grafana/ui'; import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory'; -import { EditableDashboardElement, MultiSelectedEditableDashboardElement } from '../scene/types'; +import { EditableDashboardElement } from '../scene/types/EditableDashboardElement'; +import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; export interface Props { element: EditableDashboardElement | MultiSelectedEditableDashboardElement; @@ -19,7 +20,7 @@ export function ElementEditPane({ element }: Props) { {element.renderActions && ( diff --git a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts index 29f107a2d84..7a5a9ee61af 100644 --- a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts +++ b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts @@ -2,12 +2,9 @@ import { SceneObject, SceneObjectRef, VizPanel } from '@grafana/scenes'; import { ElementSelectionContextItem } from '@grafana/ui'; import { DashboardScene } from '../scene/DashboardScene'; -import { - EditableDashboardElement, - isBulkActionElement, - isEditableDashboardElement, - MultiSelectedEditableDashboardElement, -} from '../scene/types'; +import { isBulkActionElement } from '../scene/types/BulkActionElement'; +import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement'; +import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; import { DashboardEditableElement } from './DashboardEditableElement'; import { MultiSelectedObjectsEditableElement } from './MultiSelectedObjectsEditableElement'; diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx index 574de4164c1..79724e4474e 100644 --- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx @@ -3,10 +3,13 @@ import { ReactNode } from 'react'; import { Stack, Text, Button } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; -import { BulkActionElement, MultiSelectedEditableDashboardElement } from '../scene/types'; +import { BulkActionElement } from '../scene/types/BulkActionElement'; +import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; export class MultiSelectedObjectsEditableElement implements MultiSelectedEditableDashboardElement { - public isMultiSelectedEditableDashboardElement: true = true; + public readonly isMultiSelectedEditableDashboardElement = true; + public readonly typeName = 'Objects'; + private items?: BulkActionElement[]; constructor(items: BulkActionElement[]) { @@ -19,10 +22,6 @@ export class MultiSelectedObjectsEditableElement implements MultiSelectedEditabl } }; - public getTypeName(): string { - return 'Objects'; - } - renderActions(): ReactNode { return ( diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx index bf9de853158..38db6544d3a 100644 --- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx @@ -5,11 +5,12 @@ import { Button, Stack, Text } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; -import { MultiSelectedEditableDashboardElement } from '../scene/types'; +import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; export class MultiSelectedVizPanelsEditableElement implements MultiSelectedEditableDashboardElement { - public isMultiSelectedEditableDashboardElement: true = true; + public readonly isMultiSelectedEditableDashboardElement = true; + public readonly typeName = 'Panels'; private items?: VizPanel[]; @@ -34,10 +35,6 @@ export class MultiSelectedVizPanelsEditableElement implements MultiSelectedEdita } }; - public getTypeName(): string { - return 'Panels'; - } - renderActions(): ReactNode { return ( diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx index 36a2928a309..762dcb5f745 100644 --- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx @@ -12,11 +12,14 @@ import { PanelDescriptionTextArea, PanelFrameTitleInput, } from '../panel-edit/getPanelFrameOptions'; -import { BulkActionElement, EditableDashboardElement, isDashboardLayoutItem } from '../scene/types'; +import { BulkActionElement } from '../scene/types/BulkActionElement'; +import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem'; +import { EditableDashboardElement } from '../scene/types/EditableDashboardElement'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; export class VizPanelEditableElement implements EditableDashboardElement, BulkActionElement { - public isEditableDashboardElement: true = true; + public readonly isEditableDashboardElement = true; + public readonly typeName = 'Panel'; public constructor(private panel: VizPanel) {} @@ -96,10 +99,6 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc return categories; } - public getTypeName(): string { - return 'Panel'; - } - public onDelete = () => { const layout = dashboardSceneGraph.getLayoutManagerFor(this.panel); layout.removePanel(this.panel); diff --git a/public/app/features/dashboard-scene/edit-pane/useEditableElement.ts b/public/app/features/dashboard-scene/edit-pane/useEditableElement.ts index 2bfc44fbb30..2d6c79ff0f0 100644 --- a/public/app/features/dashboard-scene/edit-pane/useEditableElement.ts +++ b/public/app/features/dashboard-scene/edit-pane/useEditableElement.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; -import { EditableDashboardElement, MultiSelectedEditableDashboardElement } from '../scene/types'; +import { EditableDashboardElement } from '../scene/types/EditableDashboardElement'; +import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; import { ElementSelection } from './ElementSelection'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index c8de84d8d46..19d25fc7bd6 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -21,7 +21,7 @@ import { saveLibPanel } from 'app/features/library-panels/state/api'; import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; import { getPanelChanges } from '../saving/getDashboardChanges'; -import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types'; +import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem'; import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { activateSceneObjectAndParentTree, diff --git a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx index 98c93b0635e..8e0ac6a5e4b 100644 --- a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx @@ -10,7 +10,7 @@ import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/ import { VizPanelLinks } from '../scene/PanelLinks'; import { PanelTimeRange } from '../scene/PanelTimeRange'; -import { isDashboardLayoutItem } from '../scene/types'; +import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem'; import { vizPanelToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 02b179a003c..e015172d4c6 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -71,7 +71,7 @@ import { isUsingAngularDatasourcePlugin, isUsingAngularPanelPlugin } from './ang import { setupKeyboardShortcuts } from './keyboardShortcuts'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; -import { DashboardLayoutManager } from './types'; +import { DashboardLayoutManager } from './types/DashboardLayoutManager'; export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links', 'meta', 'preload']; export const PANEL_SEARCH_VAR = 'systemPanelFilterVar'; @@ -258,7 +258,7 @@ export class DashboardScene extends SceneObjectBase { this.setState({ isEditing: true, showHiddenElements: true }); // Propagate change edit mode change to children - this.state.body.editModeChanged(true); + this.state.body.editModeChanged?.(true); // Propagate edit mode to scopes this._scopesFacade?.enterReadOnly(); @@ -349,7 +349,7 @@ export class DashboardScene extends SceneObjectBase { } // Disable grid dragging - this.state.body.editModeChanged(false); + this.state.body.editModeChanged?.(false); } public canDiscard() { diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts index 6092bac64c2..4a7e9fca190 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts @@ -8,7 +8,7 @@ import { getCloneKey } from '../utils/clone'; import { DashboardScene } from './DashboardScene'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; -import { DashboardRepeatsProcessedEvent } from './types'; +import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent'; describe('DashboardSceneUrlSync', () => { describe('Given a standard scene', () => { diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 1eb85045504..3493be625f9 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -19,7 +19,7 @@ import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { ViewPanelScene } from './ViewPanelScene'; import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; -import { DashboardRepeatsProcessedEvent } from './types'; +import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent'; export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { private _eventSub?: Unsubscribable; diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx index 165016a3493..2f1be088090 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx @@ -25,7 +25,8 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components import { getCloneKey } from '../../utils/clone'; import { getMultiVariableValues, getQueryRunnerFor } from '../../utils/utils'; -import { DashboardLayoutItem, DashboardRepeatsProcessedEvent } from '../types'; +import { DashboardLayoutItem } from '../types/DashboardLayoutItem'; +import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; import { getDashboardGridItemOptions } from './DashboardGridItemEditor'; @@ -47,6 +48,8 @@ export class DashboardGridItem private _prevRepeatValues?: VariableValueSingle[]; protected _variableDependency = new DashboardGridItemVariableDependencyHandler(this); + public readonly isDashboardLayoutItem = true; + public constructor(state: DashboardGridItemState) { super(state); @@ -188,11 +191,6 @@ export class DashboardGridItem this.setState(stateUpdate); } - /** - * DashboardLayoutItem interface start - */ - public isDashboardLayoutItem: true = true; - /** * Returns options for panel edit */ diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx index e4bb910235e..8a06e7c9031 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx @@ -26,19 +26,19 @@ describe('DefaultGridLayoutManager', () => { }); }); - describe('getNextPanelId', () => { - it('should get next panel id in a simple 3 panel layout', () => { + describe('getMaxPanelId', () => { + it('should get max panel id in a simple 3 panel layout', () => { const { manager } = setup(); - const id = manager.getNextPanelId(); + const id = manager.getMaxPanelId(); - expect(id).toBe(4); + expect(id).toBe(3); }); - it('should return 1 if no panels are found', () => { + it('should return 0 if no panels are found', () => { const { manager } = setup({ gridItems: [] }); - const id = manager.getNextPanelId(); + const id = manager.getMaxPanelId(); - expect(id).toBe(1); + expect(id).toBe(0); }); }); diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index 3bae6e1c50c..131df5197b4 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -22,7 +22,7 @@ import { getGridItemKeyForPanelId, getDashboardSceneFor, } from '../../utils/utils'; -import { DashboardLayoutManager, LayoutRegistryItem } from '../types'; +import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { DashboardGridItem } from './DashboardGridItem'; import { RowRepeaterBehavior } from './RowRepeaterBehavior'; @@ -39,7 +39,16 @@ export class DefaultGridLayoutManager extends SceneObjectBase implements DashboardLayoutManager { - public isDashboardLayoutManager: true = true; + public readonly isDashboardLayoutManager = true; + + public static readonly descriptor = { + name: 'Default grid', + description: 'The default grid layout', + id: 'default-grid', + createFromLayout: DefaultGridLayoutManager.createFromLayout, + }; + + public readonly descriptor = DefaultGridLayoutManager.descriptor; public editModeChanged(isEditing: boolean): void { const updateResizeAndDragging = () => { @@ -328,10 +337,6 @@ export class DefaultGridLayoutManager }); } - public getDescriptor(): LayoutRegistryItem { - return DefaultGridLayoutManager.getDescriptor(); - } - public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { return this.clone({ grid: this.state.grid.clone({ @@ -402,15 +407,6 @@ export class DefaultGridLayoutManager }); } - public static getDescriptor(): LayoutRegistryItem { - return { - name: 'Default grid', - description: 'The default grid layout', - id: 'default-grid', - createFromLayout: DefaultGridLayoutManager.createFromLayout, - }; - } - /** * Handle switching to the manual grid layout from other layouts * @param currentLayout diff --git a/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts index 6ac38847198..8cfe331ec85 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts +++ b/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts @@ -23,7 +23,7 @@ import { isClonedKey, } from '../../utils/clone'; import { getMultiVariableValues } from '../../utils/utils'; -import { DashboardRepeatsProcessedEvent } from '../types'; +import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; import { DashboardGridItem } from './DashboardGridItem'; diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx index 342d23b42de..844211dab6f 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx @@ -6,7 +6,7 @@ import { Switch, useStyles2 } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; -import { DashboardLayoutItem } from '../types'; +import { DashboardLayoutItem } from '../types/DashboardLayoutItem'; export interface ResponsiveGridItemState extends SceneObjectState { body: VizPanel; @@ -14,6 +14,8 @@ export interface ResponsiveGridItemState extends SceneObjectState { } export class ResponsiveGridItem extends SceneObjectBase implements DashboardLayoutItem { + public readonly isDashboardLayoutItem = true; + public constructor(state: ResponsiveGridItemState) { super(state); this.addActivationHandler(() => this._activationHandler()); @@ -29,11 +31,6 @@ export class ResponsiveGridItem extends SceneObjectBase this.setState({ hideWhenNoData: !this.state.hideWhenNoData }); } - /** - * DashboardLayoutElement interface - */ - public isDashboardLayoutItem: true = true; - public getOptions?(): OptionsPaneCategoryDescriptor { const model = this; diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index 11f9e107640..54059d61707 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -5,7 +5,7 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { getDashboardSceneFor, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; -import { DashboardLayoutManager, LayoutRegistryItem } from '../types'; +import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { ResponsiveGridItem } from './ResponsiveGridItem'; @@ -17,7 +17,16 @@ export class ResponsiveGridLayoutManager extends SceneObjectBase implements DashboardLayoutManager { - public isDashboardLayoutManager: true = true; + public readonly isDashboardLayoutManager = true; + + public static readonly descriptor = { + name: 'Responsive grid', + description: 'CSS layout that adjusts to the available space', + id: 'responsive-grid', + createFromLayout: ResponsiveGridLayoutManager.createFromLayout, + }; + + public readonly descriptor = ResponsiveGridLayoutManager.descriptor; public editModeChanged(isEditing: boolean): void {} @@ -83,19 +92,6 @@ export class ResponsiveGridLayoutManager return getOptions(this); } - public getDescriptor(): LayoutRegistryItem { - return ResponsiveGridLayoutManager.getDescriptor(); - } - - public static getDescriptor(): LayoutRegistryItem { - return { - name: 'Responsive grid', - description: 'CSS layout that adjusts to the available space', - id: 'responsive-grid', - createFromLayout: ResponsiveGridLayoutManager.createFromLayout, - }; - } - public static createEmpty() { return new ResponsiveGridLayoutManager({ layout: new SceneCSSGridLayout({ @@ -123,10 +119,6 @@ export class ResponsiveGridLayoutManager }); } - toSaveModel?() { - throw new Error('Method not implemented.'); - } - activateRepeaters?(): void { throw new Error('Method not implemented.'); } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/MultiSelectedRowItemsElement.tsx b/public/app/features/dashboard-scene/scene/layout-rows/MultiSelectedRowItemsElement.tsx index 8cfc0704ef0..41f57d48577 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/MultiSelectedRowItemsElement.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/MultiSelectedRowItemsElement.tsx @@ -6,12 +6,13 @@ import { t, Trans } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; -import { MultiSelectedEditableDashboardElement } from '../types'; +import { MultiSelectedEditableDashboardElement } from '../types/MultiSelectedEditableDashboardElement'; import { RowItem } from './RowItem'; export class MultiSelectedRowItemsElement implements MultiSelectedEditableDashboardElement { - public isMultiSelectedEditableDashboardElement: true = true; + public readonly isMultiSelectedEditableDashboardElement = true; + public readonly typeName = 'Rows'; private items?: RowItem[]; @@ -44,10 +45,6 @@ export class MultiSelectedRowItemsElement implements MultiSelectedEditableDashbo return [rowOptions]; } - public getTypeName(): string { - return 'Rows'; - } - public onDelete = () => { for (const item of this.items || []) { item.onDelete(); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx index 51a92a72c92..756092f00eb 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx @@ -33,7 +33,10 @@ import { isClonedKey } from '../../utils/clone'; import { getDashboardSceneFor, getDefaultVizPanel, getQueryRunnerFor } from '../../utils/utils'; import { DashboardScene } from '../DashboardScene'; import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector'; -import { BulkActionElement, DashboardLayoutManager, EditableDashboardElement, LayoutParent } from '../types'; +import { BulkActionElement } from '../types/BulkActionElement'; +import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; +import { EditableDashboardElement } from '../types/EditableDashboardElement'; +import { LayoutParent } from '../types/LayoutParent'; import { MultiSelectedRowItemsElement } from './MultiSelectedRowItemsElement'; import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior'; @@ -55,7 +58,8 @@ export class RowItem statePaths: ['title'], }); - public isEditableDashboardElement: true = true; + public readonly isEditableDashboardElement = true; + public readonly typeName = 'Row'; public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { const row = this; @@ -107,10 +111,6 @@ export class RowItem return [rowOptions, rowRepeatOptions, layoutOptions]; } - public getTypeName(): string { - return 'Row'; - } - public createMultiSelectedElement(items: SceneObject[]) { return new MultiSelectedRowItemsElement(items); } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.ts index 15b376fd0dd..175351744a6 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.ts +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.ts @@ -13,7 +13,7 @@ import { import { isClonedKeyOf, getCloneKey } from '../../utils/clone'; import { getMultiVariableValues } from '../../utils/utils'; -import { DashboardRepeatsProcessedEvent } from '../types'; +import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; import { RowItem } from './RowItem'; import { RowsLayoutManager } from './RowsLayoutManager'; 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 c888f5d9c8f..8aaa20972c5 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -18,7 +18,7 @@ import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior'; import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager'; -import { DashboardLayoutManager, LayoutRegistryItem } from '../types'; +import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { RowItem } from './RowItem'; import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior'; @@ -28,7 +28,16 @@ interface RowsLayoutManagerState extends SceneObjectState { } export class RowsLayoutManager extends SceneObjectBase implements DashboardLayoutManager { - public isDashboardLayoutManager: true = true; + public readonly isDashboardLayoutManager = true; + + public static readonly descriptor = { + name: 'Rows', + description: 'Rows layout', + id: 'rows-layout', + createFromLayout: RowsLayoutManager.createFromLayout, + }; + + public readonly descriptor = RowsLayoutManager.descriptor; public editModeChanged(isEditing: boolean): void {} @@ -113,23 +122,10 @@ export class RowsLayoutManager extends SceneObjectBase i }); } - public getDescriptor(): LayoutRegistryItem { - return RowsLayoutManager.getDescriptor(); - } - public getSelectedObject() { return sceneGraph.getAncestor(this, DashboardScene).state.editPane.state.selection?.getFirstObject(); } - public static getDescriptor(): LayoutRegistryItem { - return { - name: 'Rows', - description: 'Rows layout', - id: 'rows-layout', - createFromLayout: RowsLayoutManager.createFromLayout, - }; - } - public static createEmpty() { return new RowsLayoutManager({ rows: [] }); } diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx index d1af76ef4f2..d4b2da7b609 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx +++ b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx @@ -4,7 +4,9 @@ import { Select } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; -import { DashboardLayoutManager, isLayoutParent, LayoutRegistryItem } from '../types'; +import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; +import { isLayoutParent } from '../types/LayoutParent'; +import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; import { layoutRegistry } from './layoutRegistry'; import { findParentLayout } from './utils'; @@ -16,7 +18,7 @@ export interface Props { export function DashboardLayoutSelector({ layoutManager }: Props) { const options = useMemo(() => { const parentLayout = findParentLayout(layoutManager); - const parentLayoutId = parentLayout?.getDescriptor().id; + const parentLayoutId = parentLayout?.descriptor.id; return layoutRegistry .list() @@ -27,7 +29,7 @@ export function DashboardLayoutSelector({ layoutManager }: Props) { })); }, [layoutManager]); - const currentLayoutId = layoutManager.getDescriptor().id; + const currentLayoutId = layoutManager.descriptor.id; const currentOption = options.find((option) => option.value.id === currentLayoutId); return ( diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/layoutRegistry.ts b/public/app/features/dashboard-scene/scene/layouts-shared/layoutRegistry.ts index 95f345e71b9..20236c20018 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/layoutRegistry.ts +++ b/public/app/features/dashboard-scene/scene/layouts-shared/layoutRegistry.ts @@ -3,12 +3,8 @@ import { Registry } from '@grafana/data'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; -import { LayoutRegistryItem } from '../types'; +import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; export const layoutRegistry: Registry = new Registry(() => { - return [ - DefaultGridLayoutManager.getDescriptor(), - ResponsiveGridLayoutManager.getDescriptor(), - RowsLayoutManager.getDescriptor(), - ]; + return [DefaultGridLayoutManager.descriptor, ResponsiveGridLayoutManager.descriptor, RowsLayoutManager.descriptor]; }); diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts b/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts index 7fab7ee9aea..61a6d574877 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts +++ b/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts @@ -1,6 +1,6 @@ import { SceneObject } from '@grafana/scenes'; -import { DashboardLayoutManager, isDashboardLayoutManager } from '../types'; +import { DashboardLayoutManager, isDashboardLayoutManager } from '../types/DashboardLayoutManager'; export function findParentLayout(sceneObject: SceneObject): DashboardLayoutManager | null { let parent = sceneObject.parent; diff --git a/public/app/features/dashboard-scene/scene/types.ts b/public/app/features/dashboard-scene/scene/types.ts deleted file mode 100644 index d147d1579d9..00000000000 --- a/public/app/features/dashboard-scene/scene/types.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { BusEventWithPayload, RegistryItem } from '@grafana/data'; -import { SceneObject, VizPanel } from '@grafana/scenes'; -import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; -import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; - -/** - * A scene object that usually wraps an underlying layout - * Dealing with all the state management and editing of the layout - */ -export interface DashboardLayoutManager extends SceneObject { - /** Marks it as a DashboardLayoutManager */ - isDashboardLayoutManager: true; - - /** - * Notify the layout manager that the edit mode has changed - * @param isEditing - */ - editModeChanged(isEditing: boolean): void; - - /** - * Remove an element / panel - * @param element - */ - removePanel(panel: VizPanel): void; - - /** - * Creates a copy of an existing element and adds it to the layout - * @param element - */ - duplicatePanel(panel: VizPanel): void; - - /** - * Adds a new panel to the layout - */ - addPanel(panel: VizPanel): void; - - /** - * Add row - */ - addNewRow(): void; - - /** - * getVizPanels - */ - getVizPanels(): VizPanel[]; - - /** - * Turn into a save model - * @param saveModel - */ - toSaveModel?(): any; - - /** - * For dynamic panels that need to be viewed in isolation (SoloRoute) - */ - activateRepeaters?(): void; - - /** - * Gets the layout descriptor (which has the name and id) - */ - getDescriptor(): LayoutRegistryItem; - - /** - * Renders options and layout actions - */ - getOptions?(): OptionsPaneItemDescriptor[]; - - /** - * Create a clone of the layout manager given an ancestor key - * @param ancestorKey - * @param isSource - */ - cloneLayout?(ancestorKey: string, isSource: boolean): DashboardLayoutManager; - - /** - * Returns the highest panel id in the layout - */ - getMaxPanelId(): number; -} - -export function isDashboardLayoutManager(obj: SceneObject): obj is DashboardLayoutManager { - return 'isDashboardLayoutManager' in obj; -} - -/** - * The layout descriptor used when selecting / switching layouts - */ -export interface LayoutRegistryItem extends RegistryItem { - /** - * When switching between layouts - * @param currentLayout - */ - createFromLayout(currentLayout: DashboardLayoutManager): DashboardLayoutManager; - /** - * Create from persisted state - * @param saveModel - */ - createFromSaveModel?(saveModel: any): void; -} - -/** - * This interface is needed to support layouts existing on different levels of the scene (DashboardScene and inside the TabsLayoutManager) - */ -export interface LayoutParent extends SceneObject { - switchLayout(newLayout: DashboardLayoutManager): void; -} - -export function isLayoutParent(obj: SceneObject): obj is LayoutParent { - return 'switchLayout' in obj; -} - -/** - * Abstraction to handle editing of different layout elements (wrappers for VizPanels and other objects) - * Also useful to when rendering / viewing an element outside it's layout scope - */ -export interface DashboardLayoutItem extends SceneObject { - /** - * Marks this object as a layout item - */ - isDashboardLayoutItem: true; - /** - * Return layout item options (like repeat, repeat direction, etc for the default DashboardGridItem) - */ - getOptions?(): OptionsPaneCategoryDescriptor; - /** - * When going into panel edit - **/ - editingStarted?(): void; - /** - * When coming out of panel edit - */ - editingCompleted?(withChanges: boolean): void; -} - -export function isDashboardLayoutItem(obj: SceneObject): obj is DashboardLayoutItem { - return 'isDashboardLayoutItem' in obj; -} - -export interface DashboardRepeatsProcessedEventPayload { - source: SceneObject; -} - -export class DashboardRepeatsProcessedEvent extends BusEventWithPayload { - public static type = 'dashboard-repeats-processed'; -} - -/** - * Interface for elements that have options - */ -export interface EditableDashboardElement { - /** - * Marks this object as an element that can be selected and edited directly on the canvas - */ - isEditableDashboardElement: true; - /** - * Hook that returns edit pane options - */ - useEditPaneOptions(): OptionsPaneCategoryDescriptor[]; - /** - * Get the type name of the element - */ - getTypeName(): string; - /** - * Panel Actions - **/ - renderActions?(): React.ReactNode; - /** - * creates a new multi-selection element from a list of selected items - */ - createMultiSelectedElement?(items: SceneObject[]): MultiSelectedEditableDashboardElement; -} - -export function isEditableDashboardElement(obj: object): obj is EditableDashboardElement { - return 'isEditableDashboardElement' in obj; -} - -export interface MultiSelectedEditableDashboardElement { - /** - * Marks this object as an element that can be selected and edited directly on the canvas - */ - isMultiSelectedEditableDashboardElement: true; - /** - * Get the type name of the element - */ - getTypeName(): string; - /** - * Hook that returns edit pane options - */ - useEditPaneOptions?(): OptionsPaneCategoryDescriptor[]; - /** - * Panel Actions - **/ - renderActions?(): React.ReactNode; -} - -export function isMultiSelectedEditableDashboardElement(obj: object): obj is MultiSelectedEditableDashboardElement { - return 'isMultiSelectedEditableDashboardElement' in obj; -} - -export interface BulkActionElement { - onDelete(): void; - onCopy?(): void; -} - -export function isBulkActionElement(obj: object): obj is BulkActionElement { - return 'onDelete' in obj; -} diff --git a/public/app/features/dashboard-scene/scene/types/BulkActionElement.ts b/public/app/features/dashboard-scene/scene/types/BulkActionElement.ts new file mode 100644 index 00000000000..a654693d64d --- /dev/null +++ b/public/app/features/dashboard-scene/scene/types/BulkActionElement.ts @@ -0,0 +1,8 @@ +export interface BulkActionElement { + onDelete(): void; + onCopy?(): void; +} + +export function isBulkActionElement(obj: object): obj is BulkActionElement { + return 'onDelete' in obj; +} diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts new file mode 100644 index 00000000000..fef58b0d071 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts @@ -0,0 +1,32 @@ +import { SceneObject } from '@grafana/scenes'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; + +/** + * Abstraction to handle editing of different layout elements (wrappers for VizPanels and other objects) + * Also useful to when rendering / viewing an element outside it's layout scope + */ +export interface DashboardLayoutItem extends SceneObject { + /** + * Marks this object as a layout item + */ + isDashboardLayoutItem: true; + + /** + * Return layout item options (like repeat, repeat direction, etc. for the default DashboardGridItem) + */ + getOptions?(): OptionsPaneCategoryDescriptor; + + /** + * When going into panel edit + **/ + editingStarted?(): void; + + /** + * When coming out of panel edit + */ + editingCompleted?(withChanges: boolean): void; +} + +export function isDashboardLayoutItem(obj: SceneObject): obj is DashboardLayoutItem { + return 'isDashboardLayoutItem' in obj; +} diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts new file mode 100644 index 00000000000..572d7da9549 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts @@ -0,0 +1,82 @@ +import { SceneObject, VizPanel } from '@grafana/scenes'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; + +import { LayoutRegistryItem } from './LayoutRegistryItem'; + +/** + * A scene object that usually wraps an underlying layout + * Dealing with all the state management and editing of the layout + */ +export interface DashboardLayoutManager extends SceneObject { + /** Marks it as a DashboardLayoutManager */ + isDashboardLayoutManager: true; + + /** + * The layout descriptor (which has the name and id) + */ + descriptor: Readonly; + + /** + * Adds a new panel to the layout + */ + addPanel(panel: VizPanel): void; + + /** + * Remove an element / panel + * @param panel + */ + removePanel(panel: VizPanel): void; + + /** + * Creates a copy of an existing element and adds it to the layout + * @param panel + */ + duplicatePanel(panel: VizPanel): void; + + /** + * getVizPanels + */ + getVizPanels(): VizPanel[]; + + /** + * Returns the highest panel id in the layout + */ + getMaxPanelId(): number; + + /** + * Add row + */ + addNewRow(): void; + + /** + * Notify the layout manager that the edit mode has changed + * @param isEditing + */ + editModeChanged?(isEditing: boolean): void; + + /** + * Turn into a save model + */ + toSaveModel?(): S; + + /** + * For dynamic panels that need to be viewed in isolation (SoloRoute) + */ + activateRepeaters?(): void; + + /** + * Renders options and layout actions + */ + getOptions?(): OptionsPaneItemDescriptor[]; + + /** + * Create a clone of the layout manager given an ancestor key + * @param ancestorKey + * @param isSource + */ + cloneLayout?(ancestorKey: string, isSource: boolean): DashboardLayoutManager; +} + +export function isDashboardLayoutManager(obj: SceneObject): obj is DashboardLayoutManager { + return 'isDashboardLayoutManager' in obj; +} diff --git a/public/app/features/dashboard-scene/scene/types/DashboardRepeatsProcessedEvent.ts b/public/app/features/dashboard-scene/scene/types/DashboardRepeatsProcessedEvent.ts new file mode 100644 index 00000000000..9bf602cc171 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/types/DashboardRepeatsProcessedEvent.ts @@ -0,0 +1,10 @@ +import { BusEventWithPayload } from '@grafana/data'; +import { SceneObject } from '@grafana/scenes'; + +export interface DashboardRepeatsProcessedEventPayload { + source: SceneObject; +} + +export class DashboardRepeatsProcessedEvent extends BusEventWithPayload { + public static type = 'dashboard-repeats-processed'; +} diff --git a/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts b/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts new file mode 100644 index 00000000000..396b4f76431 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts @@ -0,0 +1,40 @@ +import { ReactNode } from 'react'; + +import { SceneObject } from '@grafana/scenes'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; + +import { MultiSelectedEditableDashboardElement } from './MultiSelectedEditableDashboardElement'; + +/** + * Interface for elements that have options + */ +export interface EditableDashboardElement { + /** + * Marks this object as an element that can be selected and edited directly on the canvas + */ + isEditableDashboardElement: true; + + /** + * Type name of the element + */ + typeName: Readonly; + + /** + * Hook that returns edit pane options + */ + useEditPaneOptions(): OptionsPaneCategoryDescriptor[]; + + /** + * Panel Actions + **/ + renderActions?(): ReactNode; + + /** + * creates a new multi-selection element from a list of selected items + */ + createMultiSelectedElement?(items: SceneObject[]): MultiSelectedEditableDashboardElement; +} + +export function isEditableDashboardElement(obj: object): obj is EditableDashboardElement { + return 'isEditableDashboardElement' in obj; +} diff --git a/public/app/features/dashboard-scene/scene/types/LayoutParent.ts b/public/app/features/dashboard-scene/scene/types/LayoutParent.ts new file mode 100644 index 00000000000..5d57ba0fc63 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/types/LayoutParent.ts @@ -0,0 +1,14 @@ +import { SceneObject } from '@grafana/scenes'; + +import { DashboardLayoutManager } from './DashboardLayoutManager'; + +/** + * This interface is needed to support layouts existing on different levels of the scene (DashboardScene and inside the TabsLayoutManager) + */ +export interface LayoutParent extends SceneObject { + switchLayout(newLayout: DashboardLayoutManager): void; +} + +export function isLayoutParent(obj: SceneObject): obj is LayoutParent { + return 'switchLayout' in obj; +} diff --git a/public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts b/public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts new file mode 100644 index 00000000000..f35b4c62d3c --- /dev/null +++ b/public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts @@ -0,0 +1,20 @@ +import { RegistryItem } from '@grafana/data'; + +import { DashboardLayoutManager } from './DashboardLayoutManager'; + +/** + * The layout descriptor used when selecting / switching layouts + */ +export interface LayoutRegistryItem extends RegistryItem { + /** + * When switching between layouts + * @param currentLayout + */ + createFromLayout(currentLayout: DashboardLayoutManager): DashboardLayoutManager; + + /** + * Create from persisted state + * @param saveModel + */ + createFromSaveModel?(saveModel: S): void; +} diff --git a/public/app/features/dashboard-scene/scene/types/MultiSelectedEditableDashboardElement.ts b/public/app/features/dashboard-scene/scene/types/MultiSelectedEditableDashboardElement.ts new file mode 100644 index 00000000000..bf956261f95 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/types/MultiSelectedEditableDashboardElement.ts @@ -0,0 +1,29 @@ +import { ReactNode } from 'react'; + +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; + +export interface MultiSelectedEditableDashboardElement { + /** + * Marks this object as an element that can be selected and edited directly on the canvas + */ + isMultiSelectedEditableDashboardElement: true; + + /** + * Type name of the element + */ + typeName: Readonly; + + /** + * Hook that returns edit pane options + */ + useEditPaneOptions?(): OptionsPaneCategoryDescriptor[]; + + /** + * Panel Actions + **/ + renderActions?(): ReactNode; +} + +export function isMultiSelectedEditableDashboardElement(obj: object): obj is MultiSelectedEditableDashboardElement { + return 'isMultiSelectedEditableDashboardElement' in obj; +} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 64df731435d..7be999edebc 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -33,7 +33,7 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { DashboardLayoutManager } from '../scene/types'; +import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getQueryRunnerFor } from '../utils/utils'; import { validateVariable, validateVizPanel } from '../v2schema/test-helpers'; diff --git a/public/app/features/dashboard-scene/solo/useSoloPanel.ts b/public/app/features/dashboard-scene/solo/useSoloPanel.ts index 3db708c0ae9..acc7409bdc3 100644 --- a/public/app/features/dashboard-scene/solo/useSoloPanel.ts +++ b/public/app/features/dashboard-scene/solo/useSoloPanel.ts @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import { VizPanel, UrlSyncManager } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; -import { DashboardRepeatsProcessedEvent } from '../scene/types'; +import { DashboardRepeatsProcessedEvent } from '../scene/types/DashboardRepeatsProcessedEvent'; import { containsCloneKey } from '../utils/clone'; import { findVizPanelByKey } from '../utils/utils'; diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 64df007a931..27ecaac2fe2 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -19,7 +19,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; -import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types'; +import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; import { getLastKeyFromClone, getOriginalKey } from './clone'; From 39a6d2e586f7c2fe7e6b55515ba17134bcc197ba Mon Sep 17 00:00:00 2001 From: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> Date: Wed, 5 Feb 2025 06:09:16 -0500 Subject: [PATCH 350/894] ExploreMetrics: Use redirect to route traffic to metrics drilldown app (#100098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: drilldown app redirect * chore: make copilot review happy, I guess 🤷 --- .../features/trails/RedirectToDrilldownApp.tsx | 17 +++++++++++++++++ public/app/routes/routes.tsx | 7 +++++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 public/app/features/trails/RedirectToDrilldownApp.tsx diff --git a/public/app/features/trails/RedirectToDrilldownApp.tsx b/public/app/features/trails/RedirectToDrilldownApp.tsx new file mode 100644 index 00000000000..0a4ff04e8d7 --- /dev/null +++ b/public/app/features/trails/RedirectToDrilldownApp.tsx @@ -0,0 +1,17 @@ +import { Navigate, useLocation, useParams } from 'react-router-dom-v5-compat'; + +import { getRouteForAppPlugin } from 'app/features/plugins/routes'; + +/** + * Navigate to the drilldown app with the remaining path parameters and search params + */ +const RedirectToDrilldownApp = () => { + const { '*': remainingPath } = useParams(); + const location = useLocation(); + const appPath = getRouteForAppPlugin('grafana-metricsdrilldown-app').path.replaceAll('*', ''); + const newPath = `${appPath}${remainingPath}${location.search}`; + + return ; +}; + +export default RedirectToDrilldownApp; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index ddadc2f40b7..839eb62ad85 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -15,7 +15,7 @@ import { getRoutes as getDataConnectionsRoutes } from 'app/features/connections/ import { DATASOURCES_ROUTES } from 'app/features/datasources/constants'; import { ConfigureIRM } from 'app/features/gops/configuration-tracker/components/ConfigureIRM'; import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes'; -import { getAppPluginRoutes, getRouteForAppPlugin } from 'app/features/plugins/routes'; +import { getAppPluginRoutes } from 'app/features/plugins/routes'; import { getProfileRoutes } from 'app/features/profile/routes'; import { AccessControlAction, DashboardRoutes } from 'app/types'; @@ -518,7 +518,10 @@ export function getAppRoutes(): RouteDescriptor[] { roles: () => contextSrv.evaluatePermission([AccessControlAction.DataSourcesExplore]), ...(config.featureToggles.exploreMetricsUseExternalAppPlugin ? { - component: getRouteForAppPlugin('grafana-metricsdrilldown-app').component, + component: SafeDynamicImport( + () => + import(/* webpackChunkName: "MetricsDrilldownRedirect"*/ 'app/features/trails/RedirectToDrilldownApp') + ), } : { chromeless: false, From c4d01143761ed3323ca62bcb3ab6dbb49f8d9fcf Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Wed, 5 Feb 2025 13:14:03 +0200 Subject: [PATCH 351/894] Dashboards: Use i18n for texts in dynamic dashboards (#100108) --- .betterer.results | 38 ------ .../edit-pane/DashboardEditPane.tsx | 7 +- .../edit-pane/DashboardEditableElement.tsx | 7 +- .../edit-pane/VizPanelEditableElement.tsx | 14 +-- .../scene/NavToolbarActions.tsx | 98 ++++++++------- .../DashboardGridItemEditor.tsx | 19 +-- .../DefaultGridLayoutManager.tsx | 9 +- .../ResponsiveGridItem.tsx | 5 +- .../ResponsiveGridLayoutManager.tsx | 34 ++++-- .../scene/layout-rows/RowItem.tsx | 28 +++-- .../scene/layout-rows/RowsLayoutManager.tsx | 15 ++- public/locales/en-US/grafana.json | 112 +++++++++++++++++- public/locales/pseudo-LOCALE/grafana.json | 112 +++++++++++++++++- 13 files changed, 361 insertions(+), 137 deletions(-) diff --git a/.betterer.results b/.betterer.results index 629774cfa72..c9ce73693f2 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3212,10 +3212,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/dashboard-scene/edit-pane/DashboardEditPane.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"] - ], "public/app/features/dashboard-scene/embedding/EmbeddedDashboardTestPage.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], @@ -3389,40 +3385,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx:5381": [ [0, 0, 0, "\'@grafana/data/src/text/sanitize\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] ], - "public/app/features/dashboard-scene/scene/NavToolbarActions.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"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "13"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "14"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "15"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "16"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "17"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "18"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "19"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "20"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "21"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "22"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "23"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "24"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "25"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "26"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "27"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "28"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "29"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "30"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "31"] - ], "public/app/features/dashboard-scene/scene/PanelLinks.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index 497b45bf109..35f2ce7123b 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { SceneObjectState, SceneObjectBase, SceneObject, sceneGraph, useSceneObjectState } from '@grafana/scenes'; import { ElementSelectionContextItem, ElementSelectionContextState, ToolbarButton, useStyles2 } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; import { isInCloneChain } from '../utils/clone'; import { getDashboardSceneFor } from '../utils/utils'; @@ -165,12 +166,12 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla <>
diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index 1e458430481..4699e8c1c3f 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { Input, TextArea } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; @@ -22,13 +23,13 @@ export class DashboardEditableElement implements EditableDashboardElement { const dashboardOptions = useMemo(() => { return new OptionsPaneCategoryDescriptor({ - title: 'Dashboard options', + title: t('dashboard.options.title', 'Dashboard options'), id: 'dashboard-options', isOpenDefault: true, }) .addItem( new OptionsPaneItemDescriptor({ - title: 'Title', + title: t('dashboard.options.title-option', 'Title'), render: function renderTitle() { return ; }, @@ -36,7 +37,7 @@ export class DashboardEditableElement implements EditableDashboardElement { ) .addItem( new OptionsPaneItemDescriptor({ - title: 'Description', + title: t('dashboard.options.description', 'Description'), render: function renderTitle() { return ; }, diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx index 762dcb5f745..732ac9df825 100644 --- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx @@ -1,8 +1,8 @@ -import { useMemo } from 'react'; +import { ReactNode, useMemo } from 'react'; import { sceneGraph, VizPanel } from '@grafana/scenes'; import { Button } from '@grafana/ui'; -import { Trans } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions'; @@ -29,13 +29,13 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc const panelOptions = useMemo(() => { return new OptionsPaneCategoryDescriptor({ - title: 'Panel options', + title: t('dashboard.viz-panel.options.title', 'Panel options'), id: 'panel-options', isOpenDefault: true, }) .addItem( new OptionsPaneItemDescriptor({ - title: 'Title', + title: t('dashboard.viz-panel.options.title-option', 'Title'), value: panel.state.title, popularRank: 1, render: function renderTitle() { @@ -45,7 +45,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc ) .addItem( new OptionsPaneItemDescriptor({ - title: 'Description', + title: t('dashboard.viz-panel.options.description', 'Description'), value: panel.state.description, render: function renderDescription() { return ; @@ -54,7 +54,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc ) .addItem( new OptionsPaneItemDescriptor({ - title: 'Transparent background', + title: t('dashboard.viz-panel.options.transparent-background', 'Transparent background'), render: function renderTransparent() { return ; }, @@ -104,7 +104,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc layout.removePanel(this.panel); }; - public renderActions(): React.ReactNode { + public renderActions(): ReactNode { return ( <> ), }); @@ -191,7 +190,7 @@ export function ToolbarActions({ dashboard }: Props) { }} data-testid={selectors.components.PageToolbar.itemButton('add_row')} > - Row + Row ), }); @@ -211,7 +210,7 @@ export function ToolbarActions({ dashboard }: Props) { DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' }); }} > - Import + Import ), }); @@ -363,7 +362,7 @@ export function ToolbarActions({ dashboard }: Props) { icon="arrow-left" data-testid={selectors.components.NavToolbar.editDashboard.backToDashboardButton} > - Back to dashboard + Back to dashboard ), }); @@ -384,7 +383,7 @@ export function ToolbarActions({ dashboard }: Props) { icon="arrow-left" data-testid={selectors.components.NavToolbar.editDashboard.backToDashboardButton} > - Back to dashboard + Back to dashboard ), }); @@ -396,7 +395,7 @@ export function ToolbarActions({ dashboard }: Props) { render: () => ( ), }); @@ -419,14 +418,14 @@ export function ToolbarActions({ dashboard }: Props) { onClick={() => { dashboard.onEnterEditMode(); }} - tooltip="Enter edit mode" + tooltip={t('dashboard.toolbar.edit.tooltip', 'Enter edit mode')} key="edit" className={styles.buttonWithExtraMargin} variant={config.featureToggles.newDashboardSharingComponent ? 'secondary' : 'primary'} size="sm" data-testid={selectors.components.NavToolbar.editDashboard.editButton} > - Edit + Edit ), }); @@ -440,14 +439,14 @@ export function ToolbarActions({ dashboard }: Props) { dashboard.onEnterEditMode(); dashboard.setState({ editable: true, meta: { ...meta, canEdit: true } }); }} - tooltip="This dashboard was marked as read only" + tooltip={t('dashboard.toolbar.enter-edit-mode.tooltip', 'This dashboard was marked as read only')} key="edit" className={styles.buttonWithExtraMargin} variant="secondary" size="sm" data-testid={selectors.components.NavToolbar.editDashboard.editButton} > - Make editable + Make editable ), }); @@ -472,14 +471,14 @@ export function ToolbarActions({ dashboard }: Props) { onClick={() => { dashboard.onOpenSettings(); }} - tooltip="Dashboard settings" + tooltip={t('dashboard.toolbar.dashboard-settings.tooltip', 'Dashboard settings')} fill="text" size="sm" key="settings" variant="secondary" data-testid={selectors.components.NavToolbar.editDashboard.settingsButton} > - Settings + Settings ), }); @@ -490,14 +489,14 @@ export function ToolbarActions({ dashboard }: Props) { render: () => ( ), }); @@ -508,7 +507,11 @@ export function ToolbarActions({ dashboard }: Props) { render: () => ( ), }); @@ -527,14 +534,14 @@ export function ToolbarActions({ dashboard }: Props) { render: () => ( ), }); @@ -545,14 +552,14 @@ export function ToolbarActions({ dashboard }: Props) { render: () => ( ), }); @@ -563,14 +570,14 @@ export function ToolbarActions({ dashboard }: Props) { render: () => ( ), }); @@ -587,13 +594,13 @@ export function ToolbarActions({ dashboard }: Props) { dashboard.openSaveDrawer({}); }} className={styles.buttonWithExtraMargin} - tooltip="Save changes" + tooltip={t('dashboard.toolbar.save-dashboard.tooltip', 'Save changes')} key="save" size="sm" - variant={'primary'} + variant="primary" data-testid={selectors.components.NavToolbar.editDashboard.saveButton} > - Save dashboard + Save dashboard ); } @@ -606,12 +613,12 @@ export function ToolbarActions({ dashboard }: Props) { dashboard.openSaveDrawer({ saveAsCopy: true }); }} className={styles.buttonWithExtraMargin} - tooltip="Save as copy" + tooltip={t('dashboard.toolbar.save-dashboard-copy.tooltip', 'Save as copy')} key="save" size="sm" variant={isDirty ? 'primary' : 'secondary'} > - Save as copy + Save as copy ); } @@ -620,14 +627,14 @@ export function ToolbarActions({ dashboard }: Props) { const menu = ( { dashboard.openSaveDrawer({}); }} /> { dashboard.openSaveDrawer({ saveAsCopy: true }); @@ -642,16 +649,16 @@ export function ToolbarActions({ dashboard }: Props) { onClick={() => { dashboard.openSaveDrawer({}); }} - tooltip="Save changes" + tooltip={t('dashboard.toolbar.save-dashboard.tooltip', 'Save changes')} size="sm" data-testid={selectors.components.NavToolbar.editDashboard.saveButton} variant={isDirty ? 'primary' : 'secondary'} > - Save dashboard + Save dashboard +
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + const baseStyles = getInputStyles({ theme }); + + return { + wrapper: baseStyles.wrapper, + inputWrapper: baseStyles.inputWrapper, + fakeInput: css([ + baseStyles.input, + { + textAlign: 'left', + }, + ]), + }; +}; diff --git a/public/app/core/components/NestedFolderPicker/Trigger.tsx b/public/app/core/components/NestedFolderPicker/Trigger.tsx index 942beab6e96..fe352b806d3 100644 --- a/public/app/core/components/NestedFolderPicker/Trigger.tsx +++ b/public/app/core/components/NestedFolderPicker/Trigger.tsx @@ -1,13 +1,14 @@ import { css, cx } from '@emotion/css'; import { forwardRef, ReactNode, ButtonHTMLAttributes } from 'react'; import * as React from 'react'; -import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2 } from '@grafana/data'; import { Icon, getInputStyles, useTheme2, Text } from '@grafana/ui'; import { getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins'; import { Trans, t } from 'app/core/internationalization'; +import { FolderPickerSkeleton } from './Skeleton'; + interface TriggerProps extends ButtonHTMLAttributes { isLoading: boolean; handleClearSelection?: (event: React.MouseEvent | React.KeyboardEvent) => void; @@ -28,6 +29,10 @@ function Trigger( } }; + if (isLoading) { + return ; + } + return (
@@ -43,9 +48,7 @@ function Trigger( {...rest} ref={ref} > - {isLoading ? ( - - ) : label ? ( + {label ? ( {label} ) : ( diff --git a/public/app/plugins/panel/dashlist/module.tsx b/public/app/plugins/panel/dashlist/module.tsx index 8cb3c286e16..a4033ebc7dc 100644 --- a/public/app/plugins/panel/dashlist/module.tsx +++ b/public/app/plugins/panel/dashlist/module.tsx @@ -1,7 +1,6 @@ import { PanelPlugin } from '@grafana/data'; import { TagsInput } from '@grafana/ui'; import { FolderPicker } from 'app/core/components/Select/FolderPicker'; -import { PermissionLevelString } from 'app/types'; import { DashList } from './DashList'; import { dashlistMigrationHandler } from './migrations'; @@ -62,12 +61,7 @@ export const plugin = new PanelPlugin(DashList) defaultValue: undefined, editor: function RenderFolderPicker({ value, onChange }) { return ( - onChange(folderUID)} - /> + onChange(folderUID)} /> ); }, }) diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index b939d67c726..684a3a158b1 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -7,6 +7,9 @@ export enum TeamPermissionLevel { export { OrgRole as OrgRole }; +export type PermissionLevel = 'view' | 'edit' | 'admin'; + +/** @deprecated Use PermissionLevel instead */ export enum PermissionLevelString { View = 'View', Edit = 'Edit', From a93664ff3d189eb436a8fb449876d9dcabd3cf2d Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Wed, 5 Feb 2025 11:12:30 -0500 Subject: [PATCH 360/894] Alerting: Add EmbeddedContents as alternative embedding in smtp (#99937) Adds support for embedding []byte in SmtpClient instead of filenames. This is backwards compatible as it uses a new field EmbeddedContents to add an alternative to the existing EmbeddedFiles which takes filenames. --- pkg/services/notifications/email.go | 25 ++++++++++++------- pkg/services/notifications/mailer.go | 17 +++++++------ pkg/services/notifications/models.go | 19 +++++++------- pkg/services/notifications/notifications.go | 19 +++++++------- .../notifications/notifications_test.go | 25 +++++++++++++++++++ pkg/services/notifications/smtp.go | 7 ++++++ 6 files changed, 77 insertions(+), 35 deletions(-) diff --git a/pkg/services/notifications/email.go b/pkg/services/notifications/email.go index e50646b9662..c60adf605d5 100644 --- a/pkg/services/notifications/email.go +++ b/pkg/services/notifications/email.go @@ -11,17 +11,24 @@ type AttachedFile struct { Content []byte } +// EmbeddedContent struct represents an embedded file. +type EmbeddedContent struct { + Name string + Content []byte +} + // Message is representation of the email message. type Message struct { - To []string - SingleEmail bool - From string - Subject string - Body map[string]string - Info string - ReplyTo []string - EmbeddedFiles []string - AttachedFiles []*AttachedFile + To []string + SingleEmail bool + From string + Subject string + Body map[string]string + Info string + ReplyTo []string + EmbeddedFiles []string + EmbeddedContents []EmbeddedContent + AttachedFiles []*AttachedFile } func setDefaultTemplateData(cfg *setting.Cfg, data map[string]any, u *user.User) { diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go index 7382454751f..eceaef3caf4 100644 --- a/pkg/services/notifications/mailer.go +++ b/pkg/services/notifications/mailer.go @@ -112,14 +112,15 @@ func (ns *NotificationService) buildEmailMessage(cmd *SendEmailCommand) (*Messag addr := mail.Address{Name: ns.Cfg.Smtp.FromName, Address: ns.Cfg.Smtp.FromAddress} return &Message{ - To: cmd.To, - SingleEmail: cmd.SingleEmail, - From: addr.String(), - Subject: subject, - Body: body, - EmbeddedFiles: cmd.EmbeddedFiles, - AttachedFiles: buildAttachedFiles(cmd.AttachedFiles), - ReplyTo: cmd.ReplyTo, + To: cmd.To, + SingleEmail: cmd.SingleEmail, + From: addr.String(), + Subject: subject, + Body: body, + EmbeddedFiles: cmd.EmbeddedFiles, + EmbeddedContents: cmd.EmbeddedContents, + AttachedFiles: buildAttachedFiles(cmd.AttachedFiles), + ReplyTo: cmd.ReplyTo, }, nil } diff --git a/pkg/services/notifications/models.go b/pkg/services/notifications/models.go index 364c6e6d262..6f5b1419ea5 100644 --- a/pkg/services/notifications/models.go +++ b/pkg/services/notifications/models.go @@ -18,15 +18,16 @@ type SendEmailAttachFile struct { // SendEmailCommand is the command for sending emails type SendEmailCommand struct { - To []string - SingleEmail bool - Template string - Subject string - Data map[string]any - Info string - ReplyTo []string - EmbeddedFiles []string - AttachedFiles []*SendEmailAttachFile + To []string + SingleEmail bool + Template string + Subject string + Data map[string]any + Info string + ReplyTo []string + EmbeddedFiles []string + EmbeddedContents []EmbeddedContent + AttachedFiles []*SendEmailAttachFile } // SendEmailCommandSync is the command for sending emails synchronously diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index 2be85628090..dc453874d14 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -201,15 +201,16 @@ func __dangerouslyInjectHTML(s string) template.HTML { func (ns *NotificationService) SendEmailCommandHandlerSync(ctx context.Context, cmd *SendEmailCommandSync) error { message, err := ns.buildEmailMessage(&SendEmailCommand{ - Data: cmd.Data, - Info: cmd.Info, - Template: cmd.Template, - To: cmd.To, - SingleEmail: cmd.SingleEmail, - EmbeddedFiles: cmd.EmbeddedFiles, - AttachedFiles: cmd.AttachedFiles, - Subject: cmd.Subject, - ReplyTo: cmd.ReplyTo, + Data: cmd.Data, + Info: cmd.Info, + Template: cmd.Template, + To: cmd.To, + SingleEmail: cmd.SingleEmail, + EmbeddedFiles: cmd.EmbeddedFiles, + EmbeddedContents: cmd.EmbeddedContents, + AttachedFiles: cmd.AttachedFiles, + Subject: cmd.Subject, + ReplyTo: cmd.ReplyTo, }) if err != nil { return err diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go index 47c1f56ccb8..b1b3bdfdcaa 100644 --- a/pkg/services/notifications/notifications_test.go +++ b/pkg/services/notifications/notifications_test.go @@ -132,6 +132,31 @@ func TestSendEmailSync(t *testing.T) { require.Equal(t, []byte("text file content"), file.Content) }) + t.Run("When embedding readers to emails", func(t *testing.T) { + ns, mailer := createSut(t, bus) + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ + Subject: "subject", + To: []string{"asdf@grafana.com"}, + SingleEmail: true, + Template: "welcome_on_signup", + EmbeddedContents: []EmbeddedContent{ + {Name: "embed.jpg", Content: []byte("image content")}, + }, + }, + } + + err := ns.SendEmailCommandHandlerSync(context.Background(), cmd) + require.NoError(t, err) + + require.NotEmpty(t, mailer.Sent) + sent := mailer.Sent[len(mailer.Sent)-1] + require.Len(t, sent.EmbeddedContents, 1) + f := sent.EmbeddedContents[0] + require.Equal(t, "embed.jpg", f.Name) + require.Equal(t, "image content", string(f.Content)) + }) + t.Run("When SMTP disabled in configuration", func(t *testing.T) { cfg := createSmtpConfig() cfg.Smtp.Enabled = false diff --git a/pkg/services/notifications/smtp.go b/pkg/services/notifications/smtp.go index 9ac0ecff570..c5eea8a958c 100644 --- a/pkg/services/notifications/smtp.go +++ b/pkg/services/notifications/smtp.go @@ -142,6 +142,13 @@ func (sc *SmtpClient) setFiles( m.Embed(file) } + for _, file := range msg.EmbeddedContents { + m.Embed(file.Name, gomail.SetCopyFunc(func(writer io.Writer) error { + _, err := writer.Write(file.Content) + return err + })) + } + for _, file := range msg.AttachedFiles { file := file m.Attach(file.Name, gomail.SetCopyFunc(func(writer io.Writer) error { From 130d268b51df3265891c7ea305f975ba8815725a Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Wed, 5 Feb 2025 22:47:08 +0600 Subject: [PATCH 361/894] Dashboards: Update `POST /api/dashboards/db` docs (#99363) update dashboard update docs --- docs/sources/developers/http_api/dashboard.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/sources/developers/http_api/dashboard.md b/docs/sources/developers/http_api/dashboard.md index d37b0e080c9..fe08dc99f15 100644 --- a/docs/sources/developers/http_api/dashboard.md +++ b/docs/sources/developers/http_api/dashboard.md @@ -139,7 +139,6 @@ The **412** status code is used for explaining that you cannot create the dashbo There can be different reasons for this: - The dashboard has been changed by someone else, `status=version-mismatch` -- A dashboard with the same name in the folder already exists, `status=name-exists` - A dashboard with the same uid already exists, `status=name-exists` - The dashboard belongs to plugin ``, `status=plugin-dashboard` @@ -156,8 +155,6 @@ Content-Length: 97 } ``` -In case of title already exists the `status` property will be `name-exists`. - ## Get dashboard by uid `GET /api/dashboards/uid/:uid` From 92bb51bddacbbac5210e4ad8c959531ca5ec49c1 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:08:50 -0500 Subject: [PATCH 362/894] Docs: move missing ref URI to correct page (#100131) --- docs/sources/dashboards/create-reports/_index.md | 5 ----- .../dashboards/create-reports/report-settings/index.md | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/sources/dashboards/create-reports/_index.md b/docs/sources/dashboards/create-reports/_index.md index 12218600293..0b1de37d290 100644 --- a/docs/sources/dashboards/create-reports/_index.md +++ b/docs/sources/dashboards/create-reports/_index.md @@ -19,11 +19,6 @@ title: Create and manage reports description: Generate and share PDF reports from your Grafana dashboards weight: 600 refs: - change-ui-theme: - - pattern: /docs/grafana/ - destination: /docs/grafana//administration/organization-preferences/#change-grafana-ui-theme - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//administration/organization-preferences/#change-grafana-ui-theme grafana-enterprise: - pattern: /docs/grafana/ destination: /docs/grafana//introduction/grafana-enterprise/ diff --git a/docs/sources/dashboards/create-reports/report-settings/index.md b/docs/sources/dashboards/create-reports/report-settings/index.md index e0c606a8f65..b878953b71a 100644 --- a/docs/sources/dashboards/create-reports/report-settings/index.md +++ b/docs/sources/dashboards/create-reports/report-settings/index.md @@ -11,6 +11,12 @@ menuTitle: Settings title: Reporting settings description: Manage organizational Reporting settings weight: 700 +refs: + change-ui-theme: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/organization-preferences/#change-grafana-ui-theme + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//administration/organization-preferences/#change-grafana-ui-theme --- # Reporting settings From d58dec79511964034c22846dc15d292728168d64 Mon Sep 17 00:00:00 2001 From: colin-stuart Date: Wed, 5 Feb 2025 12:58:14 -0500 Subject: [PATCH 363/894] Docs: Add docs for Passwordless Authentication Using Magic Links (#96877) * Docs: Add docs for Passwordless Authentication Using Magic Links * Update docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md Co-authored-by: Misi * Update docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md Co-authored-by: Misi * Update docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md Co-authored-by: Misi * match Writer's Toolkit style * Update docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md Co-authored-by: Jack Baldry --------- Co-authored-by: Misi Co-authored-by: Jack Baldry --- .../configure-authentication/_index.md | 1 + .../passwordless/index.md | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md index 1c6b0ce9256..3e20606bb1e 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md @@ -25,6 +25,7 @@ The following table shows all supported authentication methods and the features | [Auth Proxy]({{< relref "./auth-proxy" >}}) | no | yes | yes | no | yes | no | N/A | no | N/A | N/A | | [Azure AD OAuth]({{< relref "./azuread" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | | [Basic auth]({{< relref "./grafana" >}}) | yes | N/A | yes | yes | N/A | N/A | N/A | N/A | N/A | N/A | +| [Passwordless auth]({{< relref "./passwordless" >}}) | yes | N/A | yes | yes | N/A | N/A | N/A | N/A | N/A | N/A | | [Generic OAuth]({{< relref "./generic-oauth" >}}) | yes | yes | yes | yes | yes | no | N/A | yes | yes | yes | | [GitHub OAuth]({{< relref "./github" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | | [GitLab OAuth]({{< relref "./gitlab" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md new file mode 100644 index 00000000000..06120e6c7da --- /dev/null +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md @@ -0,0 +1,46 @@ +--- +description: Learn how to configure passwordless authentication with magic links in Grafana +labels: + products: + - enterprise + - oss +menuTitle: Passwordless +title: Configure passwordless authentication with magic links +weight: 200 +--- + +# Configure passwordless authentication with magic links + +Passwordless authentication lets Grafana users authenticate with a magic link or one-time password (OTP) sent via email. + +## Enable passwordless authentication + +{{< docs/experimental product="passwordless authentication" featureFlag="passwordlessMagicLinkAuthentication" >}} + +To enable passwordless authentication, use the following configuration: + +```bash +[auth.passwordless] +enabled = true +``` + +## Code expiration + +By default, the one-time password (OTP) sent to a user's email is valid for 20 minutes. Use the `code_expiration` option to change the duration that the OTP is valid. + +```bash +[auth.passwordless] +enabled = true +code_expiration = 20m +``` + +## Enable SMTP server + +The SMTP server must be enabled so that Grafana can send emails. +The following configuration enables the SMTP server. +For more information on configuring the SMTP server, refer to [SMTP](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#smtp). + +```bash +[smtp] +enabled = true +``` From 6200361f366465069b309657a64a02274a8ba157 Mon Sep 17 00:00:00 2001 From: colin-stuart Date: Wed, 5 Feb 2025 13:16:36 -0500 Subject: [PATCH 364/894] Auth: Add IP address login attempt validation (#98123) * Auth: Add IP address login attempt validation * LoginAttempt struct IpAddress field must be camelCase to match db ip_address column * add setting DisableIPAddressLoginProtection * lint * add DisableIPAddressLoginProtection setting to tests * add request object to authenticate password test * nit suggestions & rename tests * add login attempt on failed password authentication * dont need to reset login attempts if successful * don't change error message * revert go.work.sum * Update pkg/services/authn/clients/password.go Co-authored-by: Misi --------- Co-authored-by: Misi --- conf/defaults.ini | 3 + conf/sample.ini | 3 + .../setup-grafana/configure-grafana/_index.md | 4 + pkg/services/authn/clients/password.go | 13 +- pkg/services/authn/clients/password_test.go | 26 ++-- pkg/services/authn/clients/passwordless.go | 10 +- pkg/services/loginattempt/login_attempt.go | 5 +- .../loginattemptimpl/login_attempt.go | 24 +++- .../loginattemptimpl/login_attempt_test.go | 114 ++++++++++++++++-- .../loginattempt/loginattemptimpl/models.go | 7 +- .../loginattempt/loginattemptimpl/store.go | 19 ++- .../loginattemptimpl/store_test.go | 12 +- .../loginattempt/loginattempttest/fake.go | 4 + .../loginattempt/loginattempttest/mock.go | 7 +- pkg/setting/setting.go | 2 + 15 files changed, 224 insertions(+), 29 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 9fa38434c95..e8e0a152dc8 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -354,6 +354,9 @@ disable_brute_force_login_protection = false # max number of failed login attempts before user gets locked brute_force_login_protection_max_attempts = 5 +# disable protection against brute force login attempts by IP address +disable_ip_address_login_protection = true + # set to true if you host Grafana behind HTTPS. default is false. cookie_secure = false diff --git a/conf/sample.ini b/conf/sample.ini index 985fb7034b6..9834c647f5a 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -353,6 +353,9 @@ # max number of failed login attempts before user gets locked ;brute_force_login_protection_max_attempts = 5 +# disable protection against brute force login attempts by IP address +; disable_ip_address_login_protection = true + # set to true if you host Grafana behind HTTPS. default is false. ;cookie_secure = false diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 5fae19f203e..4805b42a1a7 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -700,6 +700,10 @@ An existing user's account is unable to login for five minutes if all login atte Configure how many login attempts a user can have within a five minute window before their account is locked. Default is `5`. +#### `disable_ip_address_login_protection` + +Set to `true` to disable [brute force login protection by IP address](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout). Default is `true`. Anyone from the IP address will be unable to login for 5 minutes if all login attempts are spent within a 5 minute window. + #### `cookie_secure` Set to `true` if you host Grafana behind HTTPS. Default is `false`. diff --git a/pkg/services/authn/clients/password.go b/pkg/services/authn/clients/password.go index a44f8615086..76e82d4fc59 100644 --- a/pkg/services/authn/clients/password.go +++ b/pkg/services/authn/clients/password.go @@ -39,6 +39,14 @@ func (c *Password) AuthenticatePassword(ctx context.Context, r *authn.Request, u return nil, errPasswordAuthFailed.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked") } + ok, err = c.loginAttempts.ValidateIPAddress(ctx, web.RemoteAddr(r.HTTPRequest)) + if err != nil { + return nil, err + } + if !ok { + return nil, errPasswordlessClientTooManyLoginAttempts.Errorf("too many consecutive incorrect login attempts for IP address - login for IP address temporarily blocked") + } + if len(password) == 0 { return nil, errPasswordAuthFailed.Errorf("no password provided") } @@ -56,8 +64,9 @@ func (c *Password) AuthenticatePassword(ctx context.Context, r *authn.Request, u return identity, nil } - if errors.Is(clientErrs, errInvalidPassword) { - _ = c.loginAttempts.Add(ctx, username, web.RemoteAddr(r.HTTPRequest)) + err = c.loginAttempts.Add(ctx, username, web.RemoteAddr(r.HTTPRequest)) + if err != nil { + return nil, err } return nil, errPasswordAuthFailed.Errorf("failed to authenticate identity: %w", clientErrs) diff --git a/pkg/services/authn/clients/password_test.go b/pkg/services/authn/clients/password_test.go index c981cfd4b01..6f7619ce1b7 100644 --- a/pkg/services/authn/clients/password_test.go +++ b/pkg/services/authn/clients/password_test.go @@ -2,6 +2,8 @@ package clients import ( "context" + "net/http" + "net/url" "testing" "github.com/stretchr/testify/assert" @@ -18,7 +20,6 @@ func TestPassword_AuthenticatePassword(t *testing.T) { desc string username string password string - req *authn.Request blockLogin bool clients []authn.PasswordClient expectedErr error @@ -30,7 +31,6 @@ func TestPassword_AuthenticatePassword(t *testing.T) { desc: "should success when password client return identity", username: "test", password: "test", - req: &authn.Request{}, clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "1", Type: claims.TypeUser}}}, expectedIdentity: &authn.Identity{ID: "1", Type: claims.TypeUser}, }, @@ -38,7 +38,6 @@ func TestPassword_AuthenticatePassword(t *testing.T) { desc: "should success when found in second client", username: "test", password: "test", - req: &authn.Request{}, clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "2", Type: claims.TypeUser}}}, expectedIdentity: &authn.Identity{ID: "2", Type: claims.TypeUser}, }, @@ -46,14 +45,12 @@ func TestPassword_AuthenticatePassword(t *testing.T) { desc: "should fail for empty password", username: "test", password: "", - req: &authn.Request{}, expectedErr: errPasswordAuthFailed, }, { desc: "should if login is blocked by to many attempts", username: "test", password: "test", - req: &authn.Request{}, blockLogin: true, expectedErr: errPasswordAuthFailed, }, @@ -61,7 +58,6 @@ func TestPassword_AuthenticatePassword(t *testing.T) { desc: "should fail when not found in any clients", username: "test", password: "test", - req: &authn.Request{}, clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}}, expectedErr: errPasswordAuthFailed, }, @@ -70,8 +66,22 @@ func TestPassword_AuthenticatePassword(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { c := ProvidePassword(loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}, tt.clients...) - - identity, err := c.AuthenticatePassword(context.Background(), tt.req, tt.username, tt.password) + r := &authn.Request{ + OrgID: 12345, + HTTPRequest: &http.Request{ + Method: "GET", + URL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/api/v1/resource", + }, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + "User-Agent": []string{"MyApp/1.0"}, + }, + }, + } + identity, err := c.AuthenticatePassword(context.Background(), r, tt.username, tt.password) if tt.expectedErr != nil { assert.ErrorIs(t, err, tt.expectedErr) assert.Nil(t, identity) diff --git a/pkg/services/authn/clients/passwordless.go b/pkg/services/authn/clients/passwordless.go index cda17035e81..3ee20ff055f 100644 --- a/pkg/services/authn/clients/passwordless.go +++ b/pkg/services/authn/clients/passwordless.go @@ -105,7 +105,6 @@ func (c *Passwordless) RedirectURL(ctx context.Context, r *authn.Request) (*auth return nil, err } - // TODO: add IP address validation ok, err := c.loginAttempts.Validate(ctx, form.Email) if err != nil { return nil, err @@ -115,6 +114,15 @@ func (c *Passwordless) RedirectURL(ctx context.Context, r *authn.Request) (*auth return nil, errPasswordlessClientTooManyLoginAttempts.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked") } + ok, err = c.loginAttempts.ValidateIPAddress(ctx, web.RemoteAddr(r.HTTPRequest)) + if err != nil { + return nil, err + } + + if !ok { + return nil, errPasswordlessClientTooManyLoginAttempts.Errorf("too many consecutive incorrect login attempts for IP address - login for IP address temporarily blocked") + } + err = c.loginAttempts.Add(ctx, form.Email, web.RemoteAddr(r.HTTPRequest)) if err != nil { return nil, err diff --git a/pkg/services/loginattempt/login_attempt.go b/pkg/services/loginattempt/login_attempt.go index ce3233f0f3b..7758e6716d4 100644 --- a/pkg/services/loginattempt/login_attempt.go +++ b/pkg/services/loginattempt/login_attempt.go @@ -6,10 +6,13 @@ import ( type Service interface { // Add adds a new login attempt record for provided username - Add(ctx context.Context, username, IPAddress string) error + Add(ctx context.Context, username, ipAddress string) error // Validate checks if username has to many login attempts inside a window. // Will return true if provided username do not have too many attempts. Validate(ctx context.Context, username string) (bool, error) + // Validate checks if IP address has to many login attempts inside a window. + // Will return true if provided IP address do not have too many attempts. + ValidateIPAddress(ctx context.Context, ipAddress string) (bool, error) // Reset resets all login attempts attached to username Reset(ctx context.Context, username string) error } diff --git a/pkg/services/loginattempt/loginattemptimpl/login_attempt.go b/pkg/services/loginattempt/loginattemptimpl/login_attempt.go index f55b8fe34b1..3c901415333 100644 --- a/pkg/services/loginattempt/loginattemptimpl/login_attempt.go +++ b/pkg/services/loginattempt/loginattemptimpl/login_attempt.go @@ -53,7 +53,7 @@ func (s *Service) Add(ctx context.Context, username, IPAddress string) error { _, err := s.store.CreateLoginAttempt(ctx, CreateLoginAttemptCommand{ Username: strings.ToLower(username), - IpAddress: IPAddress, + IPAddress: IPAddress, }) return err } @@ -84,6 +84,28 @@ func (s *Service) Validate(ctx context.Context, username string) (bool, error) { return true, nil } +func (s *Service) ValidateIPAddress(ctx context.Context, IPAddress string) (bool, error) { + if s.cfg.DisableIPAddressLoginProtection { + return true, nil + } + + loginAttemptCountQuery := GetIPLoginAttemptCountQuery{ + IPAddress: IPAddress, + Since: time.Now().Add(-loginAttemptsWindow), + } + + count, err := s.store.GetIPLoginAttemptCount(ctx, loginAttemptCountQuery) + if err != nil { + return false, err + } + + if count >= s.cfg.BruteForceLoginProtectionMaxAttempts { + return false, nil + } + + return true, nil +} + func (s *Service) cleanup(ctx context.Context) { err := s.lock.LockAndExecute(ctx, "delete old login attempts", time.Minute*10, func(context.Context) { cmd := DeleteOldLoginAttemptsCommand{ diff --git a/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go b/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go index 7015dff01e9..96e19039f28 100644 --- a/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go +++ b/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go @@ -22,40 +22,40 @@ func TestService_Validate(t *testing.T) { expectedErr error }{ { - name: "When brute force protection enabled and user login attempt count is less than max", + name: "Should be valid when brute force protection enabled and user login attempt count is less than max", loginAttempts: maxInvalidLoginAttempts - 1, expected: true, expectedErr: nil, }, { - name: "When brute force protection enabled and user login attempt count equals max", + name: "Should be invalid when brute force protection enabled and user login attempt count equals max", loginAttempts: maxInvalidLoginAttempts, expected: false, expectedErr: nil, }, { - name: "When brute force protection enabled and user login attempt count is greater than max", + name: "Should be invalid when brute force protection enabled and user login attempt count is greater than max", loginAttempts: maxInvalidLoginAttempts + 1, expected: false, expectedErr: nil, }, { - name: "When brute force protection disabled and user login attempt count is less than max", + name: "Should be valid when brute force protection disabled and user login attempt count is less than max", loginAttempts: maxInvalidLoginAttempts - 1, disabled: true, expected: true, expectedErr: nil, }, { - name: "When brute force protection disabled and user login attempt count equals max", + name: "Should be valid when brute force protection disabled and user login attempt count equals max", loginAttempts: maxInvalidLoginAttempts, disabled: true, expected: true, expectedErr: nil, }, { - name: "When brute force protection disabled and user login attempt count is greater than max", + name: "Should be valid when brute force protection disabled and user login attempt count is greater than max", loginAttempts: maxInvalidLoginAttempts + 1, disabled: true, expected: true, @@ -83,7 +83,7 @@ func TestService_Validate(t *testing.T) { } } -func TestLoginAttempts(t *testing.T) { +func TestUserLoginAttempts(t *testing.T) { ctx := context.Background() cfg := setting.NewCfg() cfg.DisableBruteForceLoginProtection = false @@ -109,6 +109,102 @@ func TestLoginAttempts(t *testing.T) { assert.Nil(t, err) } +func TestService_ValidateIPAddress(t *testing.T) { + const maxInvalidLoginAttempts = 5 + + testCases := []struct { + name string + loginAttempts int64 + disabled bool + expected bool + expectedErr error + }{ + { + name: "Should be valid when brute force protection enabled and IP address login attempt count is less than max", + loginAttempts: maxInvalidLoginAttempts - 1, + expected: true, + expectedErr: nil, + }, + { + name: "Should be invalid when brute force protection enabled and IP address login attempt count equals max", + loginAttempts: maxInvalidLoginAttempts, + expected: false, + expectedErr: nil, + }, + { + name: "Should be invalid when brute force protection enabled and IP address login attempt count is greater than max", + loginAttempts: maxInvalidLoginAttempts + 1, + expected: false, + expectedErr: nil, + }, + + { + name: "Should be valid when brute force protection disabled and IP address login attempt count is less than max", + loginAttempts: maxInvalidLoginAttempts - 1, + disabled: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when brute force protection disabled and IP address login attempt count equals max", + loginAttempts: maxInvalidLoginAttempts, + disabled: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when brute force protection disabled and IP address login attempt count is greater than max", + loginAttempts: maxInvalidLoginAttempts + 1, + disabled: true, + expected: true, + expectedErr: nil, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + cfg := setting.NewCfg() + cfg.BruteForceLoginProtectionMaxAttempts = maxInvalidLoginAttempts + cfg.DisableIPAddressLoginProtection = tt.disabled + service := &Service{ + store: fakeStore{ + ExpectedCount: tt.loginAttempts, + ExpectedErr: tt.expectedErr, + }, + cfg: cfg, + } + + ok, err := service.ValidateIPAddress(context.Background(), "192.168.1.1") + assert.Equal(t, tt.expected, ok) + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +func TestIPLoginAttempts(t *testing.T) { + ctx := context.Background() + cfg := setting.NewCfg() + cfg.DisableIPAddressLoginProtection = false + cfg.BruteForceLoginProtectionMaxAttempts = 3 + db := db.InitTestDB(t) + service := ProvideService(db, cfg, nil) + + _ = service.Add(ctx, "user1", "192.168.1.1") + _ = service.Add(ctx, "user2", "10.0.0.123") + _ = service.Add(ctx, "user3", "192.168.1.1") + _ = service.Add(ctx, "user4", "[::1]") + _ = service.Add(ctx, "user5", "192.168.1.1") + _ = service.Add(ctx, "user6", "192.168.1.1") + + count, err := service.store.GetIPLoginAttemptCount(ctx, GetIPLoginAttemptCountQuery{IPAddress: "192.168.1.1"}) + assert.Nil(t, err) + assert.Equal(t, int64(4), count) + + ok, err := service.ValidateIPAddress(ctx, "192.168.1.1") + assert.False(t, ok) + assert.Nil(t, err) +} + var _ store = new(fakeStore) type fakeStore struct { @@ -121,6 +217,10 @@ func (f fakeStore) GetUserLoginAttemptCount(ctx context.Context, query GetUserLo return f.ExpectedCount, f.ExpectedErr } +func (f fakeStore) GetIPLoginAttemptCount(ctx context.Context, query GetIPLoginAttemptCountQuery) (int64, error) { + return f.ExpectedCount, f.ExpectedErr +} + func (f fakeStore) CreateLoginAttempt(ctx context.Context, command CreateLoginAttemptCommand) (loginattempt.LoginAttempt, error) { return loginattempt.LoginAttempt{}, f.ExpectedErr } diff --git a/pkg/services/loginattempt/loginattemptimpl/models.go b/pkg/services/loginattempt/loginattemptimpl/models.go index f235cd3d308..4957daa8414 100644 --- a/pkg/services/loginattempt/loginattemptimpl/models.go +++ b/pkg/services/loginattempt/loginattemptimpl/models.go @@ -6,7 +6,7 @@ import ( type CreateLoginAttemptCommand struct { Username string - IpAddress string + IPAddress string } type GetUserLoginAttemptCountQuery struct { @@ -14,6 +14,11 @@ type GetUserLoginAttemptCountQuery struct { Since time.Time } +type GetIPLoginAttemptCountQuery struct { + IPAddress string + Since time.Time +} + type DeleteOldLoginAttemptsCommand struct { OlderThan time.Time } diff --git a/pkg/services/loginattempt/loginattemptimpl/store.go b/pkg/services/loginattempt/loginattemptimpl/store.go index f273e6f5ef0..0afa93fbaeb 100644 --- a/pkg/services/loginattempt/loginattemptimpl/store.go +++ b/pkg/services/loginattempt/loginattemptimpl/store.go @@ -18,13 +18,14 @@ type store interface { DeleteOldLoginAttempts(ctx context.Context, cmd DeleteOldLoginAttemptsCommand) (int64, error) DeleteLoginAttempts(ctx context.Context, cmd DeleteLoginAttemptsCommand) error GetUserLoginAttemptCount(ctx context.Context, query GetUserLoginAttemptCountQuery) (int64, error) + GetIPLoginAttemptCount(ctx context.Context, query GetIPLoginAttemptCountQuery) (int64, error) } func (xs *xormStore) CreateLoginAttempt(ctx context.Context, cmd CreateLoginAttemptCommand) (result loginattempt.LoginAttempt, err error) { err = xs.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { loginAttempt := loginattempt.LoginAttempt{ Username: cmd.Username, - IpAddress: cmd.IpAddress, + IpAddress: cmd.IPAddress, Created: xs.now().Unix(), } @@ -73,6 +74,22 @@ func (xs *xormStore) GetUserLoginAttemptCount(ctx context.Context, query GetUser And("created >= ?", query.Since.Unix()). Count(loginAttempt) + return queryErr + }) + + return total, err +} + +func (xs *xormStore) GetIPLoginAttemptCount(ctx context.Context, query GetIPLoginAttemptCountQuery) (int64, error) { + var total int64 + err := xs.db.WithDbSession(ctx, func(dbSession *db.Session) error { + var queryErr error + loginAttempt := new(loginattempt.LoginAttempt) + total, queryErr = dbSession. + Where("ip_address = ?", query.IPAddress). + And("created >= ?", query.Since.Unix()). + Count(loginAttempt) + if queryErr != nil { return queryErr } diff --git a/pkg/services/loginattempt/loginattemptimpl/store_test.go b/pkg/services/loginattempt/loginattemptimpl/store_test.go index 194e64ff572..c8041bbd6da 100644 --- a/pkg/services/loginattempt/loginattemptimpl/store_test.go +++ b/pkg/services/loginattempt/loginattemptimpl/store_test.go @@ -60,21 +60,21 @@ func TestIntegrationLoginAttemptsQuery(t *testing.T) { _, err := s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{ Username: user, - IpAddress: "192.168.0.1", + IPAddress: "192.168.0.1", }) require.Nil(t, err) mockTime = timePlusOneMinute _, err = s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{ Username: user, - IpAddress: "192.168.0.1", + IPAddress: "192.168.0.1", }) require.Nil(t, err) mockTime = timePlusTwoMinutes _, err = s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{ Username: user, - IpAddress: "192.168.0.1", + IPAddress: "192.168.0.1", }) require.Nil(t, err) @@ -125,21 +125,21 @@ func TestIntegrationLoginAttemptsDelete(t *testing.T) { _, err := s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{ Username: user, - IpAddress: "192.168.0.1", + IPAddress: "192.168.0.1", }) require.Nil(t, err) mockTime = timePlusOneMinute _, err = s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{ Username: user, - IpAddress: "192.168.0.1", + IPAddress: "192.168.0.1", }) require.Nil(t, err) mockTime = timePlusTwoMinutes _, err = s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{ Username: user, - IpAddress: "192.168.0.1", + IPAddress: "192.168.0.1", }) require.Nil(t, err) diff --git a/pkg/services/loginattempt/loginattempttest/fake.go b/pkg/services/loginattempt/loginattempttest/fake.go index 581aeae1742..4e6cb56b52b 100644 --- a/pkg/services/loginattempt/loginattempttest/fake.go +++ b/pkg/services/loginattempt/loginattempttest/fake.go @@ -24,3 +24,7 @@ func (f FakeLoginAttemptService) Reset(ctx context.Context, username string) err func (f FakeLoginAttemptService) Validate(ctx context.Context, username string) (bool, error) { return f.ExpectedValid, f.ExpectedErr } + +func (f FakeLoginAttemptService) ValidateIPAddress(ctx context.Context, IpAddress string) (bool, error) { + return f.ExpectedValid, f.ExpectedErr +} diff --git a/pkg/services/loginattempt/loginattempttest/mock.go b/pkg/services/loginattempt/loginattempttest/mock.go index c775323a9a3..33cff397161 100644 --- a/pkg/services/loginattempt/loginattempttest/mock.go +++ b/pkg/services/loginattempt/loginattempttest/mock.go @@ -17,7 +17,7 @@ type MockLoginAttemptService struct { ExpectedErr error } -func (f *MockLoginAttemptService) Add(ctx context.Context, username, IPAddress string) error { +func (f *MockLoginAttemptService) Add(ctx context.Context, username, ipAddress string) error { f.AddCalled = true return f.ExpectedErr } @@ -31,3 +31,8 @@ func (f *MockLoginAttemptService) Validate(ctx context.Context, username string) f.ValidateCalled = true return f.ExpectedValid, f.ExpectedErr } + +func (f *MockLoginAttemptService) ValidateIPAddress(ctx context.Context, ipAddress string) (bool, error) { + f.ValidateCalled = true + return f.ExpectedValid, f.ExpectedErr +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index d1f47153138..5b560c3b937 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -156,6 +156,7 @@ type Cfg struct { DisableInitAdminCreation bool DisableBruteForceLoginProtection bool BruteForceLoginProtectionMaxAttempts int64 + DisableIPAddressLoginProtection bool CookieSecure bool CookieSameSiteDisabled bool CookieSameSiteMode http.SameSite @@ -1527,6 +1528,7 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error { cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false) cfg.BruteForceLoginProtectionMaxAttempts = security.Key("brute_force_login_protection_max_attempts").MustInt64(5) + cfg.DisableIPAddressLoginProtection = security.Key("disable_ip_address_login_protection").MustBool(true) // Ensure at least one login attempt can be performed. if cfg.BruteForceLoginProtectionMaxAttempts <= 0 { From 6787cdccb9670f79ed90a2ea938f7b5a2878c3cd Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:04:03 -0600 Subject: [PATCH 365/894] CI: Backport to release branches (#100067) * update backport and release comms * Backport to release branches and change docs source branch for publishing * Add new workflows to CODEOWNERS * Re-add removed line oops * backport-testing -> grafana * checkout grafana repo in backport action, reference repo / branch in reusable action * generate -> create --- .github/CODEOWNERS | 2 + .github/workflows/backport.yml | 15 ++---- .../workflows/create-next-release-branch.yml | 43 ++++++++++++++++ .github/workflows/migrate-prs.yml | 50 +++++++++++++++++++ ...ublish-technical-documentation-release.yml | 6 +-- .github/workflows/release-comms.yml | 50 +++++++++++++++++++ .github/workflows/release-pr.yml | 18 ++++--- 7 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/create-next-release-branch.yml create mode 100644 .github/workflows/migrate-prs.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 645625d3d19..cd0bde5129f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -756,6 +756,8 @@ embed.go @grafana/grafana-as-code /.github/workflows/close-milestone.yml @grafana/grafana-developer-enablement-squad /.github/workflows/release-pr.yml @grafana/grafana-developer-enablement-squad /.github/workflows/release-comms.yml @grafana/grafana-developer-enablement-squad +/.github/workflows/migrate-prs.yml @grafana/grafana-developer-enablement-squad +/.github/workflows/create-next-release-branch.yml @grafana/grafana-developer-enablement-squad /.github/workflows/codeowners-validator.yml @tolzhabayev /.github/workflows/codeql-analysis.yml @DanCech /.github/workflows/commands.yml @torkelo diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 18407b67f76..384a3b36ddf 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -10,24 +10,19 @@ jobs: if: github.repository == 'grafana/grafana' runs-on: ubuntu-latest steps: - - name: Checkout Actions + - name: Checkout uses: actions/checkout@v4 - with: - repository: "grafana/grafana-github-actions" - path: ./actions ref: main - - name: Install Actions - run: npm install --production --prefix ./actions - name: "Generate token" id: generate_token uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 with: app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }} private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }} + - run: git config --global user.email '132647405+grafana-delivery-bot[bot]@users.noreply.github.com' + - run: git config --global user.name 'grafana-delivery-bot[bot]' + - run: git remote set-url origin "https://grafana-delivery-bot:${{ steps.generate_token.outputs.token }}@github.com/grafana/grafana.git" - name: Run backport - uses: ./actions/backport + uses: grafana/grafana-github-actions-go/backport@main with: - metricsWriteAPIKey: ${{secrets.GRAFANA_MISC_STATS_API_KEY}} token: ${{ steps.generate_token.outputs.token }} - labelsToAdd: "backport" - title: "[{{base}}] {{originalTitle}}" diff --git a/.github/workflows/create-next-release-branch.yml b/.github/workflows/create-next-release-branch.yml new file mode 100644 index 00000000000..e1cc71c02a0 --- /dev/null +++ b/.github/workflows/create-next-release-branch.yml @@ -0,0 +1,43 @@ +name: Create next release branch +on: + workflow_call: + inputs: + ownerRepo: + type: string + description: Owner/repo of the repository where the branch is created (e.g. 'grafana/grafana') + required: true + source: + description: The release branch to increment (eg providing `release-11.2.3` will result in `release-11.2.4` being created) + type: string + required: true + secrets: + token: + required: true + outputs: + branch: + description: The new branch that was created + value: ${{ jobs.main.outputs.branch }} + workflow_dispatch: + inputs: + ownerRepo: + description: Owner/repo of the repository where the branch is created (e.g. 'grafana/grafana') + source: + description: The release branch to increment (eg providing `release-11.2.3` will result in `release-11.2.4` being created) + type: string + required: true + secrets: + token: + required: true +jobs: + main: + runs-on: ubuntu-latest + outputs: + branch: ${{ steps.branch.outputs.branch }} + steps: + - name: Create release branch + id: branch + uses: grafana/grafana-github-actions-go/bump-release@main + with: + ownerRepo: ${{ inputs.ownerRepo }} + source: ${{ inputs.source }} + token: ${{ secrets.token }} diff --git a/.github/workflows/migrate-prs.yml b/.github/workflows/migrate-prs.yml new file mode 100644 index 00000000000..9a41ee6ee8a --- /dev/null +++ b/.github/workflows/migrate-prs.yml @@ -0,0 +1,50 @@ +name: Migrate open PRs +# Migrate open PRs from a superseded release branch to the current release branch and notify authors +on: + workflow_call: + inputs: + from: + description: 'The base branch to check for open PRs' + required: true + type: string + to: + description: 'The base branch to migrate open PRs to' + required: true + type: string + ownerRepo: + description: Owner/repo of the repository where the branch is created (e.g. 'grafana/grafana') + required: true + type: string + secrets: + token: + required: true + workflow_dispatch: + inputs: + from: + description: 'The base branch to check for open PRs' + required: true + type: string + to: + description: 'The base branch to migrate open PRs to' + required: true + type: string + ownerRepo: + description: Owner/repo of the repository where the branch is created (e.g. 'grafana/grafana') + required: true + type: string + secrets: + token: + required: true + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Migrate PRs + uses: grafana/grafana-github-actions-go/migrate-open-prs@main + with: + token: ${{ secrets.token }} + ownerRepo: ${{ inputs.ownerRepo }} + from: ${{ inputs.from }} + to: ${{ inputs.to }} + binary_release_tag: 'dev' diff --git a/.github/workflows/publish-technical-documentation-release.yml b/.github/workflows/publish-technical-documentation-release.yml index b36eb48885a..bd77c286018 100644 --- a/.github/workflows/publish-technical-documentation-release.yml +++ b/.github/workflows/publish-technical-documentation-release.yml @@ -3,7 +3,7 @@ name: publish-technical-documentation-release on: push: branches: - - v[0-9]+.[0-9]+.x + - release-v[0-9]+.[0-9]+.[0-9]+ tags: - v[0-9]+.[0-9]+.[0-9]+ paths: @@ -23,7 +23,7 @@ jobs: - uses: grafana/writers-toolkit/publish-technical-documentation-release@publish-technical-documentation-release/v2 with: release_tag_regexp: "^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$" - release_branch_regexp: "^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.x$" - release_branch_with_patch_regexp: "^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$" + release_branch_regexp: "^release-(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$" + release_branch_with_patch_regexp: "^release-(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$" website_directory: content/docs/grafana version_suffix: "" diff --git a/.github/workflows/release-comms.yml b/.github/workflows/release-comms.yml index 02d53057184..fbc7d94ec13 100644 --- a/.github/workflows/release-comms.yml +++ b/.github/workflows/release-comms.yml @@ -27,10 +27,18 @@ jobs: name: Setup and establish latest outputs: version: ${{ steps.output.outputs.version }} + release_branch: ${{ steps.output.outputs.release_branch }} dry_run: ${{ steps.output.outputs.dry_run }} latest: ${{ steps.output.outputs.latest }} + token: ${{ steps.output.outputs.token }} runs-on: ubuntu-latest steps: + - name: "Generate token" + id: generate_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }} + private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }} # The github-release action expects a `LATEST` value of a string of either '1' or '0' - if: ${{ github.event_name == 'workflow_dispatch' }} run: | @@ -49,9 +57,51 @@ jobs: echo "latest: $LATEST" echo "version: $VERSION" + echo "release_branch=$(echo $VERSION | sed -s 's/^v/release-/g')" >> "$GITHUB_OUTPUT" + echo "token=${{ steps.generate_token.outputs.token }}" >> "$GITHUB_OUTPUT" echo "dry_run=$DRY_RUN" >> "$GITHUB_OUTPUT" echo "latest=$LATEST" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" + create_next_release_branch_grafana: + name: Create next release branch (Grafana) + needs: setup + uses: ./.github/workflows/create-next-release-branch.yml + with: + ownerRepo: 'grafana/grafana' + source: ${{ needs.setup.outputs.release_branch }} + secrets: + token: ${{ needs.setup.outputs.token }} + create_next_release_branch_enterprise: + name: Create next release branch (Grafana Enterprise) + needs: setup + uses: ./.github/workflows/create-next-release-branch.yml + with: + ownerRepo: 'grafana/grafana' + source: ${{ needs.setup.outputs.release_branch }} + secrets: + token: ${{ needs.setup.outputs.token }} + migrate_prs_grafana: + needs: + - setup + - create_next_release_branch_grafana + uses: ./.github/workflows/migrate-prs.yml + with: + ownerRepo: 'grafana/grafana' + from: ${{ needs.setup.outputs.release_branch }} + to: ${{ needs.create_next_release_branch_grafana.outputs.branch }} + secrets: + token: ${{ needs.setup.outputs.token }} + migrate_prs_enterprise: + needs: + - setup + - create_next_release_branch_enterprise + uses: ./.github/workflows/migrate-prs.yml + with: + ownerRepo: 'grafana/grafana-enterprise' + from: ${{ needs.setup.outputs.release_branch }} + to: ${{ needs.create_next_release_branch_enterprise.outputs.branch }} + secrets: + token: ${{ needs.setup.outputs.token }} post_changelog_on_forum: needs: setup uses: ./.github/workflows/community-release.yml diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index d6bcd7d5446..004d1ab3299 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -4,7 +4,7 @@ # Please refrain from including any processes that do not result in code changes in this workflow. Instead, they should # either be triggered in the release promotion process or in the release comms process (that is triggered by merging # this PR). -name: Complete a Grafana release +name: Grafana Release PR on: workflow_dispatch: inputs: @@ -19,7 +19,7 @@ on: target: required: true type: string - description: The base branch that these changes are being merged into + description: The release branch pattern (eg v9.5.x) that these changes are being merged into backport: required: false type: string @@ -61,10 +61,17 @@ jobs: with: app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }} private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }} + - name: Get release branch + id: branch + uses: grafana/grafana-github-actions-go/latest-release-branch@main + with: + token: ${{ steps.generate_token.outputs.token }} + ownerRepo: 'grafana/grafana' + pattern: ${{ inputs.target }} - name: Checkout Grafana uses: actions/checkout@v4 with: - ref: ${{ inputs.target }} + ref: ${{ steps.branch.outputs.branch }} fetch-depth: 0 fetch-tags: true - name: Checkout Grafana (main) @@ -124,7 +131,6 @@ jobs: rm -f CHANGELOG.part changelog_items.md git diff CHANGELOG.md - - name: "Prettify CHANGELOG.md" run: npx prettier --write CHANGELOG.md - name: Commit CHANGELOG.md changes @@ -151,7 +157,7 @@ jobs: $( [ "x${{ inputs.latest }}" == "xtrue" ] && printf %s '-l "release/latest"') \ -l "no-changelog" \ --dry-run=${{ inputs.dry_run }} \ - -B "${{ inputs.target }}" \ + -B "${{ steps.branch.outputs.branch }}" \ --title "Release: ${{ inputs.version }}" \ --body "These code changes must be merged after a release is complete" env: @@ -165,7 +171,7 @@ jobs: -l "product-approved" \ -l "no-changelog" \ --dry-run=${{ inputs.dry_run }} \ - -B "${{ inputs.target }}" \ + -B "${{ steps.branch.outputs.branch }}" \ --title "Release: ${{ inputs.version }}" \ --body "These code changes must be merged after a release is complete" env: From 33b11d5c76baa5f6885776e16ba9090bcbd1a71f Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 5 Feb 2025 14:15:02 -0500 Subject: [PATCH 366/894] Alerting: Remove ID and OrgID from hash calculation (#100140) --- pkg/services/ngalert/schedule/registry.go | 2 -- pkg/services/ngalert/schedule/registry_test.go | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go index 2b162ca87be..11f72229f51 100644 --- a/pkg/services/ngalert/schedule/registry.go +++ b/pkg/services/ngalert/schedule/registry.go @@ -314,8 +314,6 @@ func (r ruleWithFolder) Fingerprint() fingerprint { // fields that do not affect the state. // TODO consider removing fields below from the fingerprint - writeInt(rule.ID) - writeInt(rule.OrgID) writeInt(int64(rule.For)) if rule.DashboardUID != nil { writeString(*rule.DashboardUID) diff --git a/pkg/services/ngalert/schedule/registry_test.go b/pkg/services/ngalert/schedule/registry_test.go index 6ee2517a706..fb779c346ea 100644 --- a/pkg/services/ngalert/schedule/registry_test.go +++ b/pkg/services/ngalert/schedule/registry_test.go @@ -262,6 +262,8 @@ func TestRuleWithFolderFingerprint(t *testing.T) { "UpdatedBy": {}, "IntervalSeconds": {}, "Annotations": {}, + "ID": {}, + "OrgID": {}, } tp := reflect.TypeOf(rule).Elem() From b7b2e2bbaaf615767fb08b6029599a49358fa2f1 Mon Sep 17 00:00:00 2001 From: Kevin Yu Date: Wed, 5 Feb 2025 11:29:18 -0800 Subject: [PATCH 367/894] CI: Commit package.json changes in e2e/test-plugins when bumping versions (#97506) --- .github/workflows/release-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 004d1ab3299..56a5e50f332 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -143,7 +143,7 @@ jobs: - name: Add package.json changes run: | - git add package.json lerna.json yarn.lock packages public + git add package.json lerna.json yarn.lock packages public e2e/test-plugins git commit -m "Update version to ${{ inputs.version }}" - name: Git push From 8e10ee90568630aefa25fdfe0386f9e3d43ba4c2 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:37:19 -0600 Subject: [PATCH 368/894] CI: remove unnecessary line in backport.yml (#100144) remove bad line --- .github/workflows/backport.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 384a3b36ddf..5f21ab276ee 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -12,7 +12,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - ref: main - name: "Generate token" id: generate_token uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 From 364559e85629e3551c9f89fb8a4e74ab8d94f0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Wed, 5 Feb 2025 21:02:35 +0100 Subject: [PATCH 369/894] feat(unified-storage): use continue token when building list (#100143) --- pkg/apiserver/rest/dualwriter_syncer.go | 41 ++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/pkg/apiserver/rest/dualwriter_syncer.go b/pkg/apiserver/rest/dualwriter_syncer.go index 1c18c71737e..9c36fdd22b7 100644 --- a/pkg/apiserver/rest/dualwriter_syncer.go +++ b/pkg/apiserver/rest/dualwriter_syncer.go @@ -179,7 +179,9 @@ func legacyToUnifiedStorageDataSyncer(ctx context.Context, cfg *SyncerConfig) (b log.Info("got items from unified storage", "items", len(storageList)) - legacyList, err := getList(ctx, cfg.LegacyStorage, &metainternalversion.ListOptions{}) + legacyList, err := getList(ctx, cfg.LegacyStorage, &metainternalversion.ListOptions{ + Limit: int64(cfg.DataSyncerRecordsLimit), + }) if err != nil { log.Error(err, "unable to extract list from legacy storage") return @@ -295,10 +297,39 @@ func legacyToUnifiedStorageDataSyncer(ctx context.Context, cfg *SyncerConfig) (b } func getList(ctx context.Context, obj rest.Lister, listOptions *metainternalversion.ListOptions) ([]runtime.Object, error) { - ll, err := obj.List(ctx, listOptions) - if err != nil { - return nil, err + var allItems []runtime.Object + + for { + if int64(len(allItems)) >= listOptions.Limit { + return nil, fmt.Errorf("list has more than %d records. Aborting sync", listOptions.Limit) + } + + ll, err := obj.List(ctx, listOptions) + if err != nil { + return nil, err + } + + items, err := meta.ExtractList(ll) + if err != nil { + return nil, err + } + + allItems = append(allItems, items...) + + // Get continue token from the list metadata. + listMeta, err := meta.ListAccessor(ll) + if err != nil { + return nil, err + } + + // If no continue token, we're done paginating. + if listMeta.GetContinue() == "" { + break + } + + // Set continue token for next page. + listOptions.Continue = listMeta.GetContinue() } - return meta.ExtractList(ll) + return allItems, nil } From 69e4d8468b6ab9a8e4bf48e5a5750f49e2d0cc9a Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:01:51 -0500 Subject: [PATCH 370/894] Dashboard: Add v1alpha1 test coverage (#100149) --- pkg/tests/apis/dashboard/dashboards_test.go | 109 ++++++++++++++++---- 1 file changed, 91 insertions(+), 18 deletions(-) diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index 4232fd478ba..26c9d65fa06 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -17,17 +17,11 @@ import ( "github.com/grafana/grafana/pkg/tests/testsuite" ) -var gvr = schema.GroupVersionResource{ - Group: "dashboard.grafana.app", - Version: "v0alpha1", - Resource: "dashboards", -} - func TestMain(m *testing.M) { testsuite.Run(m) } -func runDashboardTest(t *testing.T, helper *apis.K8sTestHelper) { +func runDashboardTest(t *testing.T, helper *apis.K8sTestHelper, gvr schema.GroupVersionResource) { t.Run("simple crud+list", func(t *testing.T) { ctx := context.Background() client := helper.GetResourceClient(apis.ResourceClientArgs{ @@ -62,6 +56,7 @@ func runDashboardTest(t *testing.T, helper *apis.K8sTestHelper) { require.NoError(t, err) require.Equal(t, created, obj.GetName()) require.Equal(t, int64(1), obj.GetGeneration()) + require.Equal(t, "Test empty dashboard", obj.Object["spec"].(map[string]any)["title"]) wrap, err := utils.MetaAccessor(obj) require.NoError(t, err) @@ -87,6 +82,7 @@ func runDashboardTest(t *testing.T, helper *apis.K8sTestHelper) { require.Equal(t, obj.GetName(), updated.GetName()) require.Equal(t, obj.GetUID(), updated.GetUID()) require.Less(t, obj.GetResourceVersion(), updated.GetResourceVersion()) + require.Equal(t, "Changed title", updated.Object["spec"].(map[string]any)["title"]) // Delete the object, skipping the provisioned dashboard check zeroInt64 := int64(0) @@ -102,12 +98,17 @@ func runDashboardTest(t *testing.T, helper *apis.K8sTestHelper) { }) } -func TestIntegrationDashboardsApp(t *testing.T) { +func TestIntegrationDashboardsAppV0Alpha1(t *testing.T) { + gvr := schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Resource: "dashboards", + } if testing.Short() { t.Skip("skipping integration test") } - t.Run("with dual writer mode 0", func(t *testing.T) { + t.Run("v0alpha1 with dual writer mode 0", func(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ DisableAnonymous: true, UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ @@ -116,10 +117,10 @@ func TestIntegrationDashboardsApp(t *testing.T) { }, }, }) - runDashboardTest(t, helper) + runDashboardTest(t, helper, gvr) }) - t.Run("with dual writer mode 1", func(t *testing.T) { + t.Run("v0alpha1 with dual writer mode 1", func(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ DisableAnonymous: true, UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ @@ -128,10 +129,10 @@ func TestIntegrationDashboardsApp(t *testing.T) { }, }, }) - runDashboardTest(t, helper) + runDashboardTest(t, helper, gvr) }) - t.Run("with dual writer mode 2", func(t *testing.T) { + t.Run("v0alpha1 with dual writer mode 2", func(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ DisableAnonymous: true, UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ @@ -140,10 +141,10 @@ func TestIntegrationDashboardsApp(t *testing.T) { }, }, }) - runDashboardTest(t, helper) + runDashboardTest(t, helper, gvr) }) - t.Run("with dual writer mode 3", func(t *testing.T) { + t.Run("v0alpha1 with dual writer mode 3", func(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ DisableAnonymous: true, UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ @@ -152,10 +153,10 @@ func TestIntegrationDashboardsApp(t *testing.T) { }, }, }) - runDashboardTest(t, helper) + runDashboardTest(t, helper, gvr) }) - t.Run("with dual writer mode 4", func(t *testing.T) { + t.Run("v0alpha1 with dual writer mode 4", func(t *testing.T) { t.Skip("skipping test because of authorizer issue") helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ DisableAnonymous: true, @@ -165,6 +166,78 @@ func TestIntegrationDashboardsApp(t *testing.T) { }, }, }) - runDashboardTest(t, helper) + runDashboardTest(t, helper, gvr) + }) +} + +func TestIntegrationDashboardsAppV1Alpha1(t *testing.T) { + gvr := schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v1alpha1", + Resource: "dashboards", + } + if testing.Short() { + t.Skip("skipping integration test") + } + + t.Run("v1alpha1 with dual writer mode 0", func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: 0, + }, + }, + }) + runDashboardTest(t, helper, gvr) + }) + + t.Run("v1alpha1 with dual writer mode 1", func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: 1, + }, + }, + }) + runDashboardTest(t, helper, gvr) + }) + + t.Run("v1alpha1 with dual writer mode 2", func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: 2, + }, + }, + }) + runDashboardTest(t, helper, gvr) + }) + + t.Run("v1alpha1 with dual writer mode 3", func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: 3, + }, + }, + }) + runDashboardTest(t, helper, gvr) + }) + + t.Run("v1alpha1 with dual writer mode 4", func(t *testing.T) { + t.Skip("skipping test because of authorizer issue") + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: 4, + }, + }, + }) + runDashboardTest(t, helper, gvr) }) } From 0fe4b15d0082bd72a1392a7b16ab78d69e6bb0fd Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Wed, 5 Feb 2025 15:11:43 -0600 Subject: [PATCH 371/894] Explore metrics: Add option to use regex (#100146) add option to use regex --- public/app/features/trails/DataTrail.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index 46a854ffdb1..fa85dd32773 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -653,6 +653,8 @@ function getVariableSet( layout: 'vertical', defaultKeys: [], applyMode: 'manual', + supportsMultiValueOperators: true, + allowCustomValue: true, }), new AdHocFiltersVariable({ name: VAR_FILTERS, @@ -666,6 +668,7 @@ function getVariableSet( applyMode: 'manual', // since we only support prometheus datasources, this is always true supportsMultiValueOperators: true, + allowCustomValue: true, }), ...getVariablesWithOtelJoinQueryConstant(otelJoinQuery ?? ''), new ConstantVariable({ @@ -689,6 +692,7 @@ function getVariableSet( applyMode: 'manual', // since we only support prometheus datasources, this is always true supportsMultiValueOperators: true, + allowCustomValue: true, // skipUrlSync: true }), // Legacy variable needed for bookmarking which is necessary because From f7d476e408248bc9b2a968f3a2518835a8e0c1b4 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 5 Feb 2025 16:13:22 -0500 Subject: [PATCH 372/894] Alerting: Remove id and org_id from grafana alert rule API model (#100139) --- pkg/services/ngalert/api/api_ruler.go | 2 -- pkg/services/ngalert/api/tooling/api.json | 8 ------- .../api/tooling/definitions/cortex-ruler.go | 2 -- pkg/services/ngalert/api/tooling/post.json | 8 ------- pkg/services/ngalert/api/tooling/spec.json | 8 ------- pkg/tests/api/alerting/api_ruler_test.go | 21 ------------------- .../alerting/test-data/rulegroup-1-get.json | 4 ---- .../alerting/test-data/rulegroup-2-get.json | 2 -- .../alerting/test-data/rulegroup-3-get.json | 4 ---- public/api-merged.json | 8 ------- public/openapi3.json | 8 ------- 11 files changed, 75 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 1f122c04ad1..718922ff8e3 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -585,8 +585,6 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[stri gettableExtendedRuleNode := apimodels.GettableExtendedRuleNode{ GrafanaManagedAlert: &apimodels.GettableGrafanaRule{ - ID: r.ID, - OrgID: r.OrgID, Title: r.Title, Condition: r.Condition, Data: ApiAlertQueriesFromAlertQueries(r.Data), diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 24d1d9d81c4..25f7b2ff8c3 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -1593,10 +1593,6 @@ ], "type": "string" }, - "id": { - "format": "int64", - "type": "integer" - }, "intervalSeconds": { "format": "int64", "type": "integer" @@ -1621,10 +1617,6 @@ "notification_settings": { "$ref": "#/definitions/AlertRuleNotificationSettings" }, - "orgId": { - "format": "int64", - "type": "integer" - }, "provenance": { "$ref": "#/definitions/Provenance" }, diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index d97b99512bf..60fbd2518dd 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -550,8 +550,6 @@ type PostableGrafanaRule struct { // swagger:model type GettableGrafanaRule struct { - ID int64 `json:"id" yaml:"id"` - OrgID int64 `json:"orgId" yaml:"orgId"` Title string `json:"title" yaml:"title"` Condition string `json:"condition" yaml:"condition"` Data []AlertQuery `json:"data" yaml:"data"` diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 0e02ed6f68a..e8c4c20d008 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -1593,10 +1593,6 @@ ], "type": "string" }, - "id": { - "format": "int64", - "type": "integer" - }, "intervalSeconds": { "format": "int64", "type": "integer" @@ -1621,10 +1617,6 @@ "notification_settings": { "$ref": "#/definitions/AlertRuleNotificationSettings" }, - "orgId": { - "format": "int64", - "type": "integer" - }, "provenance": { "$ref": "#/definitions/Provenance" }, diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 960fb35a5fe..abe3364c501 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -5281,10 +5281,6 @@ "Error" ] }, - "id": { - "type": "integer", - "format": "int64" - }, "intervalSeconds": { "type": "integer", "format": "int64" @@ -5309,10 +5305,6 @@ "notification_settings": { "$ref": "#/definitions/AlertRuleNotificationSettings" }, - "orgId": { - "type": "integer", - "format": "int64" - }, "provenance": { "$ref": "#/definitions/Provenance" }, diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 6da7f57c963..4a5ae0cc6fa 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -1143,8 +1143,6 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { "__panelId__": "1" }, "grafana_alert": { - "id": 1, - "orgId": 1, "title": "AlwaysFiring", "condition": "A", "data": [{ @@ -1186,8 +1184,6 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { "expr": "", "for":"0s", "grafana_alert": { - "id": 2, - "orgId": 1, "title": "AlwaysFiringButSilenced", "condition": "A", "data": [{ @@ -1241,8 +1237,6 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { "__panelId__": "1" }, "grafana_alert": { - "id": 1, - "orgId": 1, "title": "AlwaysFiring", "condition": "A", "data": [{ @@ -1583,7 +1577,6 @@ func TestIntegrationRuleCreate(t *testing.T) { }, }, GrafanaManagedAlert: &apimodels.GettableGrafanaRule{ - OrgID: 1, Title: "test1 rule1", Condition: "A", Data: []apimodels.AlertQuery{ @@ -2526,8 +2519,6 @@ func TestIntegrationQuota(t *testing.T) { "expr":"", "for": "2m", "grafana_alert":{ - "id":1, - "orgId":1, "title":"Updated alert rule", "condition":"A", "data":[ @@ -2641,8 +2632,6 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) { "annotation1": "val1" }, "grafana_alert": { - "id": 1, - "orgId": 1, "title": "rule under folder default", "condition": "A", "data": [ @@ -3124,8 +3113,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "label1": "val1" }, "grafana_alert":{ - "id":1, - "orgId":1, "title":"AlwaysFiring", "condition":"A", "data":[ @@ -3170,8 +3157,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expr":"", "for": "0s", "grafana_alert":{ - "id":2, - "orgId":1, "title":"AlwaysFiringButSilenced", "condition":"A", "data":[ @@ -3488,8 +3473,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "label2": "val2" }, "grafana_alert":{ - "id":1, - "orgId":1, "title":"AlwaysNormal", "condition":"A", "data":[ @@ -3607,8 +3590,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expr":"", "for": "30s", "grafana_alert":{ - "id":1, - "orgId":1, "title":"AlwaysNormal", "condition":"A", "data":[ @@ -3705,8 +3686,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expr":"", "for": "30s", "grafana_alert":{ - "id":1, - "orgId":1, "title":"AlwaysNormal", "condition":"A", "data":[ diff --git a/pkg/tests/api/alerting/test-data/rulegroup-1-get.json b/pkg/tests/api/alerting/test-data/rulegroup-1-get.json index cb2b3df8345..d9b66b010a5 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-1-get.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-1-get.json @@ -12,8 +12,6 @@ "annotation": "test-annotation" }, "grafana_alert": { - "id": 1, - "orgId": 1, "title": "Rule1", "condition": "A", "data": [ @@ -60,8 +58,6 @@ "annotation": "test-annotation" }, "grafana_alert": { - "id": 2, - "orgId": 1, "title": "Rule2", "condition": "A", "data": [ diff --git a/pkg/tests/api/alerting/test-data/rulegroup-2-get.json b/pkg/tests/api/alerting/test-data/rulegroup-2-get.json index 572454ad1e7..90adbac6265 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-2-get.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-2-get.json @@ -12,8 +12,6 @@ "annotation": "test-annotation" }, "grafana_alert": { - "id": 3, - "orgId": 1, "title": "Rule3", "condition": "A", "data": [ diff --git a/pkg/tests/api/alerting/test-data/rulegroup-3-get.json b/pkg/tests/api/alerting/test-data/rulegroup-3-get.json index 5e454967b5d..efcf5822ea5 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-3-get.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-3-get.json @@ -12,8 +12,6 @@ "annotation": "test-annotation" }, "grafana_alert": { - "id": 1, - "orgId": 1, "title": "Rule1", "condition": "A", "data": [ @@ -60,8 +58,6 @@ "annotation": "test-annotation" }, "grafana_alert": { - "id": 2, - "orgId": 1, "title": "Rule2", "condition": "A", "data": [ diff --git a/public/api-merged.json b/public/api-merged.json index e6dd277974a..e063c8c959c 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -16013,10 +16013,6 @@ "Error" ] }, - "id": { - "type": "integer", - "format": "int64" - }, "intervalSeconds": { "type": "integer", "format": "int64" @@ -16041,10 +16037,6 @@ "notification_settings": { "$ref": "#/definitions/AlertRuleNotificationSettings" }, - "orgId": { - "type": "integer", - "format": "int64" - }, "provenance": { "$ref": "#/definitions/Provenance" }, diff --git a/public/openapi3.json b/public/openapi3.json index bea198b06a2..f326616d62d 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -6087,10 +6087,6 @@ ], "type": "string" }, - "id": { - "format": "int64", - "type": "integer" - }, "intervalSeconds": { "format": "int64", "type": "integer" @@ -6115,10 +6111,6 @@ "notification_settings": { "$ref": "#/components/schemas/AlertRuleNotificationSettings" }, - "orgId": { - "format": "int64", - "type": "integer" - }, "provenance": { "$ref": "#/components/schemas/Provenance" }, From 495aa65c6ef8028fd10386459fc529c9aed1e71d Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Wed, 5 Feb 2025 17:57:26 -0700 Subject: [PATCH 373/894] FindDashboards: filter by dashboard type (#100160) --- .../dashboards/service/dashboard_service.go | 31 ++++- .../service/dashboard_service_test.go | 128 ++++++++++++++++++ pkg/services/folder/folderimpl/folder.go | 8 +- .../folderimpl/folder_unifiedstorage.go | 8 +- pkg/services/libraryelements/database.go | 2 + .../publicdashboards/service/service.go | 9 +- 6 files changed, 176 insertions(+), 10 deletions(-) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 6b3af7f67bf..d4abb3627fc 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -46,6 +46,7 @@ import ( "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/search/model" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -232,7 +233,7 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardData(ctx context.Context, for _, org := range orgs { func(orgID int64) { g.Go(func() error { - res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{ + res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ ProvisionedRepo: name, OrgId: orgID, }) @@ -273,7 +274,7 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(ctx con } for _, org := range orgs { - res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{ + res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ OrgId: org.ID, DashboardIds: []int64{dashboardID}, }) @@ -300,7 +301,7 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx co return nil, nil } - res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{ + res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ OrgId: orgID, DashboardUIDs: []string{dashboardUID}, }) @@ -559,7 +560,7 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context. for _, org := range orgs { ctx, _ := identity.WithServiceIdentity(ctx, org.ID) // find all dashboards in the org that have a file repo set that is not in the given readers list - foundDashs, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{ + foundDashs, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ ProvisionedReposNotIn: cmd.ReaderNames, OrgId: org.ID, }) @@ -1436,7 +1437,12 @@ func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64 } // We need a list of dashboard uids inside the folder to delete related public dashboards - dashes, err := dr.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{SignedInUser: u, FolderUIDs: folderUIDs, OrgId: orgID}) + dashes, err := dr.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{ + SignedInUser: u, + FolderUIDs: folderUIDs, + OrgId: orgID, + Type: searchstore.TypeDashboard, + }) if err != nil { return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err) } @@ -1729,7 +1735,11 @@ type dashboardProvisioningWithUID struct { DashboardUID string } -func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]*dashboardProvisioningWithUID, error) { +func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]*dashboardProvisioningWithUID, error) { + if query == nil { + return nil, errors.New("query cannot be nil") + } + ctx, _ = identity.WithServiceIdentity(ctx, query.OrgId) if query.ProvisionedRepo != "" { @@ -1744,7 +1754,9 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex query.ProvisionedReposNotIn = repos } - searchResults, err := dr.searchDashboardsThroughK8sRaw(ctx, &query) + query.Type = searchstore.TypeDashboard + + searchResults, err := dr.searchDashboardsThroughK8sRaw(ctx, query) if err != nil { return nil, err } @@ -1807,6 +1819,11 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex } func (dr *DashboardServiceImpl) searchDashboardsThroughK8s(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]*dashboards.Dashboard, error) { + if query == nil { + return nil, errors.New("query cannot be nil") + } + query.Type = searchstore.TypeDashboard + response, err := dr.searchDashboardsThroughK8sRaw(ctx, query) if err != nil { return nil, err diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 1ae6c4f5b90..58cc555bfd3 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -1899,6 +1899,134 @@ func TestCountInFolders(t *testing.T) { }) } +func TestSearchDashboardsThroughK8sRaw(t *testing.T) { + ctx := context.Background() + k8sCliMock := new(client.MockK8sHandler) + service := &DashboardServiceImpl{k8sclient: k8sCliMock} + query := &dashboards.FindPersistedDashboardsQuery{ + OrgId: 1, + } + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder1"), + }, + }, + }, + }, + TotalHits: 1, + }, nil) + res, err := service.searchDashboardsThroughK8s(ctx, query) + require.NoError(t, err) + assert.Equal(t, []*dashboards.Dashboard{ + { + UID: "uid", + OrgID: 1, + FolderUID: "folder1", + Title: "Dashboard 1", + Slug: "dashboard-1", // should be slugified + }, + }, res) + assert.Equal(t, "dash-db", query.Type) // query type should be added +} + +func TestSearchProvisionedDashboardsThroughK8sRaw(t *testing.T) { + ctx := context.Background() + k8sCliMock := new(client.MockK8sHandler) + service := &DashboardServiceImpl{k8sclient: k8sCliMock} + query := &dashboards.FindPersistedDashboardsQuery{ + OrgId: 1, + } + dashboardUnstructuredProvisioned := unstructured.Unstructured{Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "annotations": map[string]any{ + utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test", + utils.AnnoKeyRepoHash: "hash", + utils.AnnoKeyRepoPath: "path/to/file", + utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z", + }, + }, + "spec": map[string]any{}, + }} + dashboardUnstructuredNotProvisioned := unstructured.Unstructured{Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid2", + }, + "spec": map[string]any{}, + }} + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder1"), + }, + }, + { + Key: &resource.ResourceKey{ + Name: "uid2", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 2"), + []byte("folder2"), + }, + }, + }, + }, + TotalHits: 1, + }, nil) + k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructuredProvisioned, nil).Once() + k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructuredNotProvisioned, nil).Once() + res, err := service.searchProvisionedDashboardsThroughK8s(ctx, query) + require.NoError(t, err) + assert.Equal(t, []*dashboardProvisioningWithUID{ + { + DashboardUID: "uid", + DashboardProvisioning: dashboards.DashboardProvisioning{ + Name: "test", + ExternalID: "path/to/file", + CheckSum: "hash", + Updated: 1735689600, + }, + }, + }, res) // only should return the one provisioned dashboard + assert.Equal(t, "dash-db", query.Type) // query type should be added as dashboards only +} + func TestLegacySaveCommandToUnstructured(t *testing.T) { namespace := "test-namespace" t.Run("successfully converts save command to unstructured", func(t *testing.T) { diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index b97ca614cc5..9ac7c8a2e1e 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -37,6 +37,7 @@ import ( "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/services/user" @@ -1028,7 +1029,12 @@ func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderComm // if dashboard restore is on we don't delete public dashboards, the hard delete will take care of it later if !s.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) { // We need a list of dashboard uids inside the folder to delete related public dashboards - dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{SignedInUser: cmd.SignedInUser, FolderUIDs: folderUIDs, OrgId: cmd.OrgID}) + dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{ + SignedInUser: cmd.SignedInUser, + FolderUIDs: folderUIDs, + OrgId: cmd.OrgID, + Type: searchstore.TypeDashboard, + }) if err != nil { return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err) } diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index 6d4ae13da06..203aa4f37ab 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -31,6 +31,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/search/model" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -737,7 +738,12 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol } } } else { - dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{SignedInUser: cmd.SignedInUser, FolderUIDs: folders, OrgId: cmd.OrgID}) + dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{ + SignedInUser: cmd.SignedInUser, + FolderUIDs: folders, + OrgId: cmd.OrgID, + Type: searchstore.TypeDashboard, + }) if err != nil { return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err) } diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index fb946bafb77..306352fce84 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -252,6 +253,7 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn // then find the dashboards that were supposed to be connected to this element _, requester := identity.WithServiceIdentity(c, signedInUser.GetOrgID()) dashs, err := l.dashboardsService.FindDashboards(c, &dashboards.FindPersistedDashboardsQuery{ + Type: searchstore.TypeDashboard, OrgId: signedInUser.GetOrgID(), DashboardIds: dashboardIDs, SignedInUser: requester, // a user may be able to delete a library element but not read all dashboards. We still need to run this check, so we don't allow deleting elements if dashboards are connected diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index b5459992780..d3239e2857d 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -25,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/services/query" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -375,7 +376,13 @@ func (pd *PublicDashboardServiceImpl) FindAllWithPagination(ctx context.Context, dashUIDs[i] = pubdash.DashboardUid } - dashboardsFound, err := pd.dashboardService.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{OrgId: query.OrgID, DashboardUIDs: dashUIDs, SignedInUser: query.User, Limit: int64(len(dashUIDs))}) + dashboardsFound, err := pd.dashboardService.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{ + OrgId: query.OrgID, + DashboardUIDs: dashUIDs, + SignedInUser: query.User, + Limit: int64(len(dashUIDs)), + Type: searchstore.TypeDashboard, + }) if err != nil { return nil, ErrInternalServerError.Errorf("FindAllWithPagination: GetDashboards: %w", err) } From 36275a551041d507896c9c061b1d7e6398e73bba Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 6 Feb 2025 09:55:17 +0100 Subject: [PATCH 374/894] Advisor: Refactor check interfaces (#100043) --- apps/advisor/pkg/app/app.go | 2 +- .../pkg/app/checks/datasourcecheck/check.go | 102 ++++++++++++++---- .../app/checks/datasourcecheck/check_test.go | 43 ++++++-- apps/advisor/pkg/app/checks/ifaces.go | 22 +++- .../pkg/app/checks/plugincheck/check.go | 100 ++++++++++++++--- .../pkg/app/checks/plugincheck/check_test.go | 13 ++- apps/advisor/pkg/app/utils.go | 24 ++++- apps/advisor/pkg/app/utils_test.go | 38 ++++++- 8 files changed, 284 insertions(+), 60 deletions(-) diff --git a/apps/advisor/pkg/app/app.go b/apps/advisor/pkg/app/app.go index 42848758d5a..554357caca2 100644 --- a/apps/advisor/pkg/app/app.go +++ b/apps/advisor/pkg/app/app.go @@ -37,7 +37,7 @@ func New(cfg app.Config) (app.App, error) { // Initialize checks checkMap := map[string]checks.Check{} for _, c := range checkRegistry.Checks() { - checkMap[c.Type()] = c + checkMap[c.ID()] = c } simpleConfig := simple.AppConfig{ diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go index df05bf40688..f7dbcc53024 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -15,6 +15,13 @@ import ( "k8s.io/klog/v2" ) +type check struct { + DatasourceSvc datasources.DataSourceService + PluginStore pluginstore.Store + PluginContextProvider pluginContextProvider + PluginClient plugins.Client +} + func New( datasourceSvc datasources.DataSourceService, pluginStore pluginstore.Store, @@ -29,28 +36,53 @@ func New( } } -type check struct { - DatasourceSvc datasources.DataSourceService - PluginStore pluginstore.Store - PluginContextProvider pluginContextProvider - PluginClient plugins.Client -} - -func (c *check) Type() string { - return "datasource" -} - -func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.CheckV0alpha1StatusReport, error) { - // Optionally read the check input encoded in the object - // fmt.Println(obj.Data) - +func (c *check) Items(ctx context.Context) ([]any, error) { dss, err := c.DatasourceSvc.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{}) if err != nil { return nil, err } + res := make([]any, len(dss)) + for i, ds := range dss { + res[i] = ds + } + return res, nil +} +func (c *check) ID() string { + return "datasource" +} + +func (c *check) Steps() []checks.Step { + return []checks.Step{ + &uidValidationStep{}, + &healthCheckStep{ + PluginContextProvider: c.PluginContextProvider, + PluginClient: c.PluginClient, + }, + } +} + +type uidValidationStep struct{} + +func (s *uidValidationStep) ID() string { + return "uid-validation" +} + +func (s *uidValidationStep) Title() string { + return "UID validation" +} + +func (s *uidValidationStep) Description() string { + return "Check if the UID of each data source is valid." +} + +func (s *uidValidationStep) Run(ctx context.Context, obj *advisor.CheckSpec, items []any) ([]advisor.CheckReportError, error) { dsErrs := []advisor.CheckReportError{} - for _, ds := range dss { + for _, i := range items { + ds, ok := i.(*datasources.DataSource) + if !ok { + return nil, fmt.Errorf("invalid item type %T", i) + } // Data source UID validation err := util.ValidateUID(ds.UID) if err != nil { @@ -60,13 +92,41 @@ func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.Check Action: "Check the documentation for more information.", }) } + } + return dsErrs, nil +} + +type healthCheckStep struct { + PluginContextProvider pluginContextProvider + PluginClient plugins.Client +} + +func (s *healthCheckStep) Title() string { + return "Health check" +} + +func (s *healthCheckStep) Description() string { + return "Check if all data sources are healthy." +} + +func (s *healthCheckStep) ID() string { + return "health-check" +} + +func (s *healthCheckStep) Run(ctx context.Context, obj *advisor.CheckSpec, items []any) ([]advisor.CheckReportError, error) { + dsErrs := []advisor.CheckReportError{} + for _, i := range items { + ds, ok := i.(*datasources.DataSource) + if !ok { + return nil, fmt.Errorf("invalid item type %T", i) + } // Health check execution requester, err := identity.GetRequester(ctx) if err != nil { return nil, err } - pCtx, err := c.PluginContextProvider.GetWithDataSource(ctx, ds.Type, requester, ds) + pCtx, err := s.PluginContextProvider.GetWithDataSource(ctx, ds.Type, requester, ds) if err != nil { klog.ErrorS(err, "Error creating plugin context", "datasource", ds.Name) continue @@ -75,7 +135,7 @@ func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.Check PluginContext: pCtx, Headers: map[string]string{}, } - resp, err := c.PluginClient.CheckHealth(ctx, req) + resp, err := s.PluginClient.CheckHealth(ctx, req) if err != nil { fmt.Println("Error checking health", err) continue @@ -90,11 +150,7 @@ func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.Check }) } } - - return &advisor.CheckV0alpha1StatusReport{ - Count: int64(len(dss)), - Errors: dsErrs, - }, nil + return dsErrs, nil } type pluginContextProvider interface { diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go index 5c3dfb913e3..c360f96ad14 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go @@ -31,11 +31,18 @@ func TestCheck_Run(t *testing.T) { } ctx := identity.WithRequester(context.Background(), &user.SignedInUser{}) - report, err := check.Run(ctx, &advisor.CheckSpec{}) + items, err := check.Items(ctx) + assert.NoError(t, err) + errs := []advisor.CheckReportError{} + for _, step := range check.Steps() { + stepErrs, err := step.Run(ctx, &advisor.CheckSpec{}, items) + assert.NoError(t, err) + errs = append(errs, stepErrs...) + } assert.NoError(t, err) - assert.Equal(t, int64(2), report.Count) - assert.Empty(t, report.Errors) + assert.Equal(t, 2, len(items)) + assert.Empty(t, errs) }) t.Run("should return errors when datasource UID is invalid", func(t *testing.T) { @@ -54,12 +61,19 @@ func TestCheck_Run(t *testing.T) { } ctx := identity.WithRequester(context.Background(), &user.SignedInUser{}) - report, err := check.Run(ctx, &advisor.CheckSpec{}) + items, err := check.Items(ctx) + assert.NoError(t, err) + errs := []advisor.CheckReportError{} + for _, step := range check.Steps() { + stepErrs, err := step.Run(ctx, &advisor.CheckSpec{}, items) + assert.NoError(t, err) + errs = append(errs, stepErrs...) + } assert.NoError(t, err) - assert.Equal(t, int64(1), report.Count) - assert.Len(t, report.Errors, 1) - assert.Equal(t, "Invalid UID 'invalid uid' for data source Prometheus", report.Errors[0].Reason) + assert.Equal(t, 1, len(items)) + assert.Len(t, errs, 1) + assert.Equal(t, "Invalid UID 'invalid uid' for data source Prometheus", errs[0].Reason) }) t.Run("should return errors when datasource health check fails", func(t *testing.T) { @@ -78,12 +92,19 @@ func TestCheck_Run(t *testing.T) { } ctx := identity.WithRequester(context.Background(), &user.SignedInUser{}) - report, err := check.Run(ctx, &advisor.CheckSpec{}) + items, err := check.Items(ctx) + assert.NoError(t, err) + errs := []advisor.CheckReportError{} + for _, step := range check.Steps() { + stepErrs, err := step.Run(ctx, &advisor.CheckSpec{}, items) + assert.NoError(t, err) + errs = append(errs, stepErrs...) + } assert.NoError(t, err) - assert.Equal(t, int64(1), report.Count) - assert.Len(t, report.Errors, 1) - assert.Equal(t, "Health check failed for Prometheus", report.Errors[0].Reason) + assert.Equal(t, 1, len(items)) + assert.Len(t, errs, 1) + assert.Equal(t, "Health check failed for Prometheus", errs[0].Reason) }) } diff --git a/apps/advisor/pkg/app/checks/ifaces.go b/apps/advisor/pkg/app/checks/ifaces.go index 008f5656298..b630a84871c 100644 --- a/apps/advisor/pkg/app/checks/ifaces.go +++ b/apps/advisor/pkg/app/checks/ifaces.go @@ -6,8 +6,24 @@ import ( advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" ) -// Check defines the methods that a check must implement to be executed. +// Check returns metadata about the check being executed and the list of Steps type Check interface { - Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec) (*advisorv0alpha1.CheckV0alpha1StatusReport, error) - Type() string + // ID returns the unique identifier of the check + ID() string + // Items returns the list of items that will be checked + Items(ctx context.Context) ([]any, error) + // Steps returns the list of steps that will be executed + Steps() []Step +} + +// Step is a single step in a check, including its metadata +type Step interface { + // ID returns the unique identifier of the step + ID() string + // Title returns the title of the step + Title() string + // Description returns the description of the step + Description() string + // Run executes the step and returns a list of errors + Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec, items []any) ([]advisorv0alpha1.CheckReportError, error) } diff --git a/apps/advisor/pkg/app/checks/plugincheck/check.go b/apps/advisor/pkg/app/checks/plugincheck/check.go index 8a20820f450..c64bc1e7579 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check.go @@ -36,22 +36,63 @@ type check struct { ManagedPlugins managedplugins.Manager } -func (c *check) Type() string { +func (c *check) ID() string { return "plugin" } -func (c *check) Run(ctx context.Context, _ *advisor.CheckSpec) (*advisor.CheckV0alpha1StatusReport, error) { +func (c *check) Items(ctx context.Context) ([]any, error) { ps := c.PluginStore.Plugins(ctx) + res := make([]any, len(ps)) + for i, p := range ps { + res[i] = p + } + return res, nil +} +func (c *check) Steps() []checks.Step { + return []checks.Step{ + &deprecationStep{ + PluginRepo: c.PluginRepo, + }, + &updateStep{ + PluginRepo: c.PluginRepo, + PluginPreinstall: c.PluginPreinstall, + ManagedPlugins: c.ManagedPlugins, + }, + } +} + +type deprecationStep struct { + PluginRepo repo.Service +} + +func (s *deprecationStep) Title() string { + return "Deprecation check" +} + +func (s *deprecationStep) Description() string { + return "Check if any installed plugins are deprecated." +} + +func (s *deprecationStep) ID() string { + return "deprecation" +} + +func (s *deprecationStep) Run(ctx context.Context, _ *advisor.CheckSpec, items []any) ([]advisor.CheckReportError, error) { errs := []advisor.CheckReportError{} - for _, p := range ps { + for _, i := range items { + p, ok := i.(pluginstore.Plugin) + if !ok { + return nil, fmt.Errorf("invalid item type %T", i) + } + // Skip if it's a core plugin if p.IsCorePlugin() { continue } // Check if plugin is deprecated - i, err := c.PluginRepo.PluginInfo(ctx, p.ID) + i, err := s.PluginRepo.PluginInfo(ctx, p.ID) if err != nil { continue } @@ -62,13 +103,49 @@ func (c *check) Run(ctx context.Context, _ *advisor.CheckSpec) (*advisor.CheckV0 Action: "Check the documentation for recommended steps.", }) } + } + return errs, nil +} - // Check if plugin has a newer version, only if it's not managed or pinned - if c.isManaged(ctx, p.ID) || c.PluginPreinstall.IsPinned(p.ID) { +type updateStep struct { + PluginRepo repo.Service + PluginPreinstall plugininstaller.Preinstall + ManagedPlugins managedplugins.Manager +} + +func (s *updateStep) Title() string { + return "Update check" +} + +func (s *updateStep) Description() string { + return "Check if any installed plugins have a newer version available." +} + +func (s *updateStep) ID() string { + return "update" +} + +func (s *updateStep) Run(ctx context.Context, _ *advisor.CheckSpec, items []any) ([]advisor.CheckReportError, error) { + errs := []advisor.CheckReportError{} + for _, i := range items { + p, ok := i.(pluginstore.Plugin) + if !ok { + return nil, fmt.Errorf("invalid item type %T", i) + } + + // Skip if it's a core plugin + if p.IsCorePlugin() { continue } + + // Skip if it's managed or pinned + if s.isManaged(ctx, p.ID) || s.PluginPreinstall.IsPinned(p.ID) { + continue + } + + // Check if plugin has a newer version available compatOpts := repo.NewCompatOpts(services.GrafanaVersion, sysruntime.GOOS, sysruntime.GOARCH) - info, err := c.PluginRepo.GetPluginArchiveInfo(ctx, p.ID, "", compatOpts) + info, err := s.PluginRepo.GetPluginArchiveInfo(ctx, p.ID, "", compatOpts) if err != nil { continue } @@ -83,10 +160,7 @@ func (c *check) Run(ctx context.Context, _ *advisor.CheckSpec) (*advisor.CheckV0 } } - return &advisor.CheckV0alpha1StatusReport{ - Count: int64(len(ps)), - Errors: errs, - }, nil + return errs, nil } func hasUpdate(current pluginstore.Plugin, latest *repo.PluginArchiveInfo) bool { @@ -100,8 +174,8 @@ func hasUpdate(current pluginstore.Plugin, latest *repo.PluginArchiveInfo) bool return current.Info.Version != latest.Version } -func (c *check) isManaged(ctx context.Context, pluginID string) bool { - for _, managedPlugin := range c.ManagedPlugins.ManagedPlugins(ctx) { +func (s *updateStep) isManaged(ctx context.Context, pluginID string) bool { + for _, managedPlugin := range s.ManagedPlugins.ManagedPlugins(ctx) { if managedPlugin == pluginID { return true } diff --git a/apps/advisor/pkg/app/checks/plugincheck/check_test.go b/apps/advisor/pkg/app/checks/plugincheck/check_test.go index 41c0705e904..53450ce7bc5 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check_test.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check_test.go @@ -126,10 +126,17 @@ func TestRun(t *testing.T) { managedPlugins := &mockManagedPlugins{managed: tt.pluginManaged} check := New(pluginStore, pluginRepo, pluginPreinstall, managedPlugins) - report, err := check.Run(context.Background(), nil) + items, err := check.Items(context.Background()) assert.NoError(t, err) - assert.Equal(t, int64(len(tt.plugins)), report.Count) - assert.Equal(t, tt.expectedErrors, report.Errors) + errs := []advisor.CheckReportError{} + for _, step := range check.Steps() { + stepErrs, err := step.Run(context.Background(), &advisor.CheckSpec{}, items) + assert.NoError(t, err) + errs = append(errs, stepErrs...) + } + assert.NoError(t, err) + assert.Equal(t, len(tt.plugins), len(items)) + assert.Equal(t, tt.expectedErrors, errs) }) } } diff --git a/apps/advisor/pkg/app/utils.go b/apps/advisor/pkg/app/utils.go index 4eae090198b..c59dbaca21e 100644 --- a/apps/advisor/pkg/app/utils.go +++ b/apps/advisor/pkg/app/utils.go @@ -72,14 +72,32 @@ func processCheck(ctx context.Context, client resource.Client, obj resource.Obje UserUID: uid, FallbackType: typ, }) - // Run the checks - report, err := check.Run(ctx, &c.Spec) + // Get the items to check + items, err := check.Items(ctx) if err != nil { setErr := setStatusAnnotation(ctx, client, obj, "error") if setErr != nil { return setErr } - return err + return fmt.Errorf("error initializing check: %w", err) + } + // Run the steps + steps := check.Steps() + errs := []advisorv0alpha1.CheckReportError{} + for _, step := range steps { + stepErrs, err := step.Run(ctx, &c.Spec, items) + if err != nil { + setErr := setStatusAnnotation(ctx, client, obj, "error") + if setErr != nil { + return setErr + } + return fmt.Errorf("error running step %s: %w", step.Title(), err) + } + errs = append(errs, stepErrs...) + } + report := &advisorv0alpha1.CheckV0alpha1StatusReport{ + Errors: errs, + Count: int64(len(items)), } err = setStatusAnnotation(ctx, client, obj, "processed") if err != nil { diff --git a/apps/advisor/pkg/app/utils_test.go b/apps/advisor/pkg/app/utils_test.go index 6626f645eb7..a13880f8209 100644 --- a/apps/advisor/pkg/app/utils_test.go +++ b/apps/advisor/pkg/app/utils_test.go @@ -115,10 +115,42 @@ func (m *mockClient) PatchInto(ctx context.Context, id resource.Identifier, req } type mockCheck struct { - checks.Check err error } -func (m *mockCheck) Run(ctx context.Context, spec *advisorv0alpha1.CheckSpec) (*advisorv0alpha1.CheckV0alpha1StatusReport, error) { - return &advisorv0alpha1.CheckV0alpha1StatusReport{}, m.err +func (m *mockCheck) ID() string { + return "mock" +} + +func (m *mockCheck) Items(ctx context.Context) ([]any, error) { + return []any{}, nil +} + +func (m *mockCheck) Steps() []checks.Step { + return []checks.Step{ + &mockStep{err: m.err}, + } +} + +type mockStep struct { + err error +} + +func (m *mockStep) Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec, items []any) ([]advisorv0alpha1.CheckReportError, error) { + if m.err != nil { + return nil, m.err + } + return nil, nil +} + +func (m *mockStep) Title() string { + return "mock" +} + +func (m *mockStep) Description() string { + return "mock" +} + +func (m *mockStep) ID() string { + return "mock" } From f89da88f0fbdbc7d253171f1e92b5c6fe09ae5c7 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 6 Feb 2025 09:16:47 +0000 Subject: [PATCH 375/894] Storybook: Support an arbitrary number of themes (#100111) * support more themes in storybook * default to dark theme * fix type error * change theme in docs container * add TODO * only show extra themes in development mode * add comment --- .github/renovate.json5 | 1 - packages/grafana-ui/.storybook/main.ts | 1 - packages/grafana-ui/.storybook/manager.ts | 6 +++-- packages/grafana-ui/.storybook/preview.ts | 26 ++++++++++++------ .../grafana-ui/.storybook/storybookTheme.ts | 10 ++----- packages/grafana-ui/package.json | 1 - .../utils/storybook/ThemedDocsContainer.tsx | 19 +++++++++---- .../src/utils/storybook/withTheme.tsx | 20 ++++++++------ yarn.lock | 27 ++++--------------- 9 files changed, 55 insertions(+), 56 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 6cc2ac80423..a782ac2c0a0 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -13,7 +13,6 @@ "slate-react", // we don't want to continue using this on the long run, use Monaco editor instead of Slate "@types/slate-react", // we don't want to continue using this on the long run, use Monaco editor instead of Slate "@types/slate", // we don't want to continue using this on the long run, use Monaco editor instead of Slate - "storybook-dark-mode", // 4.0.2 causes storybook 8.4 to break with react hooks errors // Temporarily pause updating lerna and nx until we resolve build issues "lerna", "nx" diff --git a/packages/grafana-ui/.storybook/main.ts b/packages/grafana-ui/.storybook/main.ts index 5b169002dc9..401a79ef7ea 100644 --- a/packages/grafana-ui/.storybook/main.ts +++ b/packages/grafana-ui/.storybook/main.ts @@ -43,7 +43,6 @@ const mainConfig: StorybookConfig = { }, }, getAbsolutePath('@storybook/addon-storysource'), - getAbsolutePath('storybook-dark-mode'), getAbsolutePath('@storybook/addon-webpack5-compiler-swc'), ], framework: { diff --git a/packages/grafana-ui/.storybook/manager.ts b/packages/grafana-ui/.storybook/manager.ts index 1caddf59335..1a32c4aeaa4 100644 --- a/packages/grafana-ui/.storybook/manager.ts +++ b/packages/grafana-ui/.storybook/manager.ts @@ -1,6 +1,8 @@ import { addons } from '@storybook/manager-api'; -import { GrafanaDark } from './storybookTheme'; +import { getThemeById } from '@grafana/data'; +import { createStorybookTheme } from './storybookTheme'; +const systemTheme = getThemeById('system'); addons.setConfig({ isFullscreen: false, panelPosition: 'right', @@ -10,5 +12,5 @@ addons.setConfig({ sidebar: { showRoots: true, }, - theme: GrafanaDark, + theme: createStorybookTheme(systemTheme), }); diff --git a/packages/grafana-ui/.storybook/preview.ts b/packages/grafana-ui/.storybook/preview.ts index cc4be9bfbe8..a8dc3e9f062 100644 --- a/packages/grafana-ui/.storybook/preview.ts +++ b/packages/grafana-ui/.storybook/preview.ts @@ -1,6 +1,6 @@ import { Preview } from '@storybook/react'; import 'jquery'; -import { getTimeZone, getTimeZones } from '@grafana/data'; +import { getBuiltInThemes, getTimeZone, getTimeZones, GrafanaTheme2 } from '@grafana/data'; import '../../../public/vendor/flot/jquery.flot.js'; import '../../../public/vendor/flot/jquery.flot.selection'; @@ -20,10 +20,9 @@ import { ThemedDocsContainer } from '../src/utils/storybook/ThemedDocsContainer' import lightTheme from '../../../public/sass/grafana.light.scss'; // @ts-ignore import darkTheme from '../../../public/sass/grafana.dark.scss'; -import { GrafanaDark, GrafanaLight } from './storybookTheme'; -const handleThemeChange = (theme: any) => { - if (theme !== 'light') { +const handleThemeChange = (theme: GrafanaTheme2) => { + if (theme.colors.mode !== 'light') { lightTheme.unuse(); darkTheme.use(); } else { @@ -32,14 +31,12 @@ const handleThemeChange = (theme: any) => { } }; +const showExtraThemes = process.env.NODE_ENV === 'development'; + const preview: Preview = { decorators: [withTheme(handleThemeChange), withTimeZone()], parameters: { actions: { argTypesRegex: '^on[A-Z].*' }, - darkMode: { - dark: GrafanaDark, - light: GrafanaLight, - }, docs: { container: ThemedDocsContainer, }, @@ -68,6 +65,19 @@ const preview: Preview = { }, }, globalTypes: { + theme: { + name: 'Theme', + description: 'Global theme for components', + defaultValue: 'system', + toolbar: { + icon: 'paintbrush', + items: getBuiltInThemes(showExtraThemes).map((theme) => ({ + value: theme.id, + title: theme.name, + })), + showName: true, + }, + }, timeZone: { description: 'Set the timezone for the storybook preview', defaultValue: getTimeZone(), diff --git a/packages/grafana-ui/.storybook/storybookTheme.ts b/packages/grafana-ui/.storybook/storybookTheme.ts index 32649630d9f..7bb3b29e2cb 100644 --- a/packages/grafana-ui/.storybook/storybookTheme.ts +++ b/packages/grafana-ui/.storybook/storybookTheme.ts @@ -1,8 +1,7 @@ -import { GrafanaTheme2, createTheme } from '@grafana/data'; -//@ts-ignore +import { GrafanaTheme2 } from '@grafana/data'; import { create } from '@storybook/theming'; -const createStorybookTheme = (theme: GrafanaTheme2) => { +export const createStorybookTheme = (theme: GrafanaTheme2) => { return create({ base: theme.colors.mode, colorPrimary: theme.colors.primary.main, @@ -38,8 +37,3 @@ const createStorybookTheme = (theme: GrafanaTheme2) => { brandImage: `public/img/grafana_text_logo-${theme.colors.mode}.svg`, }); }; - -const GrafanaLight = createStorybookTheme(createTheme({ colors: { mode: 'light' } })); -const GrafanaDark = createStorybookTheme(createTheme({ colors: { mode: 'dark' } })); - -export { GrafanaLight, GrafanaDark }; diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index ebe6a77ecdc..c7bc33a0a57 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -180,7 +180,6 @@ "rollup-plugin-svg-import": "3.0.0", "sass-loader": "16.0.4", "storybook": "^8.4.2", - "storybook-dark-mode": "4.0.1", "style-loader": "4.0.0", "typescript": "5.7.3", "webpack": "5.97.1" diff --git a/packages/grafana-ui/src/utils/storybook/ThemedDocsContainer.tsx b/packages/grafana-ui/src/utils/storybook/ThemedDocsContainer.tsx index 1c989e3fd3d..d6485671d84 100644 --- a/packages/grafana-ui/src/utils/storybook/ThemedDocsContainer.tsx +++ b/packages/grafana-ui/src/utils/storybook/ThemedDocsContainer.tsx @@ -1,9 +1,10 @@ -// Wrap the DocsContainer for storybook-dark-mode theme switching support. +// Wrap the DocsContainer for theme switching support. import { DocsContainer, DocsContextProps } from '@storybook/addon-docs'; import * as React from 'react'; -import { useDarkMode } from 'storybook-dark-mode'; -import { GrafanaLight, GrafanaDark } from '../../../.storybook/storybookTheme'; +import { getThemeById } from '@grafana/data'; + +import { createStorybookTheme } from '../../../.storybook/storybookTheme'; import { GlobalStyles } from '../../themes'; type Props = { @@ -12,10 +13,18 @@ type Props = { }; export const ThemedDocsContainer = ({ children, context }: Props) => { - const dark = useDarkMode(); + // Default to system theme for pages that don't have associated stories + // Currently this is only the case for the docs `Intro` page + let themeId = 'system'; + if (context.componentStories().length > 0) { + const story = context.storyById(); + const { globals } = context.getStoryContext(story); + themeId = globals.theme; + } + const theme = getThemeById(themeId); return ( - + {children} diff --git a/packages/grafana-ui/src/utils/storybook/withTheme.tsx b/packages/grafana-ui/src/utils/storybook/withTheme.tsx index d865769c5ab..aa1f761f85f 100644 --- a/packages/grafana-ui/src/utils/storybook/withTheme.tsx +++ b/packages/grafana-ui/src/utils/storybook/withTheme.tsx @@ -1,17 +1,17 @@ import { Decorator } from '@storybook/react'; import * as React from 'react'; -import { useDarkMode } from 'storybook-dark-mode'; -import { createTheme, GrafanaTheme2, ThemeContext } from '@grafana/data'; +import { getThemeById, GrafanaTheme2, ThemeContext } from '@grafana/data'; import { GlobalStyles } from '../../themes/GlobalStyles/GlobalStyles'; type SassThemeChangeHandler = (theme: GrafanaTheme2) => void; -const ThemeableStory = ({ - children, - handleSassThemeChange, -}: React.PropsWithChildren<{ handleSassThemeChange: SassThemeChangeHandler }>) => { - const theme = createTheme({ colors: { mode: useDarkMode() ? 'dark' : 'light' } }); +interface ThemeableStoryProps { + themeId: string; + handleSassThemeChange: SassThemeChangeHandler; +} +const ThemeableStory = ({ children, handleSassThemeChange, themeId }: React.PropsWithChildren) => { + const theme = getThemeById(themeId); handleSassThemeChange(theme); @@ -38,4 +38,8 @@ const ThemeableStory = ({ export const withTheme = (handleSassThemeChange: SassThemeChangeHandler): Decorator => // eslint-disable-next-line react/display-name - (story) => {story()}; + (story, context) => ( + + {story()} + + ); diff --git a/yarn.lock b/yarn.lock index 3ebde167b43..1c4fee5ed1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4129,7 +4129,6 @@ __metadata: slate-plain-serializer: "npm:0.7.13" slate-react: "npm:0.22.10" storybook: "npm:^8.4.2" - storybook-dark-mode: "npm:4.0.1" style-loader: "npm:4.0.0" tinycolor2: "npm:1.6.0" tslib: "npm:2.8.1" @@ -7524,7 +7523,7 @@ __metadata: languageName: node linkType: hard -"@storybook/components@npm:8.4.4, @storybook/components@npm:^8.0.0, @storybook/components@npm:^8.4.2": +"@storybook/components@npm:8.4.4, @storybook/components@npm:^8.4.2": version: 8.4.4 resolution: "@storybook/components@npm:8.4.4" peerDependencies: @@ -7533,7 +7532,7 @@ __metadata: languageName: node linkType: hard -"@storybook/core-events@npm:^8.0.0, @storybook/core-events@npm:^8.4.2": +"@storybook/core-events@npm:^8.4.2": version: 8.4.4 resolution: "@storybook/core-events@npm:8.4.4" peerDependencies: @@ -7605,7 +7604,7 @@ __metadata: languageName: node linkType: hard -"@storybook/icons@npm:^1.2.12, @storybook/icons@npm:^1.2.5": +"@storybook/icons@npm:^1.2.12": version: 1.2.12 resolution: "@storybook/icons@npm:1.2.12" peerDependencies: @@ -7615,7 +7614,7 @@ __metadata: languageName: node linkType: hard -"@storybook/manager-api@npm:8.4.4, @storybook/manager-api@npm:^8.0.0, @storybook/manager-api@npm:^8.4.2": +"@storybook/manager-api@npm:8.4.4, @storybook/manager-api@npm:^8.4.2": version: 8.4.4 resolution: "@storybook/manager-api@npm:8.4.4" peerDependencies: @@ -7766,7 +7765,7 @@ __metadata: languageName: node linkType: hard -"@storybook/theming@npm:8.4.4, @storybook/theming@npm:^8.0.0, @storybook/theming@npm:^8.4.2": +"@storybook/theming@npm:8.4.4, @storybook/theming@npm:^8.4.2": version: 8.4.4 resolution: "@storybook/theming@npm:8.4.4" peerDependencies: @@ -28886,22 +28885,6 @@ __metadata: languageName: node linkType: hard -"storybook-dark-mode@npm:4.0.1": - version: 4.0.1 - resolution: "storybook-dark-mode@npm:4.0.1" - dependencies: - "@storybook/components": "npm:^8.0.0" - "@storybook/core-events": "npm:^8.0.0" - "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.2.5" - "@storybook/manager-api": "npm:^8.0.0" - "@storybook/theming": "npm:^8.0.0" - fast-deep-equal: "npm:^3.1.3" - memoizerific: "npm:^1.11.3" - checksum: 10/3225e5bdaba0ea76b65d642202d9712d7de234e3b5673fb46e444892ab114be207dd287778e2002b662ec35bb8153d2624ff280ce51c5299fb13c711431dad40 - languageName: node - linkType: hard - "storybook@npm:^8.4.2": version: 8.4.4 resolution: "storybook@npm:8.4.4" From d5f1f4eb5cffde05f4e88b546fbece9cd51e353f Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Thu, 6 Feb 2025 03:33:08 -0600 Subject: [PATCH 376/894] ComboBox: Fall back to substring matching for symbols/operators (#100148) --- .../src/components/Combobox/filter.test.ts | 13 +++++++++++++ .../grafana-ui/src/components/Combobox/filter.ts | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/packages/grafana-ui/src/components/Combobox/filter.test.ts b/packages/grafana-ui/src/components/Combobox/filter.test.ts index f7a5eb534fd..8b3c13ed354 100644 --- a/packages/grafana-ui/src/components/Combobox/filter.test.ts +++ b/packages/grafana-ui/src/components/Combobox/filter.test.ts @@ -58,4 +58,17 @@ describe('combobox filter', () => { expect(matches.map((m) => m.value)).toEqual(['台南市', '南投縣']); }); }); + + describe('operators', () => { + it('should do substring match when needle is only symbols', () => { + const needle = '='; + + const stringOptions = ['=', '<=', '>', '!~']; + const options = stringOptions.map((value) => ({ value })); + + const matches = fuzzyFind(options, stringOptions, needle); + + expect(matches.map((m) => m.value)).toEqual(['=', '<=']); + }); + }); }); diff --git a/packages/grafana-ui/src/components/Combobox/filter.ts b/packages/grafana-ui/src/components/Combobox/filter.ts index a03cbec475c..7653286f42a 100644 --- a/packages/grafana-ui/src/components/Combobox/filter.ts +++ b/packages/grafana-ui/src/components/Combobox/filter.ts @@ -4,6 +4,9 @@ import { ALL_OPTION_VALUE, ComboboxOption } from './types'; // https://catonmat.net/my-favorite-regex :) const REGEXP_NON_ASCII = /[^ -~]/m; +// https://www.asciitable.com/ +// matches only these: `~!@#$%^&*()_+-=[]\{}|;':",./<>? +const REGEXP_ONLY_SYMBOLS = /^[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+$/m; // limit max terms in needle that qualify for re-ordering const outOfOrderLimit = 5; // beyond 25 chars fall back to substring search @@ -51,6 +54,8 @@ export function fuzzyFind( else if ( // contains non-ascii REGEXP_NON_ASCII.test(needle) || + // is only ascii symbols (operators) + REGEXP_ONLY_SYMBOLS.test(needle) || // too long (often copy-paste from somewhere) needle.length > maxNeedleLength || uf.split(needle).length > maxFuzzyTerms From 0916994d0af3107853393557ab5153414a7f4fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Feb 2025 10:57:08 +0100 Subject: [PATCH 377/894] Dashboard: Various fixes to new layouts (#100107) * Dashboard: Various fixes to new layouts * review fixes * Fix * Update * Fix test --- .../dashboard-scene/scene/DashboardScene.tsx | 16 ++--- .../DefaultGridLayoutManager.test.tsx | 16 ----- .../DefaultGridLayoutManager.tsx | 56 ++--------------- .../ResponsiveGridLayoutManager.tsx | 63 +++++++++++-------- .../scene/layout-rows/RowsLayoutManager.tsx | 8 --- .../scene/types/DashboardLayoutManager.ts | 5 -- .../utils/dashboardSceneGraph.test.ts | 24 ++++++- .../utils/dashboardSceneGraph.ts | 29 ++++++++- 8 files changed, 99 insertions(+), 118 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index e015172d4c6..29c9380039c 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -58,7 +58,13 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { getDashboardUrl } from '../utils/getDashboardUrl'; import { getViewPanelUrl } from '../utils/urlBuilders'; -import { getClosestVizPanel, getDashboardSceneFor, getDefaultVizPanel, getPanelIdForVizPanel } from '../utils/utils'; +import { + getClosestVizPanel, + getDashboardSceneFor, + getDefaultVizPanel, + getLayoutManagerFor, + getPanelIdForVizPanel, +} from '../utils/utils'; import { SchemaV2EditorDrawer } from '../v2schema/SchemaV2EditorDrawer'; import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer'; @@ -475,10 +481,6 @@ export class DashboardScene extends SceneObjectBase { return this._initialState; } - public getNextPanelId(): number { - return this.state.body.getMaxPanelId() + 1; - } - public addPanel(vizPanel: VizPanel): void { if (!this.state.isEditing) { this.onEnterEditMode(); @@ -502,7 +504,7 @@ export class DashboardScene extends SceneObjectBase { } public duplicatePanel(vizPanel: VizPanel) { - this.state.body.duplicatePanel(vizPanel); + getLayoutManagerFor(vizPanel).duplicatePanel(vizPanel); } public copyPanel(vizPanel: VizPanel) { @@ -536,7 +538,7 @@ export class DashboardScene extends SceneObjectBase { } public removePanel(panel: VizPanel) { - this.state.body.removePanel(panel); + getLayoutManagerFor(panel).removePanel(panel); } public unlinkLibraryPanel(panel: VizPanel) { diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx index 8a06e7c9031..fdb9dd9fc38 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx @@ -26,22 +26,6 @@ describe('DefaultGridLayoutManager', () => { }); }); - describe('getMaxPanelId', () => { - it('should get max panel id in a simple 3 panel layout', () => { - const { manager } = setup(); - const id = manager.getMaxPanelId(); - - expect(id).toBe(3); - }); - - it('should return 0 if no panels are found', () => { - const { manager } = setup({ gridItems: [] }); - const id = manager.getMaxPanelId(); - - expect(id).toBe(0); - }); - }); - describe('addPanel', () => { it('Should add a new panel', () => { const { manager } = setup(); diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index d714bc87d4d..c7389618108 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -14,6 +14,7 @@ import { GRID_COLUMN_COUNT } from 'app/core/constants'; import { t } from 'app/core/internationalization'; import { isClonedKey, joinCloneKeys } from '../../utils/clone'; +import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; import { forceRenderChildren, getPanelIdForVizPanel, @@ -21,7 +22,6 @@ import { NEW_PANEL_WIDTH, getVizPanelKeyForPanelId, getGridItemKeyForPanelId, - getDashboardSceneFor, } from '../../utils/utils'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; @@ -72,7 +72,7 @@ export class DefaultGridLayoutManager } public addPanel(vizPanel: VizPanel): void { - const panelId = this.getNextPanelId(); + const panelId = dashboardSceneGraph.getNextPanelId(this); vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) }); vizPanel.clearParent(); @@ -95,7 +95,8 @@ export class DefaultGridLayoutManager * Adds a new empty row */ public addNewRow(): SceneGridRow { - const id = this.getNextPanelId(); + const id = dashboardSceneGraph.getNextPanelId(this); + const row = new SceneGridRow({ key: getVizPanelKeyForPanelId(id), title: 'Row title', @@ -183,7 +184,7 @@ export class DefaultGridLayoutManager let panelData; let newGridItem; - const newPanelId = this.getNextPanelId(); + const newPanelId = dashboardSceneGraph.getNextPanelId(this); const grid = this.state.grid; if (gridItem instanceof DashboardGridItem) { @@ -248,53 +249,6 @@ export class DefaultGridLayoutManager return panels; } - public getMaxPanelId(): number { - let max = 0; - - for (const child of this.state.grid.state.children) { - if (child instanceof DashboardGridItem) { - const vizPanel = child.state.body; - - if (vizPanel) { - const panelId = getPanelIdForVizPanel(vizPanel); - - if (panelId > max) { - max = panelId; - } - } - } - - if (child instanceof SceneGridRow) { - //rows follow the same key pattern --- e.g.: `panel-6` - const panelId = getPanelIdForVizPanel(child); - - if (panelId > max) { - max = panelId; - } - - for (const rowChild of child.state.children) { - if (rowChild instanceof DashboardGridItem) { - const vizPanel = rowChild.state.body; - - if (vizPanel) { - const panelId = getPanelIdForVizPanel(vizPanel); - - if (panelId > max) { - max = panelId; - } - } - } - } - } - } - - return max; - } - - public getNextPanelId(): number { - return getDashboardSceneFor(this).getNextPanelId(); - } - public collapseAllRows(): void { this.state.grid.state.children.forEach((child) => { if (!(child instanceof SceneGridRow)) { diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index a1e4798b6c2..91fcbec0a48 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -4,7 +4,8 @@ import { Select } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; -import { getDashboardSceneFor, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils'; +import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; +import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelId } from '../../utils/utils'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; @@ -33,10 +34,17 @@ export class ResponsiveGridLayoutManager public readonly descriptor = ResponsiveGridLayoutManager.descriptor; + public constructor(state: ResponsiveGridLayoutManagerState) { + super(state); + + //@ts-ignore + this.state.layout.getDragClassCancel = () => 'drag-cancel'; + } + public editModeChanged(isEditing: boolean): void {} public addPanel(vizPanel: VizPanel): void { - const panelId = this.getNextPanelId(); + const panelId = dashboardSceneGraph.getNextPanelId(this); vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) }); vizPanel.clearParent(); @@ -52,33 +60,35 @@ export class ResponsiveGridLayoutManager getDashboardSceneFor(this).switchLayout(rowsLayout); } - public getMaxPanelId(): number { - let max = 0; - - for (const child of this.state.layout.state.children) { - if (child instanceof VizPanel) { - let panelId = getPanelIdForVizPanel(child); - - if (panelId > max) { - max = panelId; - } - } - } - - return max; - } - - public getNextPanelId(): number { - return getDashboardSceneFor(this).getNextPanelId(); - } - public removePanel(panel: VizPanel) { const element = panel.parent; this.state.layout.setState({ children: this.state.layout.state.children.filter((child) => child !== element) }); } public duplicatePanel(panel: VizPanel): void { - throw new Error('Method not implemented.'); + const gridItem = panel.parent; + if (!(gridItem instanceof ResponsiveGridItem)) { + console.error('Trying to duplicate a panel that is not inside a DashboardGridItem'); + return; + } + + const newPanelId = dashboardSceneGraph.getNextPanelId(this); + const grid = this.state.layout; + + const newGridItem = gridItem.clone({ + key: getGridItemKeyForPanelId(newPanelId), + body: panel.clone({ + key: getVizPanelKeyForPanelId(newPanelId), + }), + }); + + const sourceIndex = grid.state.children.indexOf(gridItem); + const newChildren = [...grid.state.children]; + + // insert after + newChildren.splice(sourceIndex + 1, 0, newGridItem); + + grid.setState({ children: newChildren }); } public getVizPanels(): VizPanel[] { @@ -124,9 +134,10 @@ export class ResponsiveGridLayoutManager }); } - activateRepeaters?(): void { - throw new Error('Method not implemented.'); - } + /** + * Might as well implement this as a no-top function as vs-code very eagerly adds optional functions that throw Method not implemented + */ + public activateRepeaters(): void {} public static Component = ({ model }: SceneComponentProps) => { return ; 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 10ff3f66550..dc9154d3b2b 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -75,14 +75,6 @@ export class RowsLayoutManager extends SceneObjectBase i }); } - public getMaxPanelId(): number { - return Math.max(...this.state.rows.map((row) => row.getLayout().getMaxPanelId())); - } - - public getNextPanelId(): number { - return 0; - } - public removePanel(panel: VizPanel) {} public removeRow(row: RowItem) { diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts index 572d7da9549..fbcd4200d36 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts @@ -38,11 +38,6 @@ export interface DashboardLayoutManager extends SceneObject { */ getVizPanels(): VizPanel[]; - /** - * Returns the highest panel id in the layout - */ - getMaxPanelId(): number; - /** * Add row */ diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index 6e0b078ea22..2aa39746339 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -9,7 +9,7 @@ import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { dashboardSceneGraph } from './dashboardSceneGraph'; +import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph'; import { findVizPanelByKey } from './utils'; describe('dashboardSceneGraph', () => { @@ -22,7 +22,7 @@ describe('dashboardSceneGraph', () => { it('should resolve VizPanelLinks object', () => { const scene = buildTestScene(); - const panelWithNoLinks = findVizPanelByKey(scene, 'panel-with-links')!; + const panelWithNoLinks = findVizPanelByKey(scene, 'panel-2')!; expect(dashboardSceneGraph.getPanelLinks(panelWithNoLinks)).toBeInstanceOf(VizPanelLinks); }); }); @@ -66,6 +66,24 @@ describe('dashboardSceneGraph', () => { expect(cursorSync).toBeUndefined(); }); }); + + describe('getNextPanelId', () => { + it('should get next panel id in a simple 3 panel layout', () => { + const scene = buildTestScene(); + const id = getNextPanelId(scene); + + expect(id).toBe(3); + }); + + it('should return 1 if no panels are found', () => { + const scene = buildTestScene(); + + const grid = scene.state.body as DefaultGridLayoutManager; + grid.state.grid.setState({ children: [] }); + const id = getNextPanelId(scene); + expect(id).toBe(1); + }); + }); }); function buildTestScene(overrides?: Partial) { @@ -111,7 +129,7 @@ function buildTestScene(overrides?: Partial) { new DashboardGridItem({ body: new VizPanel({ title: 'Panel D', - key: 'panel-with-links', + key: 'panel-2', pluginId: 'table', $data: new SceneQueryRunner({ key: 'data-query-runner3', queries: [{ refId: 'A' }] }), titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 10edc9904d7..8d1a8176754 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -1,10 +1,11 @@ -import { VizPanel, sceneGraph, behaviors } from '@grafana/scenes'; +import { VizPanel, sceneGraph, behaviors, SceneObject, SceneGridRow } from '@grafana/scenes'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene } from '../scene/DashboardScene'; import { VizPanelLinks } from '../scene/PanelLinks'; -import { getLayoutManagerFor } from './utils'; +import { isClonedKey } from './clone'; +import { getLayoutManagerFor, getPanelIdForVizPanel } from './utils'; function getTimePicker(scene: DashboardScene) { return scene.state.controls?.state.timePicker; @@ -28,6 +29,29 @@ function getVizPanels(scene: DashboardScene): VizPanel[] { return scene.state.body.getVizPanels(); } +/** + * Will look for all panels in the entire scene starting from root + * and find the next free panel id + */ +export function getNextPanelId(scene: SceneObject): number { + let max = 0; + + sceneGraph + .findAllObjects(scene.getRoot(), (obj) => obj instanceof VizPanel || obj instanceof SceneGridRow) + .forEach((panel) => { + if (isClonedKey(panel.state.key!)) { + return; + } + + const panelId = getPanelIdForVizPanel(panel); + if (panelId > max) { + max = panelId; + } + }); + + return max + 1; +} + function getDataLayers(scene: DashboardScene): DashboardDataLayerSet { const data = sceneGraph.getData(scene); @@ -56,4 +80,5 @@ export const dashboardSceneGraph = { getDataLayers, getCursorSync, getLayoutManagerFor, + getNextPanelId, }; From e05413dcc43f72e1323dd3c65fefcbb0400f913d Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 6 Feb 2025 12:07:52 +0100 Subject: [PATCH 378/894] Dashboards+Folders: Ensure the service identity is used for resolvers (#100128) * Dashboards+Folders: Ensure the service identity is used for dashboard and folder resolvers * Add convinient function to call closure with service context --- pkg/api/annotations_test.go | 2 +- pkg/apimachinery/identity/context.go | 13 ++- pkg/services/dashboards/accesscontrol.go | 81 +++++++++---------- pkg/services/dashboards/accesscontrol_test.go | 8 +- .../dashboards/service/dashboard_service.go | 4 +- .../guardian/accesscontrol_guardian_test.go | 2 +- 6 files changed, 58 insertions(+), 52 deletions(-) diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 977f3307ca1..83e11eb91cf 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -403,7 +403,7 @@ func TestAPI_Annotations(t *testing.T) { hs.folderService = folderService hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, hs.Features, dashService, folderService)) - hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderDB, dashService, folderService)) + hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(dashService, folderService)) }) var body io.Reader if tt.body != "" { diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index e71ec51d376..627cace5d61 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -32,7 +32,7 @@ func checkNilRequester(r Requester) bool { const serviceName = "service" -// WithServiceIdentity sets creates an identity representing the service itself in provided org and store it in context. +// WithServiceIdentity sets an identity representing the service itself in provided org and store it in context. // This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with // static permissions so it can be used in legacy code paths. func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) { @@ -53,6 +53,17 @@ func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Req return WithRequester(ctx, r), r } +// WithServiceIdentityContext sets an identity representing the service itself in context. +func WithServiceIdentityContext(ctx context.Context, orgID int64) context.Context { + ctx, _ = WithServiceIdentity(ctx, orgID) + return ctx +} + +// WithServiceIdentityFN calls provided closure with an context contaning the identity of the service. +func WithServiceIdentityFn[T any](ctx context.Context, orgID int64, fn func(ctx context.Context) (T, error)) (T, error) { + return fn(WithServiceIdentityContext(ctx, orgID)) +} + func getWildcardPermissions(actions ...string) map[string][]string { permissions := make(map[string][]string, len(actions)) for _, a := range actions { diff --git a/pkg/services/dashboards/accesscontrol.go b/pkg/services/dashboards/accesscontrol.go index 759c60b2e4a..664874d872e 100644 --- a/pkg/services/dashboards/accesscontrol.go +++ b/pkg/services/dashboards/accesscontrol.go @@ -5,7 +5,7 @@ import ( "errors" "strings" - "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/apimachinery/identity" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/folder" "go.opentelemetry.io/otel" @@ -65,18 +65,19 @@ func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderSvc folder.Serv return []string{ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)}, nil } - folder, err := folderDB.GetFolderByID(ctx, orgID, id) - if err != nil { - return nil, err - } + return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) { + folder, err := folderDB.GetFolderByID(ctx, orgID, id) + if err != nil { + return nil, err + } - result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) - if err != nil { - return nil, err - } + result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) + if err != nil { + return nil, err + } - result = append([]string{ScopeFoldersProvider.GetResourceScopeUID(folder.UID)}, result...) - return result, nil + return append([]string{ScopeFoldersProvider.GetResourceScopeUID(folder.UID)}, result...), nil + }) }) } @@ -97,17 +98,19 @@ func NewFolderUIDScopeResolver(folderSvc folder.Service) (string, ac.ScopeAttrib return nil, err } - inheritedScopes, err := GetInheritedScopes(ctx, orgID, uid, folderSvc) - if err != nil { - return nil, err - } - return append(inheritedScopes, ScopeFoldersProvider.GetResourceScopeUID(uid)), nil + return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) { + inheritedScopes, err := GetInheritedScopes(ctx, orgID, uid, folderSvc) + if err != nil { + return nil, err + } + return append(inheritedScopes, ScopeFoldersProvider.GetResourceScopeUID(uid)), nil + }) }) } // NewDashboardIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "dashboards:id:" // into uid based scopes for both dashboard and folder -func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewDashboardIDScopeResolver(ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { prefix := ScopeDashboardsProvider.GetResourceScope("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewDashboardIDScopeResolver") @@ -122,18 +125,20 @@ func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardServic return nil, err } - dashboard, err := ds.GetDashboard(ctx, &GetDashboardQuery{ID: id, OrgID: orgID}) - if err != nil { - return nil, err - } + return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) { + dashboard, err := ds.GetDashboard(ctx, &GetDashboardQuery{ID: id, OrgID: orgID}) + if err != nil { + return nil, err + } - return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderSvc) + return resolveDashboardScope(ctx, orgID, dashboard, folderSvc) + }) }) } // NewDashboardUIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "dashboards:uid:" // into uid based scopes for both dashboard and folder -func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewDashboardUIDScopeResolver(ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { prefix := ScopeDashboardsProvider.GetResourceScopeUID("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewDashboardUIDScopeResolver") @@ -148,36 +153,26 @@ func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardServi return nil, err } - dashboard, err := ds.GetDashboard(ctx, &GetDashboardQuery{UID: uid, OrgID: orgID}) - if err != nil { - return nil, err - } + return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) { + dashboard, err := ds.GetDashboard(ctx, &GetDashboardQuery{UID: uid, OrgID: orgID}) + if err != nil { + return nil, err + } - return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderSvc) + return resolveDashboardScope(ctx, orgID, dashboard, folderSvc) + }) }) } -func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, orgID int64, dashboard *Dashboard, folderSvc folder.Service) ([]string, error) { +func resolveDashboardScope(ctx context.Context, orgID int64, dashboard *Dashboard, folderSvc folder.Service) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.resolveDashboardScope") span.End() var folderUID string - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - if dashboard.FolderID < 0 { - return []string{ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID)}, nil - } - - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - if dashboard.FolderID == 0 { + if dashboard.FolderUID == "" { folderUID = ac.GeneralFolderUID } else { - folder, err := folderDB.GetFolderByID(ctx, orgID, dashboard.FolderID) - if err != nil { - return nil, err - } - folderUID = folder.UID + folderUID = dashboard.FolderUID } result, err := GetInheritedScopes(ctx, orgID, folderUID, folderSvc) diff --git a/pkg/services/dashboards/accesscontrol_test.go b/pkg/services/dashboards/accesscontrol_test.go index 026081d0dd0..a08313052f8 100644 --- a/pkg/services/dashboards/accesscontrol_test.go +++ b/pkg/services/dashboards/accesscontrol_test.go @@ -60,12 +60,12 @@ func TestNewFolderIDScopeResolver(t *testing.T) { func TestNewDashboardIDScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + prefix, _ := NewDashboardIDScopeResolver(&FakeDashboardService{}, foldertest.NewFakeService()) require.Equal(t, "dashboards:id:", prefix) }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + _, resolver := NewDashboardIDScopeResolver(&FakeDashboardService{}, foldertest.NewFakeService()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "dashboards:uid:123") require.ErrorIs(t, err, ac.ErrInvalidScope) }) @@ -73,12 +73,12 @@ func TestNewDashboardIDScopeResolver(t *testing.T) { func TestNewDashboardUIDScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + prefix, _ := NewDashboardUIDScopeResolver(&FakeDashboardService{}, foldertest.NewFakeService()) require.Equal(t, "dashboards:uid:", prefix) }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + _, resolver := NewDashboardUIDScopeResolver(&FakeDashboardService{}, foldertest.NewFakeService()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "dashboards:id:123") require.ErrorIs(t, err, ac.ErrInvalidScope) }) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index d4abb3627fc..efbd0787a45 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -123,8 +123,8 @@ func ProvideDashboardServiceImpl( return nil, err } - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderStore, dashSvc, folderSvc)) - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, dashSvc, folderSvc)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(dashSvc, folderSvc)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(dashSvc, folderSvc)) if err := folderSvc.RegisterService(dashSvc); err != nil { return nil, err diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index 59ac7d49828..5de4e91da24 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -962,7 +962,7 @@ func setupAccessControlGuardianTest( folderStore := foldertest.NewFakeFolderStore(t) - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, fakeDashboardService, folderSvc)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(fakeDashboardService, folderSvc)) ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(folderSvc)) ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, folderSvc)) From 7d3a77a45c4a48582892a95b60b160b69543e51f Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 6 Feb 2025 11:08:04 +0000 Subject: [PATCH 379/894] Themes: Add new theme definitions behind feature toggle (#100129) * create new toggle * add survey link behind feature toggle * fix translations * better theme structure * add all the themes back * update matrix * fix mars contrast * fix color contrast probs with tron * fix a11y issues with synthwave/victorian themes * fix aubergine/zen * rename green + gold * rename to space * rename, only enable 4 for grafanacon * add survey link * fix info color in sapphiredusk * handle extra themes in storybook --- packages/grafana-data/src/themes/registry.ts | 7 +- .../src/themes/themeDefinitions/aubergine.ts | 55 +++++++++++++ .../themes/themeDefinitions/desertbloom.ts | 79 ++++++++++++++++++ .../themes/themeDefinitions/gildedgrove.ts | 67 +++++++++++++++ .../src/themes/themeDefinitions/index.ts | 10 +++ .../src/themes/themeDefinitions/mars.ts | 55 +++++++++++++ .../src/themes/themeDefinitions/matrix.ts | 43 ++++++++++ .../themes/themeDefinitions/sapphiredusk.ts | 81 +++++++++++++++++++ .../src/themes/themeDefinitions/synthwave.ts | 55 +++++++++++++ .../src/themes/themeDefinitions/tron.ts | 55 +++++++++++++ .../src/themes/themeDefinitions/victorian.ts | 59 ++++++++++++++ .../src/themes/themeDefinitions/zen.ts | 55 +++++++++++++ .../src/types/featureToggles.gen.ts | 1 + packages/grafana-ui/.storybook/preview.ts | 12 ++- pkg/services/featuremgmt/registry.go | 9 +++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 15 ++++ pkg/services/preference/themes.go | 10 +++ .../SharedPreferences/SharedPreferences.tsx | 30 ++++++- public/locales/en-US/grafana.json | 1 + public/locales/pseudo-LOCALE/grafana.json | 1 + 22 files changed, 700 insertions(+), 5 deletions(-) create mode 100644 packages/grafana-data/src/themes/themeDefinitions/aubergine.ts create mode 100644 packages/grafana-data/src/themes/themeDefinitions/desertbloom.ts create mode 100644 packages/grafana-data/src/themes/themeDefinitions/gildedgrove.ts create mode 100644 packages/grafana-data/src/themes/themeDefinitions/mars.ts create mode 100644 packages/grafana-data/src/themes/themeDefinitions/matrix.ts create mode 100644 packages/grafana-data/src/themes/themeDefinitions/sapphiredusk.ts create mode 100644 packages/grafana-data/src/themes/themeDefinitions/synthwave.ts create mode 100644 packages/grafana-data/src/themes/themeDefinitions/tron.ts create mode 100644 packages/grafana-data/src/themes/themeDefinitions/victorian.ts create mode 100644 packages/grafana-data/src/themes/themeDefinitions/zen.ts diff --git a/packages/grafana-data/src/themes/registry.ts b/packages/grafana-data/src/themes/registry.ts index 240e5b75717..c4cf352b622 100644 --- a/packages/grafana-data/src/themes/registry.ts +++ b/packages/grafana-data/src/themes/registry.ts @@ -22,9 +22,12 @@ export function getThemeById(id: string): GrafanaTheme2 { * @internal * For internal use only */ -export function getBuiltInThemes(includeExtras?: boolean) { +export function getBuiltInThemes(allowedExtras: string[]) { const themes = themeRegistry.list().filter((item) => { - return includeExtras ? true : !item.isExtra; + if (item.isExtra) { + return allowedExtras.includes(item.id); + } + return true; }); // sort themes alphabetically, but put built-in themes (default, dark, light, system) first const sortedThemes = themes.sort((a, b) => { diff --git a/packages/grafana-data/src/themes/themeDefinitions/aubergine.ts b/packages/grafana-data/src/themes/themeDefinitions/aubergine.ts new file mode 100644 index 00000000000..45260c2fae0 --- /dev/null +++ b/packages/grafana-data/src/themes/themeDefinitions/aubergine.ts @@ -0,0 +1,55 @@ +import { NewThemeOptions } from '../createTheme'; + +const aubergineTheme: NewThemeOptions = { + name: 'Aubergine', + colors: { + mode: 'dark', + border: { + weak: '#4F2A3D', + medium: '#6A3C4B', + strong: '#8C5A69', + }, + text: { + primary: '#E5D0D6', + secondary: '#D1A8C4', + disabled: '#B7A0A6', + link: '#A56BB6', + maxContrast: '#FFFFFF', + }, + primary: { + main: '#8C5A69', + }, + secondary: { + main: '#6A3C4B', + text: '#D1A8C4', + border: '#8C5A69', + }, + background: { + canvas: '#2E1F2D', + primary: '#3C2136', + secondary: '#4A2D47', + }, + action: { + hover: '#6A3C4B', + selected: '#8C5A69', + selectedBorder: '#FFB300', + focus: '#A56BB6', + hoverOpacity: 0.1, + disabledText: '#B7A0A6', + disabledBackground: '#4A2D47', + disabledOpacity: 0.38, + }, + gradients: { + brandHorizontal: 'linear-gradient(270deg, #6A3C4B 0%, #A56BB6 100%)', + brandVertical: 'linear-gradient(0deg, #6A3C4B 0%, #A56BB6 100%)', + }, + contrastThreshold: 4, + hoverFactor: 0.07, + tonalOffset: 0.15, + }, + shape: { + borderRadius: 6, + }, +}; + +export default aubergineTheme; diff --git a/packages/grafana-data/src/themes/themeDefinitions/desertbloom.ts b/packages/grafana-data/src/themes/themeDefinitions/desertbloom.ts new file mode 100644 index 00000000000..20e6938ab36 --- /dev/null +++ b/packages/grafana-data/src/themes/themeDefinitions/desertbloom.ts @@ -0,0 +1,79 @@ +import { NewThemeOptions } from '../createTheme'; + +const desertBloomTheme: NewThemeOptions = { + name: 'Desert bloom', + colors: { + mode: 'light', + border: { + weak: 'rgba(0, 0, 0, 0.12)', + medium: 'rgba(0, 0, 0, 0.20)', + strong: 'rgba(0, 0, 0, 0.30)', + }, + text: { + primary: '#333333', + secondary: '#555555', + disabled: 'rgba(0, 0, 0, 0.5)', + link: '#1A82E2', + maxContrast: '#000000', + }, + primary: { + main: '#FF6F61', + text: '#FE6F61', + border: '#E55B4D', + name: 'primary', + shade: '#E55B4D', + transparent: '#FF6F6126', + contrastText: '#FFFFFF', + borderTransparent: '#FF6F6140', + }, + secondary: { + main: '#FFFFFF', + text: '#695f53', + border: '#d9cec0', + name: 'secondary', + shade: '#d9cec0', + transparent: '#FFFFFF26', + contrastText: '#4c4339', + borderTransparent: '#FFFFFF40', + }, + info: { + main: '#1A82E2', + }, + error: { + main: '#FF6B6B', + }, + success: { + main: '#4CAF50', + }, + warning: { + main: '#FFC107', + }, + background: { + canvas: '#FFF8F0', + primary: '#FFFFFF', + secondary: '#f9f3e8', + }, + action: { + hover: 'rgba(168, 156, 134, 0.12)', + selected: 'rgba(168, 156, 134, 0.36)', + selectedBorder: '#FF6F61', + focus: 'rgba(168, 156, 134, 0.50)', + hoverOpacity: 0.08, + disabledText: 'rgba(168, 156, 134, 0.5)', + disabledBackground: 'rgba(168, 156, 134, 0.06)', + disabledOpacity: 0.38, + }, + gradients: { + brandHorizontal: 'linear-gradient(270deg, #FF6F61 0%, #ece0d1 100%)', + brandVertical: 'linear-gradient(0.01deg, #FF6F61 0.01%, #ece0d1 99.99%)', + }, + contrastThreshold: 3, + hoverFactor: 0.03, + tonalOffset: 0.15, + }, + shape: { + borderRadius: 6, + }, +}; + +export default desertBloomTheme; diff --git a/packages/grafana-data/src/themes/themeDefinitions/gildedgrove.ts b/packages/grafana-data/src/themes/themeDefinitions/gildedgrove.ts new file mode 100644 index 00000000000..546003ba07d --- /dev/null +++ b/packages/grafana-data/src/themes/themeDefinitions/gildedgrove.ts @@ -0,0 +1,67 @@ +import { NewThemeOptions } from '../createTheme'; + +const gildedGroveTheme: NewThemeOptions = { + name: 'Gilded grove', + colors: { + mode: 'dark', + border: { + weak: 'rgba(200, 200, 180, 0.12)', + medium: 'rgba(200, 200, 180, 0.20)', + strong: 'rgba(200, 200, 180, 0.30)', + }, + text: { + primary: 'rgb(250, 250, 239)', + secondary: 'rgba(200, 200, 180, 0.85)', + disabled: 'rgba(200, 200, 180, 0.6)', + link: '#FEAC34', + maxContrast: '#FFFFFF', + }, + primary: { + main: '#FEAC34', + text: '#FFD783', + border: '#FFD783', + name: 'primary', + shade: 'rgb(255, 173, 80)', + transparent: '#FEAC3426', + contrastText: '#111614', + borderTransparent: '#FFD78340', + }, + secondary: { + main: 'rgba(200, 200, 180, 0.10)', + shade: 'rgba(200, 200, 180, 0.14)', + transparent: 'rgba(200, 200, 180, 0.08)', + text: 'rgb(200, 200, 180)', + contrastText: 'rgb(200, 200, 180)', + border: 'rgba(200, 200, 180, 0.08)', + name: 'secondary', + borderTransparent: 'rgba(200, 200, 180, 0.25)', + }, + background: { + canvas: '#111614', + primary: '#1d2220', + secondary: '#27312E', + }, + action: { + hover: 'rgba(200, 200, 180, 0.16)', + selected: 'rgba(200, 200, 180, 0.12)', + selectedBorder: '#FEAC34', + focus: 'rgba(200, 200, 180, 0.16)', + hoverOpacity: 0.08, + disabledText: 'rgba(200, 200, 180, 0.6)', + disabledBackground: 'rgba(200, 200, 180, 0.04)', + disabledOpacity: 0.38, + }, + gradients: { + brandHorizontal: 'linear-gradient(270deg, #FEAC34 0%, #FFD783 100%)', + brandVertical: 'linear-gradient(0.01deg, #FEAC34 0.01%, #FFD783 99.99%)', + }, + contrastThreshold: 3, + hoverFactor: 0.03, + tonalOffset: 0.15, + }, + shape: { + borderRadius: 5, + }, +}; + +export default gildedGroveTheme; diff --git a/packages/grafana-data/src/themes/themeDefinitions/index.ts b/packages/grafana-data/src/themes/themeDefinitions/index.ts index a88efba4add..f0277312fe4 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/index.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/index.ts @@ -1 +1,11 @@ +export { default as aubergine } from './aubergine'; export { default as debug } from './debug'; +export { default as desertbloom } from './desertbloom'; +export { default as gildedgrove } from './gildedgrove'; +export { default as mars } from './mars'; +export { default as matrix } from './matrix'; +export { default as sapphiredusk } from './sapphiredusk'; +export { default as synthwave } from './synthwave'; +export { default as tron } from './tron'; +export { default as victorian } from './victorian'; +export { default as zen } from './zen'; diff --git a/packages/grafana-data/src/themes/themeDefinitions/mars.ts b/packages/grafana-data/src/themes/themeDefinitions/mars.ts new file mode 100644 index 00000000000..c695e65d992 --- /dev/null +++ b/packages/grafana-data/src/themes/themeDefinitions/mars.ts @@ -0,0 +1,55 @@ +import { NewThemeOptions } from '../createTheme'; + +const marsTheme: NewThemeOptions = { + name: 'Mars', + colors: { + mode: 'dark', + border: { + weak: 'rgba(210, 90, 60, 0.2)', + medium: 'rgba(210, 90, 60, 0.35)', + strong: 'rgba(210, 90, 60, 0.5)', + }, + text: { + primary: '#DDDDDD', + secondary: '#BBBBBB', + disabled: 'rgba(221, 221, 221, 0.5)', + link: '#FF6F61', + maxContrast: '#FFFFFF', + }, + primary: { + main: '#FF6F61', + }, + secondary: { + main: '#6a2f2f', + text: '#BBBBBB', + border: 'rgba(210, 90, 60, 0.2)', + }, + background: { + canvas: '#3C1E1E', + primary: '#522626', + secondary: '#6A2F2F', + }, + action: { + hover: 'rgba(210, 90, 60, 0.16)', + selected: 'rgba(210, 90, 60, 0.12)', + selectedBorder: '#FF6F61', + focus: 'rgba(210, 90, 60, 0.16)', + hoverOpacity: 0.08, + disabledText: 'rgba(221, 221, 221, 0.5)', + disabledBackground: 'rgba(210, 90, 60, 0.08)', + disabledOpacity: 0.38, + }, + gradients: { + brandHorizontal: 'linear-gradient(270deg, #FF6F61 0%, #D25A3C 100%)', + brandVertical: 'linear-gradient(0.01deg, #FF6F61 0.01%, #D25A3C 99.99%)', + }, + contrastThreshold: 3, + hoverFactor: 0.05, + tonalOffset: 0.2, + }, + shape: { + borderRadius: 4, + }, +}; + +export default marsTheme; diff --git a/packages/grafana-data/src/themes/themeDefinitions/matrix.ts b/packages/grafana-data/src/themes/themeDefinitions/matrix.ts new file mode 100644 index 00000000000..3d4350e4f0a --- /dev/null +++ b/packages/grafana-data/src/themes/themeDefinitions/matrix.ts @@ -0,0 +1,43 @@ +import { NewThemeOptions } from '../createTheme'; + +const matrixTheme: NewThemeOptions = { + name: 'Matrix', + colors: { + mode: 'dark', + background: { + canvas: '#000000', + primary: '#020202', + secondary: '#080808', + }, + text: { + primary: '#00c017', + secondary: '#008910', + disabled: '#006a0c', + link: '#00ff41', + maxContrast: '#00ff41', + }, + border: { + weak: '#008f1144', + medium: '#008f1188', + strong: '#008910', + }, + primary: { + main: '#008910', + }, + secondary: { + text: '#008910', + }, + gradients: { + brandVertical: 'linear-gradient(0deg, #008910 0%, #00ff41 100%)', + brandHorizontal: 'linear-gradient(90deg, #008910 0%, #00ff41 100%)', + }, + }, + shape: { + borderRadius: 0, + }, + typography: { + fontFamily: 'monospace', + }, +}; + +export default matrixTheme; diff --git a/packages/grafana-data/src/themes/themeDefinitions/sapphiredusk.ts b/packages/grafana-data/src/themes/themeDefinitions/sapphiredusk.ts new file mode 100644 index 00000000000..353ff32b712 --- /dev/null +++ b/packages/grafana-data/src/themes/themeDefinitions/sapphiredusk.ts @@ -0,0 +1,81 @@ +import { NewThemeOptions } from '../createTheme'; + +const sapphireDuskTheme: NewThemeOptions = { + name: 'Sapphire dusk', + colors: { + mode: 'dark', + border: { + weak: '#232e47', + medium: '#2c3853', + strong: '#404d6b', + }, + text: { + primary: '#FFFFFF', + secondary: '#bcccdd', + disabled: '#838da5', + link: '#93EBF0', + maxContrast: '#FFFFFF', + }, + primary: { + main: '#93EBF0', + text: '#a8e9ed', + border: '#93ebf0', + name: 'primary', + shade: '#c0f5d9', + transparent: '#93EBF029', + contrastText: '#111614', + borderTransparent: '#93ebf040', + }, + secondary: { + main: '#2c364f', + shade: '#36415e', + transparent: 'rgba(200, 200, 180, 0.08)', + text: '#d1dfff', + contrastText: '#acfeff', + border: 'rgba(200, 200, 180, 0.08)', + name: 'secondary', + borderTransparent: 'rgba(200, 200, 180, 0.25)', + }, + info: { + main: '#4d4593', + text: '#a8e9ed', + border: '#5d54a7', + }, + error: { + main: '#c63370', + }, + success: { + main: '#1A7F4B', + }, + warning: { + main: '#D448EA', + }, + background: { + canvas: '#1e273d', + primary: '#12192e', + secondary: '#212c47', + }, + action: { + hover: '#364057', + selected: '#364260', + selectedBorder: '#D448EA', + focus: '#364057', + hoverOpacity: 0.08, + disabledText: 'rgba(54, 64, 87, 0.6)', + disabledBackground: 'rgba(54, 64, 87, 0.04)', + disabledOpacity: 0.38, + }, + gradients: { + brandHorizontal: 'linear-gradient(270deg, #D346EF 0%, #2C83FE 100%)', + brandVertical: 'linear-gradient(0deg, #D346EF 0%, #2C83FE 100%)', + }, + contrastThreshold: 3, + hoverFactor: 0.03, + tonalOffset: 0.15, + }, + shape: { + borderRadius: 5, + }, +}; + +export default sapphireDuskTheme; diff --git a/packages/grafana-data/src/themes/themeDefinitions/synthwave.ts b/packages/grafana-data/src/themes/themeDefinitions/synthwave.ts new file mode 100644 index 00000000000..c54eaf71731 --- /dev/null +++ b/packages/grafana-data/src/themes/themeDefinitions/synthwave.ts @@ -0,0 +1,55 @@ +import { NewThemeOptions } from '../createTheme'; + +const synthwaveTheme: NewThemeOptions = { + name: 'Synthwave', + colors: { + mode: 'dark', + border: { + weak: 'rgba(255, 20, 147, 0.12)', + medium: 'rgba(255, 20, 147, 0.20)', + strong: 'rgba(255, 20, 147, 0.30)', + }, + text: { + primary: '#E0E0E0', + secondary: 'rgba(224, 224, 224, 0.75)', + disabled: 'rgba(224, 224, 224, 0.5)', + link: '#FF69B4', + maxContrast: '#FFFFFF', + }, + primary: { + main: '#FF1493', + }, + secondary: { + main: '#37183a', + text: 'rgba(224, 224, 224, 0.75)', + border: 'rgba(255, 20, 147, 0.10)', + }, + background: { + canvas: '#1A1A2E', + primary: '#16213E', + secondary: '#0F3460', + }, + action: { + hover: 'rgba(255, 20, 147, 0.16)', + selected: 'rgba(255, 20, 147, 0.12)', + selectedBorder: '#FF1493', + focus: 'rgba(255, 20, 147, 0.16)', + hoverOpacity: 0.08, + disabledText: 'rgba(224, 224, 224, 0.5)', + disabledBackground: 'rgba(255, 20, 147, 0.08)', + disabledOpacity: 0.38, + }, + gradients: { + brandHorizontal: 'linear-gradient(270deg, #FF1493 0%, #1E90FF 100%)', + brandVertical: 'linear-gradient(0.01deg, #FF1493 0.01%, #1E90FF 99.99%)', + }, + contrastThreshold: 3, + hoverFactor: 0.03, + tonalOffset: 0.15, + }, + shape: { + borderRadius: 6, + }, +}; + +export default synthwaveTheme; diff --git a/packages/grafana-data/src/themes/themeDefinitions/tron.ts b/packages/grafana-data/src/themes/themeDefinitions/tron.ts new file mode 100644 index 00000000000..e3e761e012b --- /dev/null +++ b/packages/grafana-data/src/themes/themeDefinitions/tron.ts @@ -0,0 +1,55 @@ +import { NewThemeOptions } from '../createTheme'; + +const tronTheme: NewThemeOptions = { + name: 'Tron', + colors: { + mode: 'dark', + border: { + weak: 'rgba(0, 255, 255, 0.12)', + medium: 'rgba(0, 255, 255, 0.20)', + strong: 'rgba(0, 255, 255, 0.30)', + }, + text: { + primary: '#E0E0E0', + secondary: 'rgba(224, 224, 224, 0.75)', + disabled: 'rgba(224, 224, 224, 0.5)', + link: '#00FFFF', + maxContrast: '#FFFFFF', + }, + primary: { + main: '#00FFFF', + }, + secondary: { + main: '#0b2e36', + text: 'rgba(224, 224, 224, 0.75)', + border: 'rgba(0, 255, 255, 0.10)', + }, + background: { + canvas: '#0A0F18', + primary: '#0F1B2A', + secondary: '#152234', + }, + action: { + hover: 'rgba(0, 255, 255, 0.16)', + selected: 'rgba(0, 255, 255, 0.12)', + selectedBorder: '#00FFFF', + focus: 'rgba(0, 255, 255, 0.16)', + hoverOpacity: 0.08, + disabledText: 'rgba(224, 224, 224, 0.5)', + disabledBackground: 'rgba(0, 255, 255, 0.08)', + disabledOpacity: 0.38, + }, + gradients: { + brandHorizontal: 'linear-gradient(270deg, #00FFFF 0%, #29ABE2 100%)', + brandVertical: 'linear-gradient(0.01deg, #00FFFF 0.01%, #29ABE2 99.99%)', + }, + contrastThreshold: 3, + hoverFactor: 0.05, + tonalOffset: 0.2, + }, + shape: { + borderRadius: 6, + }, +}; + +export default tronTheme; diff --git a/packages/grafana-data/src/themes/themeDefinitions/victorian.ts b/packages/grafana-data/src/themes/themeDefinitions/victorian.ts new file mode 100644 index 00000000000..a90879ea1fb --- /dev/null +++ b/packages/grafana-data/src/themes/themeDefinitions/victorian.ts @@ -0,0 +1,59 @@ +import { NewThemeOptions } from '../createTheme'; + +const victorianTheme: NewThemeOptions = { + name: 'Victorian', + colors: { + mode: 'dark', + border: { + weak: '#3A2C22', + medium: '#3A2C22', + strong: '#4B3D32', + }, + text: { + primary: '#D9D0A2', + secondary: '#C4B89B', + disabled: '#A89F91', + link: '#C28A4D', + maxContrast: '#FFFFFF', + }, + primary: { + main: '#C28A4D', + }, + secondary: { + main: '#3A2C22', + text: '#C4B89B', + border: '#4B3D32', + }, + background: { + canvas: '#1F1510', + primary: '#2C1A13', + secondary: '#402A21', + }, + action: { + hover: '#3A2C22', + selected: '#4B3D32', + selectedBorder: '#C28A4D', + focus: '#C28A4D', + hoverOpacity: 0.1, + disabledText: '#A89F91', + disabledBackground: '#402A21', + disabledOpacity: 0.38, + }, + gradients: { + brandHorizontal: 'linear-gradient(270deg, #D9D0a1 0%, #C28A4D 100%)', + brandVertical: 'linear-gradient(0.01deg, #D9D0a1 0.01%, #C28A4D 99.99%)', + }, + contrastThreshold: 4, + hoverFactor: 0.07, + tonalOffset: 0.15, + }, + shape: { + borderRadius: 6, + }, + typography: { + fontFamily: '"Georgia", "Times New Roman", serif', + fontFamilyMonospace: "'Courier New', monospace", + }, +}; + +export default victorianTheme; diff --git a/packages/grafana-data/src/themes/themeDefinitions/zen.ts b/packages/grafana-data/src/themes/themeDefinitions/zen.ts new file mode 100644 index 00000000000..0867de1e63e --- /dev/null +++ b/packages/grafana-data/src/themes/themeDefinitions/zen.ts @@ -0,0 +1,55 @@ +import { NewThemeOptions } from '../createTheme'; + +const zenTheme: NewThemeOptions = { + name: 'Zen', + colors: { + mode: 'light', + text: { + primary: '#333333', + secondary: '#666666', + disabled: '#B8B8B8', + link: '#4F9F6E', + maxContrast: '#000000', + }, + border: { + weak: '#B1B7B3', + medium: '#A2A8A2', + strong: '#7C7F7A', + }, + primary: { + main: '#6D8E6D', + }, + secondary: { + main: '#E0E0E0', + text: '#666666', + border: '#A2A8A2', + }, + background: { + canvas: '#F4F4F4', + primary: '#E9E9E9', + secondary: '#D8D8D8', + }, + action: { + hover: '#D1D1D1', + selected: '#B8B8B8', + selectedBorder: '#88B88B', + hoverOpacity: 0.1, + focus: '#D1D1D1', + disabledBackground: '#E0E0E0', + disabledText: '#B8B8B8', + disabledOpacity: 0.5, + }, + gradients: { + brandHorizontal: 'linear-gradient(270deg, #88B88B 0%, #6D8E6D 100%)', + brandVertical: 'linear-gradient(0.01deg, #88B88B 0.01%, #6D8E6D 99.99%)', + }, + contrastThreshold: 3, + hoverFactor: 0.03, + tonalOffset: 0.2, + }, + shape: { + borderRadius: 8, + }, +}; + +export default zenTheme; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 452659bcf74..3e0490dd97b 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -256,4 +256,5 @@ export interface FeatureToggles { alertingAlertmanagerExtraDedupStage?: boolean; alertingAlertmanagerExtraDedupStageStopPipeline?: boolean; newLogsPanel?: boolean; + grafanaconThemes?: boolean; } diff --git a/packages/grafana-ui/.storybook/preview.ts b/packages/grafana-ui/.storybook/preview.ts index a8dc3e9f062..61206e5ab7f 100644 --- a/packages/grafana-ui/.storybook/preview.ts +++ b/packages/grafana-ui/.storybook/preview.ts @@ -31,7 +31,15 @@ const handleThemeChange = (theme: GrafanaTheme2) => { } }; -const showExtraThemes = process.env.NODE_ENV === 'development'; +const allowedExtraThemes: string[] = []; + +if (process.env.NODE_ENV === 'development') { + allowedExtraThemes.push('debug'); + allowedExtraThemes.push('desertbloom'); + allowedExtraThemes.push('gildedgrove'); + allowedExtraThemes.push('sapphiredusk'); + allowedExtraThemes.push('tron'); +} const preview: Preview = { decorators: [withTheme(handleThemeChange), withTimeZone()], @@ -71,7 +79,7 @@ const preview: Preview = { defaultValue: 'system', toolbar: { icon: 'paintbrush', - items: getBuiltInThemes(showExtraThemes).map((theme) => ({ + items: getBuiltInThemes(allowedExtraThemes).map((theme) => ({ value: theme.id, title: theme.name, })), diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f0def056b77..bf5496b9c3c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1784,6 +1784,15 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, }, + { + Name: "grafanaconThemes", + Description: "Enables the temporary themes for GrafanaCon", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendPlatformSquad, + HideFromAdminPage: true, + HideFromDocs: true, + RequiresRestart: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index ad6203ebdc7..66125da1954 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -237,3 +237,4 @@ fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false alertingAlertmanagerExtraDedupStage,experimental,@grafana/alerting-squad,false,true,false alertingAlertmanagerExtraDedupStageStopPipeline,experimental,@grafana/alerting-squad,false,true,false newLogsPanel,experimental,@grafana/observability-logs,false,false,true +grafanaconThemes,experimental,@grafana/grafana-frontend-platform,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 8c36acbcc51..e910ed15857 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -958,4 +958,8 @@ const ( // FlagNewLogsPanel // Enables the new logs panel in Explore FlagNewLogsPanel = "newLogsPanel" + + // FlagGrafanaconThemes + // Enables the temporary themes for GrafanaCon + FlagGrafanaconThemes = "grafanaconThemes" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index e1673e7c8ab..bc2a152baaf 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1865,6 +1865,21 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "grafanaconThemes", + "resourceVersion": "1738661140740", + "creationTimestamp": "2025-02-04T09:25:40Z" + }, + "spec": { + "description": "Enables the temporary themes for GrafanaCon", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform", + "requiresRestart": true, + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "groupAttributeSync", diff --git a/pkg/services/preference/themes.go b/pkg/services/preference/themes.go index f1332d33f0b..4e88235d5d2 100644 --- a/pkg/services/preference/themes.go +++ b/pkg/services/preference/themes.go @@ -11,6 +11,16 @@ var themes = []ThemeDTO{ {ID: "dark", Type: "dark"}, {ID: "system", Type: "dark"}, {ID: "debug", Type: "dark", IsExtra: true}, + {ID: "aubergine", Type: "dark", IsExtra: true}, + {ID: "desertbloom", Type: "light", IsExtra: true}, + {ID: "gildedgrove", Type: "dark", IsExtra: true}, + {ID: "mars", Type: "dark", IsExtra: true}, + {ID: "matrix", Type: "dark", IsExtra: true}, + {ID: "sapphiredusk", Type: "dark", IsExtra: true}, + {ID: "synthwave", Type: "dark", IsExtra: true}, + {ID: "tron", Type: "dark", IsExtra: true}, + {ID: "victorian", Type: "dark", IsExtra: true}, + {ID: "zen", Type: "light", IsExtra: true}, } func GetThemeByID(id string) *ThemeDTO { diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index a24bd4b027b..16d91551060 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -17,6 +17,7 @@ import { FeatureBadge, Combobox, ComboboxOption, + TextLink, } from '@grafana/ui'; import { DashboardPicker } from 'app/core/components/Select/DashboardPicker'; import { t, Trans } from 'app/core/internationalization'; @@ -80,7 +81,20 @@ export class SharedPreferences extends PureComponent { navbar: { bookmarkUrls: [] }, }; - this.themeOptions = getBuiltInThemes(config.featureToggles.extraThemes).map((theme) => ({ + const allowedExtraThemes = []; + + if (config.featureToggles.extraThemes) { + allowedExtraThemes.push('debug'); + } + + if (config.featureToggles.grafanaconThemes) { + allowedExtraThemes.push('desertbloom'); + allowedExtraThemes.push('gildedgrove'); + allowedExtraThemes.push('sapphiredusk'); + allowedExtraThemes.push('tron'); + } + + this.themeOptions = getBuiltInThemes(allowedExtraThemes).map((theme) => ({ value: theme.id, label: getTranslatedThemeName(theme), })); @@ -168,6 +182,20 @@ export class SharedPreferences extends PureComponent { loading={isLoading} disabled={isLoading} label={t('shared-preferences.fields.theme-label', 'Interface theme')} + description={ + config.featureToggles.grafanaconThemes ? ( + + Enjoying the limited edition themes? Tell us what you'd like to see{' '} + + here. + + + ) : undefined + } > here.", "theme-label": "Interface theme", "week-start-label": "Week start" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index e3194c0093e..ad00f61eef6 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -3398,6 +3398,7 @@ "home-dashboard-placeholder": "Đęƒäūľŧ đäşĥþőäřđ", "locale-label": "Ŀäʼnģūäģę", "locale-placeholder": "Cĥőőşę ľäʼnģūäģę", + "theme-description": "Ēʼnĵőyįʼnģ ŧĥę ľįmįŧęđ ęđįŧįőʼn ŧĥęmęş? Ŧęľľ ūş ŵĥäŧ yőū'đ ľįĸę ŧő şęę <2>ĥęřę.", "theme-label": "Ĩʼnŧęřƒäčę ŧĥęmę", "week-start-label": "Ŵęęĸ şŧäřŧ" }, From fde475e3d9a28b19fc6a1decd7fa911123c00a2e Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Thu, 6 Feb 2025 12:35:59 +0100 Subject: [PATCH 380/894] CloudMigrations: save snapshot of alert rule groups (#100109) --- pkg/services/cloudmigration/api/dtos.go | 1 + .../cloudmigrationimpl/cloudmigration_test.go | 5 +- .../cloudmigrationimpl/snapshot_mgmt.go | 17 +++ .../snapshot_mgmt_alerts.go | 58 ++++++++++ .../snapshot_mgmt_alerts_test.go | 105 +++++++++++++++++- pkg/services/cloudmigration/model.go | 1 + public/api-enterprise-spec.json | 1 + public/api-merged.json | 1 + .../migrate-to-cloud/api/endpoints.gen.ts | 1 + .../migrate-to-cloud/onprem/NameCell.tsx | 2 + .../migrate-to-cloud/onprem/TypeCell.tsx | 2 + .../onprem/useNotifyOnSuccess.tsx | 2 + public/locales/en-US/grafana.json | 2 + public/locales/pseudo-LOCALE/grafana.json | 2 + public/openapi3.json | 1 + 15 files changed, 194 insertions(+), 7 deletions(-) diff --git a/pkg/services/cloudmigration/api/dtos.go b/pkg/services/cloudmigration/api/dtos.go index 6e1154489bf..1efa21fc77c 100644 --- a/pkg/services/cloudmigration/api/dtos.go +++ b/pkg/services/cloudmigration/api/dtos.go @@ -127,6 +127,7 @@ const ( FolderDataType MigrateDataType = "FOLDER" LibraryElementDataType MigrateDataType = "LIBRARY_ELEMENT" AlertRuleType MigrateDataType = "ALERT_RULE" + AlertRuleGroupType MigrateDataType = "ALERT_RULE_GROUP" ContactPointType MigrateDataType = "CONTACT_POINT" NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY" NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE" diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 257d2e23a07..348bf8b1e98 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -821,8 +821,11 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf secretsService := secretsfakes.NewFakeSecretsService() rr := routing.NewRouteRegister() tracer := tracing.InitializeTracerForTest() + + fakeFolder := &folder.Folder{UID: "folderUID", Title: "Folder"} mockFolder := &foldertest.FakeService{ - ExpectedFolder: &folder.Folder{UID: "folderUID", Title: "Folder"}, + ExpectedFolders: []*folder.Folder{fakeFolder}, + ExpectedFolder: fakeFolder, } cfg := setting.NewCfg() diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index 870c4949533..034c601f510 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -43,6 +43,7 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{ cloudmigration.NotificationTemplateType, cloudmigration.ContactPointType, cloudmigration.NotificationPolicyType, + cloudmigration.AlertRuleGroupType, cloudmigration.AlertRuleType, cloudmigration.PluginDataType, } @@ -106,6 +107,13 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S return nil, err } + // Alerts: Alert Rule Groups + alertRuleGroups, err := s.getAlertRuleGroups(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert rule groups", "err", err) + return nil, err + } + // Alerts: Alert Rules alertRules, err := s.getAlertRules(ctx, signedInUser) if err != nil { @@ -209,6 +217,15 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + for _, alertRuleGroup := range alertRuleGroups { + migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ + Type: cloudmigration.AlertRuleGroupType, + RefID: alertRuleGroup.Title, // no UID available + Name: alertRuleGroup.Title, + Data: alertRuleGroup, + }) + } + for _, alertRule := range alertRules { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.AlertRuleType, diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go index a848508b7eb..c75d8a35df0 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go @@ -180,3 +180,61 @@ func (s *Service) getAlertRules(ctx context.Context, signedInUser *user.SignedIn return provisionedAlertRules, nil } + +type alertRuleGroup struct { + Title string `json:"title"` + FolderUID string `json:"folderUid"` + Interval int64 `json:"interval"` + Rules []alertRule `json:"rules"` +} + +func (s *Service) getAlertRuleGroups(ctx context.Context, signedInUser *user.SignedInUser) ([]alertRuleGroup, error) { + alertRuleGroupsWithFolder, err := s.ngAlert.Api.AlertRules.GetAlertGroupsWithFolderFullpath(ctx, signedInUser, nil) + if err != nil { + return nil, fmt.Errorf("fetching alert rule groups with folders: %w", err) + } + + settingAlertRulesPaused := s.cfg.CloudMigration.AlertRulesState == setting.GMSAlertRulesPaused + + alertRuleGroups := make([]alertRuleGroup, 0, len(alertRuleGroupsWithFolder)) + + for _, ruleGroup := range alertRuleGroupsWithFolder { + provisionedAlertRules := make([]alertRule, 0, len(ruleGroup.Rules)) + + for _, rule := range ruleGroup.Rules { + isPaused := rule.IsPaused + if settingAlertRulesPaused { + isPaused = true + } + + provisionedAlertRules = append(provisionedAlertRules, alertRule{ + ID: rule.ID, + UID: rule.UID, + OrgID: rule.OrgID, + FolderUID: rule.NamespaceUID, + RuleGroup: rule.RuleGroup, + Title: rule.Title, + For: model.Duration(rule.For), + Condition: rule.Condition, + Data: ngalertapi.ApiAlertQueriesFromAlertQueries(rule.Data), + Updated: rule.Updated, + NoDataState: rule.NoDataState.String(), + ExecErrState: rule.ExecErrState.String(), + Annotations: rule.Annotations, + Labels: rule.Labels, + IsPaused: isPaused, + NotificationSettings: ngalertapi.AlertRuleNotificationSettingsFromNotificationSettings(rule.NotificationSettings), + Record: ngalertapi.ApiRecordFromModelRecord(rule.Record), + }) + } + + alertRuleGroups = append(alertRuleGroups, alertRuleGroup{ + Title: ruleGroup.Title, + FolderUID: ruleGroup.FolderUID, + Interval: ruleGroup.Interval, + Rules: provisionedAlertRules, + }) + } + + return alertRuleGroups, nil +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go index f8e99aed021..91d7ecc5cd7 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -121,7 +121,7 @@ func TestGetAlertRules(t *testing.T) { user := &user.SignedInUser{OrgID: 1} - alertRule := createAlertRule(t, ctx, s, user, false) + alertRule := createAlertRule(t, ctx, s, user, false, "") alertRules, err := s.getAlertRules(ctx, user) require.NoError(t, err) @@ -138,10 +138,10 @@ func TestGetAlertRules(t *testing.T) { user := &user.SignedInUser{OrgID: 1} - alertRulePaused := createAlertRule(t, ctx, s, user, true) + alertRulePaused := createAlertRule(t, ctx, s, user, true, "") require.True(t, alertRulePaused.IsPaused) - alertRuleUnpaused := createAlertRule(t, ctx, s, user, false) + alertRuleUnpaused := createAlertRule(t, ctx, s, user, false, "") require.False(t, alertRuleUnpaused.IsPaused) alertRules, err := s.getAlertRules(ctx, user) @@ -152,6 +152,83 @@ func TestGetAlertRules(t *testing.T) { }) } +func TestGetAlertRuleGroups(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("it returns the alert rule groups", func(t *testing.T) { + s := setUpServiceTest(t, false).(*Service) + + user := &user.SignedInUser{OrgID: 1} + + ruleGroupTitle := "ruleGroupTitle" + + alertRule1 := createAlertRule(t, ctx, s, user, true, ruleGroupTitle) + alertRule2 := createAlertRule(t, ctx, s, user, false, ruleGroupTitle) + alertRule3 := createAlertRule(t, ctx, s, user, false, "anotherRuleGroup") + + createAlertRuleGroup(t, ctx, s, user, ruleGroupTitle, []models.AlertRule{alertRule1, alertRule2}) + + ruleGroups, err := s.getAlertRuleGroups(ctx, user) + require.NoError(t, err) + require.Len(t, ruleGroups, 2) + + for _, ruleGroup := range ruleGroups { + alertRuleUIDs := make([]string, 0) + for _, alertRule := range ruleGroup.Rules { + alertRuleUIDs = append(alertRuleUIDs, alertRule.UID) + } + + if ruleGroup.Title == ruleGroupTitle { + require.Len(t, ruleGroup.Rules, 2) + require.ElementsMatch(t, []string{alertRule1.UID, alertRule2.UID}, alertRuleUIDs) + } else { + require.Len(t, ruleGroup.Rules, 1) + require.ElementsMatch(t, []string{alertRule3.UID}, alertRuleUIDs) + } + } + }) + + t.Run("with the alert rules state set to paused, it returns the alert rule groups with alert rules paused", func(t *testing.T) { + alertRulesState := func(c *setting.Cfg) { + c.CloudMigration.AlertRulesState = setting.GMSAlertRulesPaused + } + + s := setUpServiceTest(t, false, alertRulesState).(*Service) + + user := &user.SignedInUser{OrgID: 1} + + ruleGroupTitle := "ruleGroupTitle" + + alertRule1 := createAlertRule(t, ctx, s, user, true, ruleGroupTitle) + alertRule2 := createAlertRule(t, ctx, s, user, false, ruleGroupTitle) + alertRule3 := createAlertRule(t, ctx, s, user, false, "anotherRuleGroup") + + createAlertRuleGroup(t, ctx, s, user, ruleGroupTitle, []models.AlertRule{alertRule1, alertRule2}) + + ruleGroups, err := s.getAlertRuleGroups(ctx, user) + require.NoError(t, err) + require.Len(t, ruleGroups, 2) + + for _, ruleGroup := range ruleGroups { + alertRuleUIDs := make([]string, 0) + for _, alertRule := range ruleGroup.Rules { + alertRuleUIDs = append(alertRuleUIDs, alertRule.UID) + + require.True(t, alertRule.IsPaused) + } + + if ruleGroup.Title == ruleGroupTitle { + require.Len(t, ruleGroup.Rules, 2) + require.ElementsMatch(t, []string{alertRule1.UID, alertRule2.UID}, alertRuleUIDs) + } else { + require.Len(t, ruleGroup.Rules, 1) + require.ElementsMatch(t, []string{alertRule3.UID}, alertRuleUIDs) + } + } + }) +} + func createMuteTiming(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser) definitions.MuteTimeInterval { t.Helper() @@ -267,12 +344,12 @@ func updateNotificationPolicyTree(t *testing.T, ctx context.Context, service *Se require.NoError(t, err) } -func createAlertRule(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, isPaused bool) models.AlertRule { +func createAlertRule(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, isPaused bool, ruleGroup string) models.AlertRule { t.Helper() rule := models.AlertRule{ OrgID: user.GetOrgID(), - Title: fmt.Sprintf("Alert Rule SLO (Paused: %v)", isPaused), + Title: fmt.Sprintf("Alert Rule SLO (Paused: %v) - %v", isPaused, ruleGroup), NamespaceUID: "folderUID", Condition: "A", Data: []models.AlertQuery{ @@ -286,7 +363,7 @@ func createAlertRule(t *testing.T, ctx context.Context, service *Service, user * }, }, IsPaused: isPaused, - RuleGroup: "ruleGroup", + RuleGroup: ruleGroup, For: time.Minute, IntervalSeconds: 60, NoDataState: models.OK, @@ -298,3 +375,19 @@ func createAlertRule(t *testing.T, ctx context.Context, service *Service, user * return createdRule } + +func createAlertRuleGroup(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, title string, rules []models.AlertRule) models.AlertRuleGroup { + t.Helper() + + group := models.AlertRuleGroup{ + Title: title, + FolderUID: "folderUID", + Interval: 300, + Rules: rules, + } + + err := service.ngAlert.Api.AlertRules.ReplaceRuleGroup(ctx, user, group, "") + require.NoError(t, err) + + return group +} diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index 7d0b4b58f49..a46a3b46910 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -88,6 +88,7 @@ const ( FolderDataType MigrateDataType = "FOLDER" LibraryElementDataType MigrateDataType = "LIBRARY_ELEMENT" AlertRuleType MigrateDataType = "ALERT_RULE" + AlertRuleGroupType MigrateDataType = "ALERT_RULE_GROUP" ContactPointType MigrateDataType = "CONTACT_POINT" NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY" NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE" diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 7667a82756c..e2e8df6926b 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -5670,6 +5670,7 @@ "FOLDER", "LIBRARY_ELEMENT", "ALERT_RULE", + "ALERT_RULE_GROUP", "CONTACT_POINT", "NOTIFICATION_POLICY", "NOTIFICATION_TEMPLATE", diff --git a/public/api-merged.json b/public/api-merged.json index e063c8c959c..3e2239131d4 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -17182,6 +17182,7 @@ "FOLDER", "LIBRARY_ELEMENT", "ALERT_RULE", + "ALERT_RULE_GROUP", "CONTACT_POINT", "NOTIFICATION_POLICY", "NOTIFICATION_TEMPLATE", diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts index e24b6b424f0..98e962b61e0 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts @@ -194,6 +194,7 @@ export type MigrateDataResponseItemDto = { | 'FOLDER' | 'LIBRARY_ELEMENT' | 'ALERT_RULE' + | 'ALERT_RULE_GROUP' | 'CONTACT_POINT' | 'NOTIFICATION_POLICY' | 'NOTIFICATION_TEMPLATE' diff --git a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx index c6b8f78f04b..3080a26fdd9 100644 --- a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx @@ -231,6 +231,8 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) { return ; case 'ALERT_RULE': return ; + case 'ALERT_RULE_GROUP': + return ; case 'PLUGIN': if (pluginLogo) { return ; diff --git a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx index 399945cdaba..00d576db9c5 100644 --- a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx @@ -23,6 +23,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) { return t('migrate-to-cloud.resource-type.notification_policy', 'Notification Policy'); case 'ALERT_RULE': return t('migrate-to-cloud.resource-type.alert_rule', 'Alert Rule'); + case 'ALERT_RULE_GROUP': + return t('migrate-to-cloud.resource-type.alert_rule_group', 'Alert Rule Group'); case 'PLUGIN': return t('migrate-to-cloud.resource-type.plugin', 'Plugin'); default: diff --git a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx index e21fd83e815..e98a8221bb6 100644 --- a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx +++ b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx @@ -62,6 +62,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) { types.push(t('migrate-to-cloud.migrated-counts.notification_policies', 'notification policies')); } else if (type === 'ALERT_RULE') { types.push(t('migrate-to-cloud.migrated-counts.alert_rules', 'alert rules')); + } else if (type === 'ALERT_RULE_GROUP') { + types.push(t('migrate-to-cloud.migrated-counts.alert_rule_groups', 'alert rule groups')); } else if (type === 'PLUGIN') { types.push(t('migrate-to-cloud.migrated-counts.plugins', 'plugins')); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f1b5d7ba851..66492eac6b1 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2117,6 +2117,7 @@ "title": "Let us help you migrate to this stack" }, "migrated-counts": { + "alert_rule_groups": "alert rule groups", "alert_rules": "alert rules", "contact_points": "contact points", "dashboards": "dashboards", @@ -2221,6 +2222,7 @@ }, "resource-type": { "alert_rule": "Alert Rule", + "alert_rule_group": "Alert Rule Group", "contact_point": "Contact Point", "dashboard": "Dashboard", "datasource": "Data source", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index ad00f61eef6..193643a5111 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2117,6 +2117,7 @@ "title": "Ŀęŧ ūş ĥęľp yőū mįģřäŧę ŧő ŧĥįş şŧäčĸ" }, "migrated-counts": { + "alert_rule_groups": "äľęřŧ řūľę ģřőūpş", "alert_rules": "äľęřŧ řūľęş", "contact_points": "čőʼnŧäčŧ pőįʼnŧş", "dashboards": "đäşĥþőäřđş", @@ -2221,6 +2222,7 @@ }, "resource-type": { "alert_rule": "Åľęřŧ Ŗūľę", + "alert_rule_group": "Åľęřŧ Ŗūľę Ğřőūp", "contact_point": "Cőʼnŧäčŧ Pőįʼnŧ", "dashboard": "Đäşĥþőäřđ", "datasource": "Đäŧä şőūřčę", diff --git a/public/openapi3.json b/public/openapi3.json index f326616d62d..89612da9cf1 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -7250,6 +7250,7 @@ "FOLDER", "LIBRARY_ELEMENT", "ALERT_RULE", + "ALERT_RULE_GROUP", "CONTACT_POINT", "NOTIFICATION_POLICY", "NOTIFICATION_TEMPLATE", From 9fc82faea71a75b19de0376451e09fab70b74a76 Mon Sep 17 00:00:00 2001 From: Sriram <153843+yesoreyeram@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:50:48 +0000 Subject: [PATCH 381/894] [analytics] added plugin version to grafana_ds_test_datasource_clicked event (#100168) --- public/app/features/datasources/state/actions.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index f1b2fdd4ffb..6229f0d1a02 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -5,6 +5,7 @@ import { TestDataSourceResponse, DataSourceTestSucceeded, DataSourceTestFailed, + DataSourceApi, } from '@grafana/data'; import { config, @@ -125,6 +126,11 @@ export const initDataSourceSettings = ( }; }; +const getPluginVersion = (dsApi: DataSourceApi) => { + const isCorePlugin = (dsApi?.meta?.module || '').startsWith('core'); + return isCorePlugin ? config?.buildInfo?.version : dsApi?.meta?.info?.version; +}; + export const testDataSource = ( dataSourceName: string, editRoute = DATASOURCES_ROUTES.Edit, @@ -153,6 +159,7 @@ export const testDataSource = ( trackDataSourceTested({ grafana_version: config.buildInfo.version, plugin_id: dsApi.type, + plugin_version: getPluginVersion(dsApi), datasource_uid: dsApi.uid, success: true, path: editLink, @@ -165,6 +172,7 @@ export const testDataSource = ( trackDataSourceTested({ grafana_version: config.buildInfo.version, plugin_id: dsApi.type, + plugin_version: getPluginVersion(dsApi), datasource_uid: dsApi.uid, success: false, path: editLink, From 4e6bdce41c89ba7a121762d8f4a3b26a1f1ed854 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Thu, 6 Feb 2025 12:02:43 +0000 Subject: [PATCH 382/894] Loki query direction: run initialization only in Explore and Dashboards (#100182) --- .../LokiQueryBuilderOptions.test.tsx | 52 +++++++++---------- .../components/LokiQueryBuilderOptions.tsx | 9 +++- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx index bbd8f242906..10253f5851a 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { CoreApp, LogSortOrderChangeEvent, LogsSortOrder, store } from '@grafana/data'; @@ -214,42 +214,42 @@ describe('LokiQueryBuilderOptions', () => { }); describe('Query direction', () => { - it("initializes query direction when it's empty", async () => { + it("initializes query direction when it's empty in Explore or Dashboards", () => { const onChange = jest.fn(); - setup({ expr: '{foo="bar"}' }, onChange); - await waitFor(() => - expect(onChange).toHaveBeenCalledWith({ - expr: '{foo="bar"}', - refId: 'A', - direction: LokiQueryDirection.Backward, - }) - ); + setup({ expr: '{foo="bar"}' }, onChange, { app: CoreApp.Explore }); + expect(onChange).toHaveBeenCalledWith({ + expr: '{foo="bar"}', + refId: 'A', + direction: LokiQueryDirection.Backward, + }); }); - it('uses backward as default in Explore with no previous stored preference', async () => { + it('does not change direction on initialization elsewhere', () => { + const onChange = jest.fn(); + setup({ expr: '{foo="bar"}' }, onChange, { app: undefined }); + expect(onChange).not.toHaveBeenCalled(); + }); + + it('uses backward as default in Explore with no previous stored preference', () => { const onChange = jest.fn(); store.delete('grafana.explore.logs.sortOrder'); setup({ expr: '{foo="bar"}' }, onChange, { app: CoreApp.Explore }); - await waitFor(() => - expect(onChange).toHaveBeenCalledWith({ - expr: '{foo="bar"}', - refId: 'A', - direction: LokiQueryDirection.Backward, - }) - ); + expect(onChange).toHaveBeenCalledWith({ + expr: '{foo="bar"}', + refId: 'A', + direction: LokiQueryDirection.Backward, + }); }); - it('uses the stored sorting option to determine direction in Explore', async () => { + it('uses the stored sorting option to determine direction in Explore', () => { store.set('grafana.explore.logs.sortOrder', LogsSortOrder.Ascending); const onChange = jest.fn(); setup({ expr: '{foo="bar"}' }, onChange, { app: CoreApp.Explore }); - await waitFor(() => - expect(onChange).toHaveBeenCalledWith({ - expr: '{foo="bar"}', - refId: 'A', - direction: LokiQueryDirection.Forward, - }) - ); + expect(onChange).toHaveBeenCalledWith({ + expr: '{foo="bar"}', + refId: 'A', + direction: LokiQueryDirection.Forward, + }); store.delete('grafana.explore.logs.sortOrder'); }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx index 2d36a70f1af..cb4fed551f5 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx @@ -39,6 +39,9 @@ export const LokiQueryBuilderOptions = React.memo( const [splitDurationValid, setSplitDurationValid] = useState(true); useEffect(() => { + if (app !== CoreApp.Explore && app !== CoreApp.Dashboard && app !== CoreApp.PanelEditor) { + return; + } // Initialize the query direction according to the current environment. if (!query.direction) { onChange({ ...query, direction: getDefaultQueryDirection(app) }); @@ -182,7 +185,11 @@ export const LokiQueryBuilderOptions = React.memo( /> - + )} From d64f41afdc975482029a994ac7d891e84ee260ba Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Thu, 6 Feb 2025 07:27:28 -0500 Subject: [PATCH 383/894] SQL Expressions: Re-implement feature using go-mysql-server (#99521) * Under feature flag `sqlExpressions` and is experimental * Excluded from arm32 * Will not work with the Query Service yet * Does not have limits in place yet * Does not working with alerting yet * Currently requires "prepare time series" Transform for time series viz --------- Co-authored-by: Sam Jewell --- go.mod | 12 +- go.sum | 21 +- go.work.sum | 7 +- pkg/expr/convert_to_long.go | 311 ++++++++++++++ pkg/expr/convert_to_long_test.go | 48 +++ pkg/expr/converter.go | 3 +- pkg/expr/converter_test.go | 6 +- pkg/expr/graph.go | 57 +-- pkg/expr/mathexp/types.go | 2 +- pkg/expr/ml.go | 2 +- pkg/expr/nodes.go | 49 ++- pkg/expr/service.go | 5 +- pkg/expr/service_sql_test.go | 104 +++++ pkg/expr/service_test.go | 89 ++-- pkg/expr/sql/db.go | 61 ++- pkg/expr/sql/db_test.go | 187 +++++++++ pkg/expr/sql/dummy_arm.go | 18 + pkg/expr/sql/frame_db.go | 65 +++ pkg/expr/sql/frame_db_conv.go | 474 ++++++++++++++++++++++ pkg/expr/sql/frame_table.go | 126 ++++++ pkg/expr/sql/parser.go | 101 ++--- pkg/expr/sql/parser_allow.go | 136 +++++++ pkg/expr/sql/parser_allow_test.go | 80 ++++ pkg/expr/sql/parser_test.go | 331 ++++++--------- pkg/expr/sql_command.go | 8 +- pkg/registry/apis/query/query.go | 6 +- pkg/services/featuremgmt/codeowners.go | 1 + pkg/services/featuremgmt/registry.go | 2 +- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.json | 9 +- pkg/storage/unified/apistore/go.mod | 10 +- pkg/storage/unified/apistore/go.sum | 21 +- pkg/storage/unified/resource/go.sum | 20 +- 33 files changed, 1969 insertions(+), 405 deletions(-) create mode 100644 pkg/expr/convert_to_long.go create mode 100644 pkg/expr/convert_to_long_test.go create mode 100644 pkg/expr/service_sql_test.go create mode 100644 pkg/expr/sql/db_test.go create mode 100644 pkg/expr/sql/dummy_arm.go create mode 100644 pkg/expr/sql/frame_db.go create mode 100644 pkg/expr/sql/frame_db_conv.go create mode 100644 pkg/expr/sql/frame_table.go create mode 100644 pkg/expr/sql/parser_allow.go create mode 100644 pkg/expr/sql/parser_allow_test.go diff --git a/go.mod b/go.mod index ebfdb244396..82a3b4de7e3 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,8 @@ require ( github.com/centrifugal/centrifuge v0.33.3 // @grafana/grafana-app-platform-squad github.com/crewjam/saml v0.4.13 // @grafana/identity-access-team github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/grafana-backend-group + github.com/dolthub/go-mysql-server v0.19.0 // @grafana/grafana-datasources-core-services + github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 // @grafana/grafana-datasources-core-services github.com/fatih/color v1.17.0 // @grafana/grafana-backend-group github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage @@ -104,7 +106,6 @@ require ( github.com/influxdata/influxdb-client-go/v2 v2.13.0 // @grafana/partner-datasources github.com/influxdata/influxql v1.4.0 // @grafana/partner-datasources github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // @grafana/grafana-app-platform-squad - github.com/jeremywohl/flatten v1.0.1 // @grafana/grafana-app-platform-squad github.com/jmespath-community/go-jmespath v1.1.1 // @grafana/identity-access-team github.com/jmespath/go-jmespath v0.4.0 // indirect; // @grafana/grafana-backend-group github.com/jmoiron/sqlx v1.3.5 // @grafana/grafana-backend-group @@ -315,6 +316,9 @@ require ( github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect + github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 // indirect + github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect 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 @@ -399,6 +403,7 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/lestrrat-go/strftime v1.0.4 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 // indirect @@ -466,9 +471,10 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shadowspore/fossil-delta v0.0.0-20240102155221-e3a8590b820b // indirect - github.com/shopspring/decimal v1.4.0 // indirect + github.com/shopspring/decimal v1.4.0 // @grafana/grafana-datasources-core-services github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sony/gobreaker v0.5.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect @@ -477,6 +483,7 @@ require ( github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tetratelabs/wazero v1.8.2 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect @@ -517,6 +524,7 @@ require ( gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/apiextensions-apiserver v0.32.1 // indirect k8s.io/kms v0.32.1 // indirect diff --git a/go.sum b/go.sum index ce0e5946b08..efdc65bf13c 100644 --- a/go.sum +++ b/go.sum @@ -1057,8 +1057,18 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww= +github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= +github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM= +github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA= +github.com/dolthub/go-mysql-server v0.19.0 h1:NdcXyGt9v7m4sQOahU+ss++iyPy4Q3viuVvbnn3rUTQ= +github.com/dolthub/go-mysql-server v0.19.0/go.mod h1:elfIatfq2fkU5lqTBrTcpL0RcHZOgYPE8EzBD7yQFiY= +github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= +github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= +github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 h1:nzBnC0Rt1gFtscJEz4veYd/mazZEdbdmed+tujdaKOo= +github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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= @@ -1731,8 +1741,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs= -github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= @@ -1826,6 +1834,10 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6Fm github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 h1:X/79QL0b4YJVO5+OsPH9rF2u428CIrGL/jLmPsoOQQ4= github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8= +github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -2308,6 +2320,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= +github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= +github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= github.com/thanos-io/objstore v0.0.0-20240818203309-0363dadfdfb1 h1:z0v9BB/p7s4J6R//+0a5M3wCld8KzNjrGRLIwXfrAZk= github.com/thanos-io/objstore v0.0.0-20240818203309-0363dadfdfb1/go.mod h1:3ukSkG4rIRUGkKM4oIz+BSuUx2e3RlQVVv3Cc3W+Tv4= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -2847,6 +2861,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -3346,6 +3361,8 @@ gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc= +gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg= gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/go.work.sum b/go.work.sum index 0f433090da0..14d84965e7b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1321,6 +1321,8 @@ github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8 h1:IMfrF github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8/go.mod h1:LFyLie6XcDbyKGeVK6bHe+9aJTYCxWLBg5IrJZOaXKA= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= +github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81 h1:7/v8q9XGFa6q5Ap4Z/OhNkAMBaK5YeuEzwJt+NZdhiE= +github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81/go.mod h1:siLfyv2c92W1eN/R4QqG/+RjjX5W2+gCTRjZxBjI3TY= github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw= github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0= github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f h1:/jEs7lulqVO2u1+XI5rW4oFwIIusxuDOVKD9PAzlW2E= @@ -1444,6 +1446,8 @@ github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= github.com/gocql/gocql v0.0.0-20200526081602-cd04bd7f22a7 h1:TvUE5vjfoa7fFHMlmGOk0CsauNj1w4yJjR9+/GnWVCw= github.com/gocql/gocql v0.0.0-20200526081602-cd04bd7f22a7/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= +github.com/gocraft/dbr/v2 v2.7.2 h1:ccUxMuz6RdZvD7VPhMRRMSS/ECF3gytPhPtcavjktHk= +github.com/gocraft/dbr/v2 v2.7.2/go.mod h1:5bCqyIXO5fYn3jEp/L06QF4K1siFdhxChMjdNu6YJrg= github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= @@ -1512,7 +1516,6 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20250115195200-209e052dba64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:V63rh3udd7sqXJeaG+nGUmViwVnM/bY6t8U9Tols2GU= github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= @@ -1608,6 +1611,8 @@ github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInw github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jedib0t/go-pretty/v6 v6.2.4 h1:wdaj2KHD2W+mz8JgJ/Q6L/T5dB7kyqEFI16eLq7GEmk= github.com/jedib0t/go-pretty/v6 v6.2.4/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= +github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs= +github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.1-0.20181029123624-5de817a9aa20/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= diff --git a/pkg/expr/convert_to_long.go b/pkg/expr/convert_to_long.go new file mode 100644 index 00000000000..3ab3339b8b0 --- /dev/null +++ b/pkg/expr/convert_to_long.go @@ -0,0 +1,311 @@ +package expr + +import ( + "fmt" + "sort" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +func ConvertToLong(frames data.Frames) (data.Frames, error) { + if len(frames) == 0 { + // general empty case for now + return frames, nil + } + // Four Conversion Possible Cases + // 1. NumericMulti -> NumericLong + // 2. NumericWide -> NumericLong + // 3. TimeSeriesMulti -> TimeSeriesLong + // 4. TimeSeriesWide -> TimeSeriesLong + + // Detect if input type is declared + // First Check Frame Meta Type + + var inputType data.FrameType + if frames[0].Meta != nil && frames[0].Meta.Type != "" { + inputType = frames[0].Meta.Type + } + + // TODO: Add some guessing of Type if not declared + if inputType == "" { + return frames, fmt.Errorf("no input dataframe type set") + } + + if !supportedToLongConversion(inputType) { + return frames, fmt.Errorf("unsupported input dataframe type %s for SQL expression", inputType) + } + + toLong := getToLongConversionFunc(inputType) + if toLong == nil { + return frames, fmt.Errorf("could not get conversion function for input type %s", inputType) + } + + return toLong(frames) +} + +func convertNumericMultiToNumericLong(frames data.Frames) (data.Frames, error) { + // Apart from metadata, NumericMulti is basically NumericWide, except one frame per thing + // so we collapse into wide and call the wide conversion + wide := convertNumericMultiToNumericWide(frames) + return convertNumericWideToNumericLong(wide) +} + +func convertNumericMultiToNumericWide(frames data.Frames) data.Frames { + newFrame := data.NewFrame("") + for _, frame := range frames { + for _, field := range frame.Fields { + if !field.Type().Numeric() { + continue + } + newField := data.NewFieldFromFieldType(field.Type(), field.Len()) + newField.Name = field.Name + newField.Labels = field.Labels.Copy() + if field.Len() == 1 { + newField.Set(0, field.CopyAt(0)) + } + newFrame.Fields = append(newFrame.Fields, newField) + } + } + return data.Frames{newFrame} +} + +func convertNumericWideToNumericLong(frames data.Frames) (data.Frames, error) { + // Wide should only be one frame + if len(frames) != 1 { + return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames)) + } + inputFrame := frames[0] + + // The Frame should have no more than one row + if inputFrame.Rows() > 1 { + return nil, fmt.Errorf("expected no more than one row in the frame, but got %d", inputFrame.Rows()) + } + + // Gather: + // - unique numeric Field Names, and + // - unique Label Keys (from Numeric Fields only) + // each one maps to a field in the output long Frame. + uniqueNames := make([]string, 0) + uniqueKeys := make([]string, 0) + + uniqueNamesMap := make(map[string]data.FieldType) + uniqueKeysMap := make(map[string]struct{}) + + prints := make(map[string]int) + + registerPrint := func(labels data.Labels) { + fp := labels.Fingerprint().String() + if _, ok := prints[fp]; !ok { + prints[fp] = len(prints) + } + } + + for _, field := range inputFrame.Fields { + if field.Type().Numeric() { + if _, ok := uniqueNamesMap[field.Name]; !ok { + uniqueNames = append(uniqueNames, field.Name) + uniqueNamesMap[field.Name] = field.Type() + } + + if field.Labels != nil { + registerPrint(field.Labels) + for key := range field.Labels { + if _, ok := uniqueKeysMap[key]; !ok { + uniqueKeys = append(uniqueKeys, key) + } + uniqueKeysMap[key] = struct{}{} + } + } + } + } + + // Create new fields for output Long frame + fields := make([]*data.Field, 0, len(uniqueNames)+len(uniqueKeys)) + + // Create the Numeric Fields, tracking the index of each field by name + // Note: May want to use FloatAt and and prepopulate with NaN so missing + // combinations of value can be NA instead of the zero value of 0. + var nameIndexMap = make(map[string]int, len(uniqueNames)) + for i, name := range uniqueNames { + field := data.NewFieldFromFieldType(uniqueNamesMap[name], len(prints)) + field.Name = name + fields = append(fields, field) + nameIndexMap[name] = i + } + + // Create the String fields, tracking the index of each field by key + var keyIndexMap = make(map[string]int, len(uniqueKeys)) + for i, k := range uniqueKeys { + fields = append(fields, data.NewField(k, nil, make([]string, len(prints)))) + keyIndexMap[k] = len(nameIndexMap) + i + } + + longFrame := data.NewFrame("", fields...) + + if inputFrame.Rows() == 0 { + return data.Frames{longFrame}, nil + } + + // Add Rows to the fields + for _, field := range inputFrame.Fields { + if !field.Type().Numeric() { + continue + } + fieldIdx := prints[field.Labels.Fingerprint().String()] + longFrame.Fields[nameIndexMap[field.Name]].Set(fieldIdx, field.CopyAt(0)) + for key, value := range field.Labels { + longFrame.Fields[keyIndexMap[key]].Set(fieldIdx, value) + } + } + + return data.Frames{longFrame}, nil +} + +func convertTimeSeriesMultiToTimeSeriesLong(frames data.Frames) (data.Frames, error) { + // Collect all time values and ensure no duplicates + timeSet := make(map[time.Time]struct{}) + labelKeys := make(map[string]struct{}) // Collect all unique label keys + numericFields := make(map[string]struct{}) // Collect unique numeric field names + + for _, frame := range frames { + for _, field := range frame.Fields { + if field.Type() == data.FieldTypeTime { + for i := 0; i < field.Len(); i++ { + t := field.At(i).(time.Time) + timeSet[t] = struct{}{} + } + } else if field.Type().Numeric() { + numericFields[field.Name] = struct{}{} + if field.Labels != nil { + for key := range field.Labels { + labelKeys[key] = struct{}{} + } + } + } + } + } + + // Create a sorted slice of unique time values + times := make([]time.Time, 0, len(timeSet)) + for t := range timeSet { + times = append(times, t) + } + sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) }) + + // Create output fields: Time, one numeric field per unique numeric name, and label fields + timeField := data.NewField("Time", nil, times) + outputNumericFields := make(map[string]*data.Field) + for name := range numericFields { + outputNumericFields[name] = data.NewField(name, nil, make([]float64, len(times))) + } + outputLabelFields := make(map[string]*data.Field) + for key := range labelKeys { + outputLabelFields[key] = data.NewField(key, nil, make([]string, len(times))) + } + + // Map time to index for quick lookup + timeIndexMap := make(map[time.Time]int, len(times)) + for i, t := range times { + timeIndexMap[t] = i + } + + // Populate output fields + for _, frame := range frames { + var timeField *data.Field + for _, field := range frame.Fields { + if field.Type() == data.FieldTypeTime { + timeField = field + break + } + } + + if timeField == nil { + return nil, fmt.Errorf("no time field found in frame") + } + + for _, field := range frame.Fields { + if field.Type().Numeric() { + for i := 0; i < field.Len(); i++ { + t := timeField.At(i).(time.Time) + val, err := field.FloatAt(i) + if err != nil { + val = 0 // Default value for missing data + } + idx := timeIndexMap[t] + if outputField, exists := outputNumericFields[field.Name]; exists { + outputField.Set(idx, val) + } + + // Add labels for the numeric field + for key, value := range field.Labels { + if outputField, exists := outputLabelFields[key]; exists { + outputField.Set(idx, value) + } + } + } + } + } + } + + // Build the output frame + outputFields := []*data.Field{timeField} + for _, field := range outputNumericFields { + outputFields = append(outputFields, field) + } + for _, field := range outputLabelFields { + outputFields = append(outputFields, field) + } + outputFrame := data.NewFrame("time_series_long", outputFields...) + + // Set metadata + if outputFrame.Meta == nil { + outputFrame.Meta = &data.FrameMeta{} + } + outputFrame.Meta.Type = data.FrameTypeTimeSeriesLong + + return data.Frames{outputFrame}, nil +} + +func convertTimeSeriesWideToTimeSeriesLong(frames data.Frames) (data.Frames, error) { + // Wide should only be one frame + if len(frames) != 1 { + return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames)) + } + inputFrame := frames[0] + longFrame, err := data.WideToLong(inputFrame) + if err != nil { + return nil, fmt.Errorf("failed to convert wide time series to long timeseries for sql expression: %w", err) + } + return data.Frames{longFrame}, nil +} + +func getToLongConversionFunc(inputType data.FrameType) func(data.Frames) (data.Frames, error) { + switch inputType { + case data.FrameTypeNumericMulti: + return convertNumericMultiToNumericLong + case data.FrameTypeNumericWide: + return convertNumericWideToNumericLong + case data.FrameTypeTimeSeriesMulti: + return convertTimeSeriesMultiToTimeSeriesLong + case data.FrameTypeTimeSeriesWide: + return convertTimeSeriesWideToTimeSeriesLong + default: + return convertErr + } +} + +func convertErr(_ data.Frames) (data.Frames, error) { + return nil, fmt.Errorf("unsupported input type for SQL expression") +} + +func supportedToLongConversion(inputType data.FrameType) bool { + switch inputType { + case data.FrameTypeNumericMulti, data.FrameTypeNumericWide: + return true + case data.FrameTypeTimeSeriesMulti, data.FrameTypeTimeSeriesWide: + return true + default: + return false + } +} diff --git a/pkg/expr/convert_to_long_test.go b/pkg/expr/convert_to_long_test.go new file mode 100644 index 00000000000..291fdb62f17 --- /dev/null +++ b/pkg/expr/convert_to_long_test.go @@ -0,0 +1,48 @@ +package expr + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/require" +) + +func TestConvertNumericMultiToLong(t *testing.T) { + input := data.Frames{ + data.NewFrame("test", + data.NewField("Value", data.Labels{"city": "MIA"}, []int64{5})), + data.NewFrame("test", + data.NewField("Value", data.Labels{"city": "LGA"}, []int64{7}), + ), + } + expectedFrame := data.NewFrame("", + data.NewField("Value", nil, []int64{5, 7}), + data.NewField("city", nil, []string{"MIA", "LGA"}), + ) + output, err := convertNumericMultiToNumericLong(input) + require.NoError(t, err) + + if diff := cmp.Diff(expectedFrame, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff) + } +} + +func TestConvertNumericWideToLong(t *testing.T) { + input := data.Frames{ + data.NewFrame("test", + data.NewField("Value", data.Labels{"city": "MIA"}, []int64{5}), + data.NewField("Value", data.Labels{"city": "LGA"}, []int64{7}), + ), + } + expectedFrame := data.NewFrame("", + data.NewField("Value", nil, []int64{5, 7}), + data.NewField("city", nil, []string{"MIA", "LGA"}), + ) + output, err := convertNumericWideToNumericLong(input) + require.NoError(t, err) + + if diff := cmp.Diff(expectedFrame, output[0], data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff) + } +} diff --git a/pkg/expr/converter.go b/pkg/expr/converter.go index 1eae19a972d..0e8cc461d47 100644 --- a/pkg/expr/converter.go +++ b/pkg/expr/converter.go @@ -23,7 +23,6 @@ type ResultConverter struct { func (c *ResultConverter) Convert(ctx context.Context, datasourceType string, frames data.Frames, - allowLongFrames bool, ) (string, mathexp.Results, error) { if len(frames) == 0 { return "no-data", mathexp.Results{Values: mathexp.Values{mathexp.NewNoData()}}, nil @@ -80,7 +79,7 @@ func (c *ResultConverter) Convert(ctx context.Context, continue } - if schema.Type != data.TimeSeriesTypeWide && !allowLongFrames { + if schema.Type != data.TimeSeriesTypeWide { return "", mathexp.Results{}, fmt.Errorf("%w but got type %s (input refid)", ErrSeriesMustBeWide, schema.Type) } filtered = append(filtered, frame) diff --git a/pkg/expr/converter_test.go b/pkg/expr/converter_test.go index c2a8a445247..1509294e3de 100644 --- a/pkg/expr/converter_test.go +++ b/pkg/expr/converter_test.go @@ -40,7 +40,7 @@ func TestConvertDataFramesToResults(t *testing.T) { for _, dtype := range supported { t.Run(dtype, func(t *testing.T) { - resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames) + resultType, res, err := converter.Convert(context.Background(), dtype, frames) require.NoError(t, err) assert.Equal(t, "single frame series", resultType) require.Len(t, res.Values, 2) @@ -68,7 +68,7 @@ func TestConvertDataFramesToResults(t *testing.T) { for _, dtype := range supported { t.Run(dtype, func(t *testing.T) { - resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames) + resultType, res, err := converter.Convert(context.Background(), dtype, frames) require.NoError(t, err) assert.Equal(t, "multi frame series", resultType) require.Len(t, res.Values, 2) @@ -101,7 +101,7 @@ func TestConvertDataFramesToResults(t *testing.T) { for _, dtype := range supported { t.Run(dtype, func(t *testing.T) { - resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames) + resultType, res, err := converter.Convert(context.Background(), dtype, frames) require.NoError(t, err) assert.Equal(t, "multi frame series", resultType) require.Len(t, res.Values, 2) diff --git a/pkg/expr/graph.go b/pkg/expr/graph.go index 369ff116a94..6632a6b74c1 100644 --- a/pkg/expr/graph.go +++ b/pkg/expr/graph.go @@ -77,8 +77,6 @@ func (dp *DataPipeline) execute(c context.Context, now time.Time, s *Service) (m executeDSNodesGrouped(c, now, vars, s, dsNodes) } - s.allowLongFrames = hasSqlExpression(*dp) - for _, node := range *dp { if groupByDSFlag && node.NodeType() == TypeDatasourceNode { continue // already executed via executeDSNodesGrouped @@ -321,12 +319,26 @@ func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error { neededNode, ok := registry[neededVar] if !ok { _, ok := cmdNode.Command.(*SQLCommand) + // If the SSE is a SQL expression, and the node can't be found, it might be a CTE table name + // CTEs are calculated during the evaluation of the SQL, so we won't have a node for them + // So we `continue` in order to support CTE functionality + // TODO: remove CTE table names from the list of table names during parsing of the SQL if ok { continue } return fmt.Errorf("unable to find dependent node '%v'", neededVar) } + // If the input is SQL, conversion is handled differently + if _, ok := cmdNode.Command.(*SQLCommand); ok { + if dsNode, ok := neededNode.(*DSNode); ok { + dsNode.isInputToSQLExpr = true + } else { + // Only allow data source nodes as SQL expression inputs for now + return fmt.Errorf("only data source queries may be inputs to a sql expression, %v is the input for %v", neededVar, cmdNode.RefID()) + } + } + if neededNode.ID() == cmdNode.ID() { return fmt.Errorf("expression '%v' cannot reference itself. Must be query or another expression", neededVar) } @@ -343,6 +355,13 @@ func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error { } } + if neededNode.NodeType() == TypeCMDNode { + if neededNode.(*CMDNode).CMDType == TypeSQL { + // Do not allow SQL expressions to be inputs for other expressions for now + return fmt.Errorf("sql expressions can not be the input for other expressions, but %v in the input for %v", neededVar, cmdNode.RefID()) + } + } + edge := dp.NewEdge(neededNode, cmdNode) dp.SetEdge(edge) @@ -370,37 +389,3 @@ func GetCommandsFromPipeline[T Command](pipeline DataPipeline) []T { } return results } - -func hasSqlExpression(dp DataPipeline) bool { - for _, node := range dp { - if node.NodeType() == TypeCMDNode { - cmdNode := node.(*CMDNode) - _, ok := cmdNode.Command.(*SQLCommand) - if ok { - return true - } - } - } - return false -} - -// func graphHasSqlExpresssion(dp *simple.DirectedGraph) bool { -// node := dp.Nodes() -// for node.Next() { -// if cmdNode, ok := node.Node().(*CMDNode); ok { -// // res[dpNode.RefID()] = dpNode -// _, ok := cmdNode.Command.(*SQLCommand) -// if ok { -// return true -// } -// } -// // if node.NodeType() == TypeCMDNode { -// // cmdNode := node.(*CMDNode) -// // _, ok := cmdNode.Command.(*SQLCommand) -// // if ok { -// // return true -// // } -// // } -// } -// return false -// } diff --git a/pkg/expr/mathexp/types.go b/pkg/expr/mathexp/types.go index 8317f97c5bd..fd79061af82 100644 --- a/pkg/expr/mathexp/types.go +++ b/pkg/expr/mathexp/types.go @@ -250,7 +250,7 @@ func NewNoData() NoData { return NoData{data.NewFrame("no data")} } -// TableData is an untyped no data response. +// TableData is a single table data frame with no labels on any fields. type TableData struct{ Frame *data.Frame } // Type returns the Value type and allows it to fulfill the Value interface. diff --git a/pkg/expr/ml.go b/pkg/expr/ml.go index e7bde5487f2..4c0dba76b89 100644 --- a/pkg/expr/ml.go +++ b/pkg/expr/ml.go @@ -130,7 +130,7 @@ func (m *MLNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s * } // process the response the same way DSNode does. Use plugin ID as data source type. Semantically, they are the same. - responseType, result, err = s.converter.Convert(ctx, mlPluginID, dataFrames, s.allowLongFrames) + responseType, result, err = s.converter.Convert(ctx, mlPluginID, dataFrames) return result, err } diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index 80de28e9883..1159ef13d04 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -112,6 +112,12 @@ func buildCMDNode(rn *rawNode, toggles featuremgmt.FeatureToggles) (*CMDNode, er return nil, fmt.Errorf("invalid command type in expression '%v': %w", rn.RefID, err) } + if commandType == TypeSQL { + if !toggles.IsEnabledGlobally(featuremgmt.FlagSqlExpressions) { + return nil, fmt.Errorf("sql expressions are disabled") + } + } + node := &CMDNode{ baseNode: baseNode{ id: rn.idx, @@ -185,6 +191,8 @@ type DSNode struct { intervalMS int64 maxDP int64 request Request + + isInputToSQLExpr bool } func (dn *DSNode) String() string { @@ -333,7 +341,7 @@ func executeDSNodesGrouped(ctx context.Context, now time.Time, vars mathexp.Vars } var result mathexp.Results - responseType, result, err := s.converter.Convert(ctx, dn.datasource.Type, dataFrames, s.allowLongFrames) + responseType, result, err := s.converter.Convert(ctx, dn.datasource.Type, dataFrames) if err != nil { result.Error = makeConversionError(dn.RefID(), err) } @@ -401,7 +409,44 @@ func (dn *DSNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s } var result mathexp.Results - responseType, result, err = s.converter.Convert(ctx, dn.datasource.Type, dataFrames, s.allowLongFrames) + // If the datasource node is an input to a SQL expression, + // the data must be in the Long format + if dn.isInputToSQLExpr { + var needsConversion bool + // Convert it if Multi: + if len(dataFrames) > 1 { + needsConversion = true + } + + // Convert it if Wide (has labels): + if len(dataFrames) == 1 { + for _, field := range dataFrames[0].Fields { + if len(field.Labels) > 0 { + needsConversion = true + break + } + } + } + + if needsConversion { + convertedFrames, err := ConvertToLong(dataFrames) + if err != nil { + return result, fmt.Errorf("failed to convert data frames to long format for sql: %w", err) + } + result.Values = mathexp.Values{ + mathexp.TableData{Frame: convertedFrames[0]}, + } + return result, nil + } + + // Otherwise it is already Long format; return as is + result.Values = mathexp.Values{ + mathexp.TableData{Frame: dataFrames[0]}, + } + return result, nil + } + + responseType, result, err = s.converter.Convert(ctx, dn.datasource.Type, dataFrames) if err != nil { err = makeConversionError(dn.refID, err) } diff --git a/pkg/expr/service.go b/pkg/expr/service.go index 1f78491d9e3..1b04ce41c3f 100644 --- a/pkg/expr/service.go +++ b/pkg/expr/service.go @@ -64,9 +64,8 @@ type Service struct { pluginsClient backend.CallResourceHandler - tracer tracing.Tracer - metrics *metrics - allowLongFrames bool + tracer tracing.Tracer + metrics *metrics } type pluginContextProvider interface { diff --git a/pkg/expr/service_sql_test.go b/pkg/expr/service_sql_test.go new file mode 100644 index 00000000000..5730ca82fd0 --- /dev/null +++ b/pkg/expr/service_sql_test.go @@ -0,0 +1,104 @@ +package expr + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/stretchr/testify/require" +) + +func TestSQLService(t *testing.T) { + inputFrame := data.NewFrame("", + data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), + data.NewField("value", nil, []*float64{fp(2)}), + ) + + resp := map[string]backend.DataResponse{ + "A": {Frames: data.Frames{inputFrame}}, + } + + newABSQLQueries := func(q string) []Query { + q, err := jsonEscape(q) + require.NoError(t, err) + return []Query{ + { + RefID: "A", + DataSource: &datasources.DataSource{ + OrgID: 1, + UID: "test", + Type: "test", + }, + JSON: json.RawMessage(`{ "datasource": { "uid": "1" }, "intervalMs": 1000, "maxDataPoints": 1000 }`), + TimeRange: AbsoluteTimeRange{ + From: time.Time{}, + To: time.Time{}, + }, + }, + { + RefID: "B", + DataSource: dataSourceModel(), + JSON: json.RawMessage(fmt.Sprintf(`{ "datasource": { "uid": "__expr__", "type": "__expr__"}, "type": "sql", "expression": "%s" }`, q)), + TimeRange: AbsoluteTimeRange{ + From: time.Time{}, + To: time.Time{}, + }, + }, + } + } + t.Run("no feature flag no queries for you", func(t *testing.T) { + s, req := newMockQueryService(resp, newABSQLQueries("")) + + _, err := s.BuildPipeline(req) + require.Error(t, err, "should not be able to build pipeline without feature flag") + }) + + t.Run("with feature flag basic select works", func(t *testing.T) { + s, req := newMockQueryService(resp, newABSQLQueries("SELECT * FROM A")) + s.features = featuremgmt.WithFeatures(featuremgmt.FlagSqlExpressions) + pl, err := s.BuildPipeline(req) + require.NoError(t, err) + + res, err := s.ExecutePipeline(context.Background(), time.Now(), pl) + require.NoError(t, err) + + inputFrame.RefID = "B" + inputFrame.Name = "B" + if diff := cmp.Diff(res.Responses["B"].Frames[0], inputFrame, data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff) + } + }) + + t.Run("load_file is blocked", func(t *testing.T) { + s, req := newMockQueryService(resp, + newABSQLQueries(`SELECT CAST(load_file('/etc/topSecretz') AS CHAR(10000) CHARACTER SET utf8)`), + ) + + s.features = featuremgmt.WithFeatures(featuremgmt.FlagSqlExpressions) + + pl, err := s.BuildPipeline(req) + require.NoError(t, err) + + rsp, err := s.ExecutePipeline(context.Background(), time.Now(), pl) + require.NoError(t, err) + + require.Error(t, rsp.Responses["B"].Error, "should return invalid sql error") + require.ErrorContains(t, rsp.Responses["B"].Error, "blocked function load_file") + }) +} + +func jsonEscape(input string) (string, error) { + escaped, err := json.Marshal(input) + if err != nil { + return "", err + } + // json.Marshal returns the escaped string with quotes, so we need to trim them + return string(escaped[1 : len(escaped)-1]), nil +} diff --git a/pkg/expr/service_test.go b/pkg/expr/service_test.go index 35378b289e8..2fe6f6e1e9c 100644 --- a/pkg/expr/service_test.go +++ b/pkg/expr/service_test.go @@ -29,32 +29,11 @@ import ( func TestService(t *testing.T) { dsDF := data.NewFrame("test", data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), - data.NewField("value", data.Labels{"test": "label"}, []*float64{fp(2)})) + data.NewField("value", data.Labels{"test": "label"}, []*float64{fp(2)}), + ) - me := &mockEndpoint{ - Responses: map[string]backend.DataResponse{ - "A": {Frames: data.Frames{dsDF}}, - }, - } - - pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), nil, &pluginstore.FakePluginStore{ - PluginList: []pluginstore.Plugin{ - {JSONData: plugins.JSONData{ID: "test"}}, - }, - }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider()) - - features := featuremgmt.WithFeatures() - s := Service{ - cfg: setting.NewCfg(), - dataService: me, - pCtxProvider: pCtxProvider, - features: features, - tracer: tracing.InitializeTracerForTest(), - metrics: newMetrics(nil), - converter: &ResultConverter{ - Features: features, - Tracer: tracing.InitializeTracerForTest(), - }, + resp := map[string]backend.DataResponse{ + "A": {Frames: data.Frames{dsDF}}, } queries := []Query{ @@ -78,7 +57,7 @@ func TestService(t *testing.T) { }, } - req := &Request{Queries: queries, User: &user.SignedInUser{}} + s, req := newMockQueryService(resp, queries) pl, err := s.BuildPipeline(req) require.NoError(t, err) @@ -121,26 +100,9 @@ func TestService(t *testing.T) { } func TestDSQueryError(t *testing.T) { - me := &mockEndpoint{ - Responses: map[string]backend.DataResponse{ - "A": {Error: fmt.Errorf("womp womp")}, - "B": {Frames: data.Frames{}}, - }, - } - - pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), nil, &pluginstore.FakePluginStore{ - PluginList: []pluginstore.Plugin{ - {JSONData: plugins.JSONData{ID: "test"}}, - }, - }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider()) - - s := Service{ - cfg: setting.NewCfg(), - dataService: me, - pCtxProvider: pCtxProvider, - features: featuremgmt.WithFeatures(), - tracer: tracing.InitializeTracerForTest(), - metrics: newMetrics(nil), + resp := map[string]backend.DataResponse{ + "A": {Error: fmt.Errorf("womp womp")}, + "B": {Frames: data.Frames{}}, } queries := []Query{ @@ -169,19 +131,19 @@ func TestDSQueryError(t *testing.T) { }, } - req := &Request{Queries: queries, User: &user.SignedInUser{}} + s, req := newMockQueryService(resp, queries) pl, err := s.BuildPipeline(req) require.NoError(t, err) - resp, err := s.ExecutePipeline(context.Background(), time.Now(), pl) + res, err := s.ExecutePipeline(context.Background(), time.Now(), pl) require.NoError(t, err) var utilErr errutil.Error - require.ErrorContains(t, resp.Responses["A"].Error, "womp womp") - require.ErrorAs(t, resp.Responses["B"].Error, &utilErr) + require.ErrorContains(t, res.Responses["A"].Error, "womp womp") + require.ErrorAs(t, res.Responses["B"].Error, &utilErr) require.ErrorIs(t, utilErr, DependencyError) - require.Equal(t, fp(42), resp.Responses["C"].Frames[0].Fields[0].At(0)) + require.Equal(t, fp(42), res.Responses["C"].Frames[0].Fields[0].At(0)) } func fp(f float64) *float64 { @@ -204,3 +166,28 @@ func dataSourceModel() *datasources.DataSource { d, _ := DataSourceModelFromNodeType(TypeCMDNode) return d } + +func newMockQueryService(responses map[string]backend.DataResponse, queries []Query) (*Service, *Request) { + me := &mockEndpoint{ + Responses: responses, + } + pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), nil, &pluginstore.FakePluginStore{ + PluginList: []pluginstore.Plugin{ + {JSONData: plugins.JSONData{ID: "test"}}, + }, + }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider()) + + features := featuremgmt.WithFeatures() + return &Service{ + cfg: setting.NewCfg(), + dataService: me, + pCtxProvider: pCtxProvider, + features: featuremgmt.WithFeatures(), + tracer: tracing.InitializeTracerForTest(), + metrics: newMetrics(nil), + converter: &ResultConverter{ + Features: features, + Tracer: tracing.InitializeTracerForTest(), + }, + }, &Request{Queries: queries, User: &user.SignedInUser{}} +} diff --git a/pkg/expr/sql/db.go b/pkg/expr/sql/db.go index 90e23342c4b..f1af425ad6d 100644 --- a/pkg/expr/sql/db.go +++ b/pkg/expr/sql/db.go @@ -1,22 +1,61 @@ +//go:build !arm + package sql import ( - "errors" + "context" + sqle "github.com/dolthub/go-mysql-server" + mysql "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/go-mysql-server/sql/analyzer" "github.com/grafana/grafana-plugin-sdk-go/data" ) -type DB struct { -} +// DB is a database that can execute SQL queries against a set of Frames. +type DB struct{} -func (db *DB) RunCommands(commands []string) (string, error) { - return "", errors.New("not implemented") -} +// QueryFrames runs the sql query query against a database created from frames, and returns the frame. +// The RefID of each frame becomes a table in the database. +// It is expected that there is only one frame per RefID. +// The name becomes the name and RefID of the returned frame. +func (db *DB) QueryFrames(ctx context.Context, name string, query string, frames []*data.Frame) (*data.Frame, error) { + // We are parsing twice due to TablesList, but don't care fow now. We can save the parsed query and reuse it later if we want. + if allow, err := AllowQuery(query); err != nil || !allow { + if err != nil { + return nil, err + } + return nil, err + } -func (db *DB) QueryFramesInto(name string, query string, frames []*data.Frame, f *data.Frame) error { - return errors.New("not implemented") -} + pro := NewFramesDBProvider(frames) + session := mysql.NewBaseSession() + mCtx := mysql.NewContext(ctx, mysql.WithSession(session)) -func NewInMemoryDB() *DB { - return &DB{} + // Select the database in the context + mCtx.SetCurrentDatabase(dbName) + + // Empty dir does not disable secure_file_priv + //ctx.SetSessionVariable(ctx, "secure_file_priv", "") + + // TODO: Check if it's wise to reuse the existing provider, rather than creating a new one + a := analyzer.NewDefault(pro) + + engine := sqle.New(a, &sqle.Config{ + IsReadOnly: true, + }) + + schema, iter, _, err := engine.Query(mCtx, query) + if err != nil { + return nil, err + } + + f, err := convertToDataFrame(mCtx, iter, schema) + if err != nil { + return nil, err + } + f.Name = name + f.RefID = name + + return f, nil } diff --git a/pkg/expr/sql/db_test.go b/pkg/expr/sql/db_test.go new file mode 100644 index 00000000000..258507121c2 --- /dev/null +++ b/pkg/expr/sql/db_test.go @@ -0,0 +1,187 @@ +//go:build !arm + +package sql + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/require" +) + +func TestQueryFrames(t *testing.T) { + db := DB{} + + tests := []struct { + name string + query string + input_frames []*data.Frame + expected *data.Frame + }{ + { + name: "valid query with no input frames, one row one column", + query: `SELECT '1' AS 'n';`, + input_frames: []*data.Frame{}, + expected: data.NewFrame( + "sqlExpressionRefId", + data.NewField("n", nil, []string{"1"}), + ), + }, + { + name: "valid query with no input frames, one row two columns", + query: `SELECT 'sam' AS 'name', 40 AS 'age';`, + input_frames: []*data.Frame{}, + expected: data.NewFrame( + "sqlExpressionRefId", + data.NewField("name", nil, []string{"sam"}), + data.NewField("age", nil, []int8{40}), + ), + }, + { + // TODO: Also ORDER BY to ensure the order is preserved + name: "query all rows from single input frame", + query: `SELECT * FROM inputFrameRefId LIMIT 1;`, + input_frames: []*data.Frame{ + setRefID(data.NewFrame( + "", + //nolint:misspell + data.NewField("OSS Projects with Typos", nil, []string{"Garfana", "Pormetheus"}), + ), "inputFrameRefId"), + }, + expected: data.NewFrame( + "sqlExpressionRefId", + data.NewField("OSS Projects with Typos", nil, []string{"Garfana"}), + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + frame, err := db.QueryFrames(context.Background(), "sqlExpressionRefId", tt.query, tt.input_frames) + require.NoError(t, err) + require.NotNil(t, frame.Fields) + + require.Equal(t, tt.expected.Name, frame.RefID) + require.Equal(t, len(tt.expected.Fields), len(frame.Fields)) + for i := range tt.expected.Fields { + require.Equal(t, tt.expected.Fields[i].Name, frame.Fields[i].Name) + require.Equal(t, tt.expected.Fields[i].At(0), frame.Fields[i].At(0)) + } + }) + } +} + +func TestQueryFramesInOut(t *testing.T) { + frameA := &data.Frame{ + RefID: "a", + Name: "a", + Fields: []*data.Field{ + data.NewField("time", nil, []time.Time{time.Now(), time.Now()}), + data.NewField("time_nullable", nil, []*time.Time{p(time.Now()), nil}), + + data.NewField("string", nil, []string{"cat", "dog"}), + data.NewField("null_nullable", nil, []*string{p("cat"), nil}), + + data.NewField("float64", nil, []float64{1, 3}), + data.NewField("float64_nullable", nil, []*float64{p(2.0), nil}), + + data.NewField("int64", nil, []int64{1, 3}), + data.NewField("int64_nullable", nil, []*int64{p(int64(2)), nil}), + + data.NewField("bool", nil, []bool{true, false}), + data.NewField("bool_nullable", nil, []*bool{p(true), nil}), + }, + } + + db := DB{} + qry := `SELECT * from a` + + resultFrame, err := db.QueryFrames(context.Background(), "a", qry, []*data.Frame{frameA}) + require.NoError(t, err) + + if diff := cmp.Diff(frameA, resultFrame, data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff) + } +} + +func TestQueryFramesNumericSelect(t *testing.T) { + expectedFrame := &data.Frame{ + RefID: "a", + Name: "a", + Fields: []*data.Field{ + data.NewField("decimal", nil, []float64{2.35}), + data.NewField("tinySigned", nil, []int8{-128}), + data.NewField("smallSigned", nil, []int16{-32768}), + data.NewField("mediumSigned", nil, []int32{-8388608}), + data.NewField("intSigned", nil, []int32{-2147483648}), + data.NewField("bigSigned", nil, []int64{-9223372036854775808}), + data.NewField("tinyUnsigned", nil, []uint8{255}), + data.NewField("smallUnsigned", nil, []uint16{65535}), + data.NewField("mediumUnsigned", nil, []int32{16777215}), + data.NewField("intUnsigned", nil, []uint32{4294967295}), + data.NewField("bigUnsigned", nil, []uint64{18446744073709551615}), + }, + } + + db := DB{} + qry := `SELECT 2.35 AS 'decimal', + -128 AS 'tinySigned', + -32768 AS 'smallSigned', + -8388608 AS 'mediumSigned', + -2147483648 AS 'intSigned', + -9223372036854775808 AS 'bigSigned', + 255 AS 'tinyUnsigned', + 65535 AS 'smallUnsigned', + 16777215 AS 'mediumUnsigned', + 4294967295 AS 'intUnsigned', + 18446744073709551615 AS 'bigUnsigned'` + + resultFrame, err := db.QueryFrames(context.Background(), "a", qry, []*data.Frame{}) + require.NoError(t, err) + + if diff := cmp.Diff(expectedFrame, resultFrame, data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff) + } +} + +func TestQueryFramesDateTimeSelect(t *testing.T) { + t.Skip("need a fix in go-mysql-server, and then handle the datetime strings (or figure out why strings and not time.Time)") + expectedFrame := &data.Frame{ + RefID: "a", + Name: "a", + Fields: []*data.Field{ + data.NewField("ts", nil, []time.Time{}), + }, + } + + db := DB{} + + // It doesn't like the T in the time string + qry := `SELECT str_to_date('2025-02-03T03:00:00','%Y-%m-%dT%H:%i:%s') as ts` + + // This comes back as a string, which needs to be dealt with? + //qry := `SELECT str_to_date('2025-02-03-03:00:00','%Y-%m-%d-%H:%i:%s') as ts` + + // This is a datetime(6), need to deal with that as well + //qry := `SELECT current_timestamp() as ts` + + f, err := db.QueryFrames(context.Background(), "b", qry, []*data.Frame{}) + require.NoError(t, err) + + if diff := cmp.Diff(expectedFrame, f, data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff) + } +} + +// p is a utility for pointers from constants +func p[T any](v T) *T { + return &v +} + +func setRefID(f *data.Frame, refID string) *data.Frame { + f.RefID = refID + return f +} diff --git a/pkg/expr/sql/dummy_arm.go b/pkg/expr/sql/dummy_arm.go new file mode 100644 index 00000000000..95d20159e08 --- /dev/null +++ b/pkg/expr/sql/dummy_arm.go @@ -0,0 +1,18 @@ +//go:build arm + +package sql + +import ( + "context" + "fmt" + + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +type DB struct{} + +// Stub out the QueryFrames method for ARM builds +// See github.com/dolthub/go-mysql-server/issues/2837 +func (db *DB) QueryFrames(_ context.Context, _, _ string, _ []*data.Frame) (*data.Frame, error) { + return nil, fmt.Errorf("sql expressions not supported in arm") +} diff --git a/pkg/expr/sql/frame_db.go b/pkg/expr/sql/frame_db.go new file mode 100644 index 00000000000..26317594746 --- /dev/null +++ b/pkg/expr/sql/frame_db.go @@ -0,0 +1,65 @@ +//go:build !arm + +package sql + +import ( + mysql "github.com/dolthub/go-mysql-server/sql" + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +var dbName = "frames" + +// FramesDBProvider is a go-mysql-server DatabaseProvider that provides access to a set of Frames. +type FramesDBProvider struct { + db mysql.Database +} + +func (p *FramesDBProvider) Database(_ *mysql.Context, _ string) (mysql.Database, error) { + return p.db, nil +} + +func (p *FramesDBProvider) HasDatabase(_ *mysql.Context, _ string) bool { + return true +} + +func (p *FramesDBProvider) AllDatabases(_ *mysql.Context) []mysql.Database { + return []mysql.Database{p.db} +} + +// NewFramesDBProvider creates a new FramesDBProvider with the given set of Frames. +func NewFramesDBProvider(frames data.Frames) mysql.DatabaseProvider { + fMap := make(map[string]mysql.Table, len(frames)) + for _, frame := range frames { + fMap[frame.RefID] = &FrameTable{Frame: frame} + } + return &FramesDBProvider{ + db: &framesDB{ + frames: fMap, + }, + } +} + +// framesDB is a go-mysql-server Database that provides access to a set of Frames. +type framesDB struct { + frames map[string]mysql.Table +} + +func (db *framesDB) GetTableInsensitive(_ *mysql.Context, tblName string) (mysql.Table, bool, error) { + tbl, ok := mysql.GetTableInsensitive(tblName, db.frames) + if !ok { + return nil, false, nil + } + return tbl, ok, nil +} + +func (db *framesDB) GetTableNames(_ *mysql.Context) ([]string, error) { + s := make([]string, 0, len(db.frames)) + for k := range db.frames { + s = append(s, k) + } + return s, nil +} + +func (db *framesDB) Name() string { + return dbName +} diff --git a/pkg/expr/sql/frame_db_conv.go b/pkg/expr/sql/frame_db_conv.go new file mode 100644 index 00000000000..ce0edfc2341 --- /dev/null +++ b/pkg/expr/sql/frame_db_conv.go @@ -0,0 +1,474 @@ +//go:build !arm + +package sql + +import ( + "errors" + "fmt" + "io" + "time" + + mysql "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/shopspring/decimal" +) + +// TODO: Should this accept a row limit and converters, like sqlutil.FrameFromRows? +func convertToDataFrame(ctx *mysql.Context, iter mysql.RowIter, schema mysql.Schema) (*data.Frame, error) { + f := &data.Frame{} + // Create fields based on the schema + for _, col := range schema { + fT, err := MySQLColToFieldType(col) + if err != nil { + return nil, err + } + + field := data.NewFieldFromFieldType(fT, 0) + field.Name = col.Name + f.Fields = append(f.Fields, field) + } + + // Iterate through the rows and append data to fields + for { + row, err := iter.Next(ctx) + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, fmt.Errorf("error reading row: %v", err) + } + + for i, val := range row { + v, err := fieldValFromRowVal(f.Fields[i].Type(), val) + if err != nil { + return nil, fmt.Errorf("unexpected type for column %s: %w", schema[i].Name, err) + } + f.Fields[i].Append(v) + } + } + + return f, nil +} + +// MySQLColToFieldType converts a MySQL column to a data.FieldType +func MySQLColToFieldType(col *mysql.Column) (data.FieldType, error) { + var fT data.FieldType + + switch col.Type { + case types.Int8: + fT = data.FieldTypeInt8 + case types.Uint8: + fT = data.FieldTypeUint8 + case types.Int16: + fT = data.FieldTypeInt16 + case types.Uint16: + fT = data.FieldTypeUint16 + case types.Int32: + fT = data.FieldTypeInt32 + case types.Uint32: + fT = data.FieldTypeUint32 + case types.Int64: + fT = data.FieldTypeInt64 + case types.Uint64: + fT = data.FieldTypeUint64 + case types.Float64: + fT = data.FieldTypeFloat64 + // StringType represents all string types, including VARCHAR and BLOB. + case types.Text, types.LongText: + fT = data.FieldTypeString + case types.Timestamp: + fT = data.FieldTypeTime + case types.Datetime: + fT = data.FieldTypeTime + case types.Boolean: + fT = data.FieldTypeBool + default: + if types.IsDecimal(col.Type) { + fT = data.FieldTypeFloat64 + } else { + return fT, fmt.Errorf("unsupported type for column %s of type %v", col.Name, col.Type) + } + } + + if col.Nullable { + fT = fT.NullableType() + } + + return fT, nil +} + +// Helper function to convert data.FieldType to types.Type +func convertDataType(fieldType data.FieldType) mysql.Type { + switch fieldType { + case data.FieldTypeInt8, data.FieldTypeNullableInt8: + return types.Int8 + case data.FieldTypeUint8, data.FieldTypeNullableUint8: + return types.Uint8 + case data.FieldTypeInt16, data.FieldTypeNullableInt16: + return types.Int16 + case data.FieldTypeUint16, data.FieldTypeNullableUint16: + return types.Uint16 + case data.FieldTypeInt32, data.FieldTypeNullableInt32: + return types.Int32 + case data.FieldTypeUint32, data.FieldTypeNullableUint32: + return types.Uint32 + case data.FieldTypeInt64, data.FieldTypeNullableInt64: + return types.Int64 + case data.FieldTypeUint64, data.FieldTypeNullableUint64: + return types.Uint64 + case data.FieldTypeFloat32, data.FieldTypeNullableFloat32: + return types.Float32 + case data.FieldTypeFloat64, data.FieldTypeNullableFloat64: + return types.Float64 + case data.FieldTypeString, data.FieldTypeNullableString: + return types.Text + case data.FieldTypeBool, data.FieldTypeNullableBool: + return types.Boolean + case data.FieldTypeTime, data.FieldTypeNullableTime: + return types.Timestamp + default: + fmt.Printf("------- Unsupported field type: %v", fieldType) + return types.JSON + } +} + +// fieldValFromRowVal converts a go-mysql-server row value to a data.field value +// +//nolint:gocyclo +func fieldValFromRowVal(fieldType data.FieldType, val interface{}) (interface{}, error) { + // the input val may be nil, it also may not be a pointer even if the fieldtype is a nullable pointer type + if val == nil { + return nil, nil + } + + switch fieldType { + // ---------------------------- + // Int8 / Nullable Int8 + // ---------------------------- + case data.FieldTypeInt8: + v, ok := val.(int8) + if !ok { + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int8", val, val) + } + return v, nil + + case data.FieldTypeNullableInt8: + vP, ok := val.(*int8) + if ok { + return vP, nil + } + v, ok := val.(int8) + if ok { + return &v, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int8 or *int8", val, val) + + // ---------------------------- + // Uint8 / Nullable Uint8 + // ---------------------------- + case data.FieldTypeUint8: + v, ok := val.(uint8) + if !ok { + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint8", val, val) + } + return v, nil + + case data.FieldTypeNullableUint8: + vP, ok := val.(*uint8) + if ok { + return vP, nil + } + v, ok := val.(uint8) + if ok { + return &v, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint8 or *uint8", val, val) + + // ---------------------------- + // Int16 / Nullable Int16 + // ---------------------------- + case data.FieldTypeInt16: + v, ok := val.(int16) + if !ok { + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int16", val, val) + } + return v, nil + + case data.FieldTypeNullableInt16: + vP, ok := val.(*int16) + if ok { + return vP, nil + } + v, ok := val.(int16) + if ok { + return &v, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int16 or *int16", val, val) + + // ---------------------------- + // Uint16 / Nullable Uint16 + // ---------------------------- + case data.FieldTypeUint16: + v, ok := val.(uint16) + if !ok { + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint16", val, val) + } + return v, nil + + case data.FieldTypeNullableUint16: + vP, ok := val.(*uint16) + if ok { + return vP, nil + } + v, ok := val.(uint16) + if ok { + return &v, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint16 or *uint16", val, val) + + // ---------------------------- + // Int32 / Nullable Int32 + // ---------------------------- + case data.FieldTypeInt32: + v, ok := val.(int32) + if !ok { + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int32", val, val) + } + return v, nil + + case data.FieldTypeNullableInt32: + vP, ok := val.(*int32) + if ok { + return vP, nil + } + v, ok := val.(int32) + if ok { + return &v, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int32 or *int32", val, val) + + // ---------------------------- + // Uint32 / Nullable Uint32 + // ---------------------------- + case data.FieldTypeUint32: + v, ok := val.(uint32) + if !ok { + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint32", val, val) + } + return v, nil + + case data.FieldTypeNullableUint32: + vP, ok := val.(*uint32) + if ok { + return vP, nil + } + v, ok := val.(uint32) + if ok { + return &v, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint32 or *uint32", val, val) + + // ---------------------------- + // Int64 / Nullable Int64 + // ---------------------------- + case data.FieldTypeInt64: + v, ok := val.(int64) + if !ok { + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int64", val, val) + } + return v, nil + + case data.FieldTypeNullableInt64: + vP, ok := val.(*int64) + if ok { + return vP, nil + } + v, ok := val.(int64) + if ok { + return &v, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int64 or *int64", val, val) + + // ---------------------------- + // Uint64 / Nullable Uint64 + // ---------------------------- + case data.FieldTypeUint64: + v, ok := val.(uint64) + if !ok { + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint64", val, val) + } + return v, nil + + case data.FieldTypeNullableUint64: + vP, ok := val.(*uint64) + if ok { + return vP, nil + } + v, ok := val.(uint64) + if ok { + return &v, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint64 or *uint64", val, val) + + // ---------------------------- + // Float64 / Nullable Float64 + // ---------------------------- + case data.FieldTypeFloat64: + // Accept float64 or decimal.Decimal, convert decimal.Decimal -> float64 + if v, ok := val.(float64); ok { + return v, nil + } + if d, ok := val.(decimal.Decimal); ok { + return d.InexactFloat64(), nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected float64 or decimal.Decimal", val, val) + + case data.FieldTypeNullableFloat64: + // Possibly already *float64 + if vP, ok := val.(*float64); ok { + return vP, nil + } + // Possibly float64 + if v, ok := val.(float64); ok { + return &v, nil + } + // Possibly decimal.Decimal + if d, ok := val.(decimal.Decimal); ok { + f := d.InexactFloat64() + return &f, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected float64, *float64, or decimal.Decimal", val, val) + + // ---------------------------- + // Time / Nullable Time + // ---------------------------- + case data.FieldTypeTime: + v, ok := val.(time.Time) + if !ok { + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected time.Time", val, val) + } + return v, nil + + case data.FieldTypeNullableTime: + vP, ok := val.(*time.Time) + if ok { + return vP, nil + } + v, ok := val.(time.Time) + if ok { + return &v, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected time.Time or *time.Time", val, val) + + // ---------------------------- + // String / Nullable String + // ---------------------------- + case data.FieldTypeString: + v, ok := val.(string) + if !ok { + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected string", val, val) + } + return v, nil + + case data.FieldTypeNullableString: + vP, ok := val.(*string) + if ok { + return vP, nil + } + v, ok := val.(string) + if ok { + return &v, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected string or *string", val, val) + + // ---------------------------- + // Bool / Nullable Bool + // ---------------------------- + case data.FieldTypeBool: + v, ok := val.(bool) + if !ok { + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected bool", val, val) + } + return v, nil + + case data.FieldTypeNullableBool: + vP, ok := val.(*bool) + if ok { + return vP, nil + } + v, ok := val.(bool) + if ok { + return &v, nil + } + return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected bool or *bool", val, val) + + // ---------------------------- + // Fallback / Unsupported + // ---------------------------- + default: + return nil, fmt.Errorf("unsupported field type %s for val %v", fieldType, val) + } +} + +// Is the field nilAt the index. Can panic if out of range. +// TODO: Maybe this should be a method on data.Field? +func nilAt(field data.Field, at int) bool { + if !field.Nullable() { + return false + } + + switch field.Type() { + case data.FieldTypeNullableInt8: + v := field.At(at).(*int8) + return v == nil + + case data.FieldTypeNullableUint8: + v := field.At(at).(*uint8) + return v == nil + + case data.FieldTypeNullableInt16: + v := field.At(at).(*int16) + return v == nil + + case data.FieldTypeNullableUint16: + v := field.At(at).(*uint16) + return v == nil + + case data.FieldTypeNullableInt32: + v := field.At(at).(*int32) + return v == nil + + case data.FieldTypeNullableUint32: + v := field.At(at).(*uint32) + return v == nil + + case data.FieldTypeNullableInt64: + v := field.At(at).(*int64) + return v == nil + + case data.FieldTypeNullableUint64: + v := field.At(at).(*uint64) + return v == nil + + case data.FieldTypeNullableFloat64: + v := field.At(at).(*float64) + return v == nil + + case data.FieldTypeNullableString: + v := field.At(at).(*string) + return v == nil + + case data.FieldTypeNullableTime: + v := field.At(at).(*time.Time) + return v == nil + + case data.FieldTypeNullableBool: + v := field.At(at).(*bool) + return v == nil + + default: + // Either it's not a nullable type or it's unsupported + return false + } +} diff --git a/pkg/expr/sql/frame_table.go b/pkg/expr/sql/frame_table.go new file mode 100644 index 00000000000..a511a3606fa --- /dev/null +++ b/pkg/expr/sql/frame_table.go @@ -0,0 +1,126 @@ +//go:build !arm + +package sql + +import ( + "io" + "strings" + + mysql "github.com/dolthub/go-mysql-server/sql" + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +// FrameTable fulfills the mysql.Table interface for a data.Frame. +type FrameTable struct { + Frame *data.Frame + schema mysql.Schema +} + +// Name implements the sql.Nameable interface +func (ft *FrameTable) Name() string { + return ft.Frame.RefID +} + +// String implements the fmt.Stringer interface +func (ft *FrameTable) String() string { + return ft.Name() +} + +func schemaFromFrame(frame *data.Frame) mysql.Schema { + schema := make(mysql.Schema, len(frame.Fields)) + + for i, field := range frame.Fields { + schema[i] = &mysql.Column{ + Name: field.Name, + Type: convertDataType(field.Type()), + Nullable: field.Type().Nullable(), + Source: strings.ToLower(frame.RefID), + } + } + + return schema +} + +// Schema implements the mysql.Table interface +func (ft *FrameTable) Schema() mysql.Schema { + if ft.schema == nil { + ft.schema = schemaFromFrame(ft.Frame) + } + return ft.schema +} + +// Collation implements the mysql.Table interface +func (ft *FrameTable) Collation() mysql.CollationID { + return mysql.Collation_Unspecified +} + +// Partitions implements the mysql.Table interface +func (ft *FrameTable) Partitions(ctx *mysql.Context) (mysql.PartitionIter, error) { + return &noopPartitionIter{}, nil +} + +// PartitionRows implements the mysql.Table interface +func (ft *FrameTable) PartitionRows(ctx *mysql.Context, _ mysql.Partition) (mysql.RowIter, error) { + return &rowIter{ft: ft, row: 0}, nil +} + +type rowIter struct { + ft *FrameTable + row int +} + +func (ri *rowIter) Next(_ *mysql.Context) (mysql.Row, error) { + // We assume each field in the Frame has the same number of rows. + numRows := 0 + if len(ri.ft.Frame.Fields) > 0 { + numRows = ri.ft.Frame.Fields[0].Len() + } + + // If we've already exhausted all rows, return EOF + if ri.row >= numRows { + return nil, io.EOF + } + + // Construct a Row (which is []interface{} under the hood) by pulling + // the value from each column at the current row index. + row := make(mysql.Row, len(ri.ft.Frame.Fields)) + for colIndex, field := range ri.ft.Frame.Fields { + if nilAt(*field, ri.row) { + continue + } + row[colIndex], _ = field.ConcreteAt(ri.row) + } + + ri.row++ + return row, nil +} + +// Close implements the mysql.RowIter interface. +// In this no-op example, there isn't anything to do here. +func (ri *rowIter) Close(*mysql.Context) error { + return nil +} + +type noopPartitionIter struct { + done bool +} + +func (i *noopPartitionIter) Next(*mysql.Context) (mysql.Partition, error) { + if !i.done { + i.done = true + return noopParition, nil + } + return nil, io.EOF +} + +func (i *noopPartitionIter) Close(*mysql.Context) error { + return nil +} + +var noopParition = partition(nil) + +type partition []byte + +func (p partition) Key() []byte { + return p +} diff --git a/pkg/expr/sql/parser.go b/pkg/expr/sql/parser.go index b5456b3b6ff..12269b4a68f 100644 --- a/pkg/expr/sql/parser.go +++ b/pkg/expr/sql/parser.go @@ -1,89 +1,62 @@ package sql import ( - "encoding/json" "fmt" "sort" - "strings" + "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/grafana/grafana/pkg/infra/log" - "github.com/jeremywohl/flatten" -) - -const ( - TABLE_NAME = "table_name" - ERROR = ".error" - ERROR_MESSAGE = ".error_message" ) var logger = log.New("sql_expr") // TablesList returns a list of tables for the sql statement func TablesList(rawSQL string) ([]string, error) { - db := NewInMemoryDB() - rawSQL = strings.Replace(rawSQL, "'", "''", -1) - cmd := fmt.Sprintf("SELECT json_serialize_sql('%s')", rawSQL) - ret, err := db.RunCommands([]string{cmd}) + stmt, err := sqlparser.Parse(rawSQL) if err != nil { - logger.Error("error serializing sql", "error", err.Error(), "sql", rawSQL, "cmd", cmd) - return nil, fmt.Errorf("error serializing sql: %s", err.Error()) + logger.Error("error parsing sql: %s", err.Error(), "sql", rawSQL) + return nil, fmt.Errorf("error parsing sql: %s", err.Error()) } - ast := []map[string]any{} - err = json.Unmarshal([]byte(ret), &ast) - if err != nil { - logger.Error("error converting json sql to ast", "error", err.Error(), "ret", ret) - return nil, fmt.Errorf("error converting json to ast: %s", err.Error()) - } + tables := make(map[string]struct{}) - return tablesFromAST(ast) -} - -// tablesFromAST returns a list of tables from the ast -func tablesFromAST(ast []map[string]any) ([]string, error) { - flat, err := flatten.Flatten(ast[0], "", flatten.DotStyle) - if err != nil { - logger.Error("error flattening ast", "error", err.Error(), "ast", ast) - return nil, fmt.Errorf("error flattening ast: %s", err.Error()) - } - - tables := []string{} - for k, v := range flat { - if strings.HasSuffix(k, ERROR) { - v, ok := v.(bool) - if ok && v { - logger.Error("error in sql", "error", k) - return nil, astError(k, flat) + walkSubtree := func(node sqlparser.SQLNode) error { + err = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch v := node.(type) { + case *sqlparser.AliasedTableExpr: + if tableName, ok := v.Expr.(sqlparser.TableName); ok { + tables[tableName.Name.String()] = struct{}{} + } + case *sqlparser.TableName: + tables[v.Name.String()] = struct{}{} } + return true, nil + }, node) + + if err != nil { + logger.Error("error walking sql", "error", err, "node", node) + return fmt.Errorf("failed to parse SQL expression: %w", err) } - if strings.Contains(k, TABLE_NAME) { - table, ok := v.(string) - if ok && !existsInList(table, tables) { - tables = append(tables, v.(string)) - } + return nil + } + + if err := walkSubtree(stmt); err != nil { + return nil, err + } + + result := make([]string, 0, len(tables)) + for table := range tables { + // Remove 'dual' table if it exists + // This is a special table in MySQL that always returns a single row with a single column + // See: https://dev.mysql.com/doc/refman/5.7/en/select.html#:~:text=You%20are%20permitted%20to%20specify%20DUAL%20as%20a%20dummy%20table%20name%20in%20situations%20where%20no%20tables%20are%20referenced + if table != "dual" { + result = append(result, table) } } - sort.Strings(tables) + + sort.Strings(result) logger.Debug("tables found in sql", "tables", tables) - return tables, nil -} - -func astError(k string, flat map[string]any) error { - key := strings.Replace(k, ERROR, "", 1) - message, ok := flat[key+ERROR_MESSAGE] - if !ok { - message = "unknown error in sql" - } - return fmt.Errorf("error in sql: %s", message) -} - -func existsInList(table string, list []string) bool { - for _, t := range list { - if t == table { - return true - } - } - return false + return result, nil } diff --git a/pkg/expr/sql/parser_allow.go b/pkg/expr/sql/parser_allow.go new file mode 100644 index 00000000000..8a2ad436920 --- /dev/null +++ b/pkg/expr/sql/parser_allow.go @@ -0,0 +1,136 @@ +package sql + +import ( + "fmt" + "strings" + + "github.com/dolthub/vitess/go/vt/sqlparser" +) + +// AllowQuery parses the query and checks it against an allow list of allowed SQL nodes +// and functions. +func AllowQuery(rawSQL string) (bool, error) { + s, err := sqlparser.Parse(rawSQL) + if err != nil { + return false, fmt.Errorf("error parsing sql: %s", err.Error()) + } + + walkSubtree := func(node sqlparser.SQLNode) error { + err := sqlparser.Walk(func(node sqlparser.SQLNode) (bool, error) { + if !allowedNode(node) { + if fT, ok := node.(*sqlparser.FuncExpr); ok { + return false, fmt.Errorf("blocked function %s - not supported in queries", fT.Name) + } + return false, fmt.Errorf("blocked node %T - not supported in queries", node) + } + return true, nil + }, node) + + if err != nil { + return fmt.Errorf("failed to parse SQL expression: %w", err) + } + + return nil + } + + if err := walkSubtree(s); err != nil { + return false, err + } + + return true, nil +} + +// nolint:gocyclo,nakedret +func allowedNode(node sqlparser.SQLNode) (b bool) { + b = true // so don't have to return true in every case but default + + switch v := node.(type) { + case *sqlparser.FuncExpr: + return allowedFunction(v) + + case *sqlparser.AsOf: + return + + case *sqlparser.AliasedExpr, *sqlparser.AliasedTableExpr: + return + + case *sqlparser.BinaryExpr: + return + + case sqlparser.ColIdent, *sqlparser.ColName, sqlparser.Columns: + return + + case sqlparser.Comments: // TODO: understand why some are pointer vs not + return + + case *sqlparser.CommonTableExpr: + return + + case *sqlparser.ComparisonExpr: + return + + case *sqlparser.ConvertExpr: + return + + case sqlparser.GroupBy: + return + + case *sqlparser.IndexHints: + return + + case *sqlparser.Into: + return + + case *sqlparser.JoinTableExpr, sqlparser.JoinCondition: + return + + case *sqlparser.Select, sqlparser.SelectExprs: + return + + case *sqlparser.StarExpr: + return + + case *sqlparser.SQLVal: + return + + case *sqlparser.Limit: + return + + case *sqlparser.Order, sqlparser.OrderBy: + return + + case *sqlparser.Over: + return + + case *sqlparser.Subquery: + return + + case sqlparser.TableName, sqlparser.TableExprs, sqlparser.TableIdent: + return + + case *sqlparser.With: + return + + case *sqlparser.Where: + return + + default: + return false + } +} + +// nolint:gocyclo,nakedret +func allowedFunction(f *sqlparser.FuncExpr) (b bool) { + b = true // so don't have to return true in every case but default + + switch strings.ToLower(f.Name.String()) { + case "sum", "avg", "count", "min", "max": + return + + case "coalesce": + return + + default: + return false + } +} diff --git a/pkg/expr/sql/parser_allow_test.go b/pkg/expr/sql/parser_allow_test.go new file mode 100644 index 00000000000..aeeb1a40c69 --- /dev/null +++ b/pkg/expr/sql/parser_allow_test.go @@ -0,0 +1,80 @@ +package sql + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAllowQuery(t *testing.T) { + testCases := []struct { + name string + q string + err error + }{ + { + name: "a big catch all for now", + q: example_metrics_query, + err: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := AllowQuery(tc.q) + if tc.err != nil { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +var example_metrics_query = `WITH + metrics_this_month AS ( + SELECT + Month, + namespace, + sum(BillableSeries) AS billable_series + FROM metrics + WHERE + Month = "2024-11" + GROUP BY + Month, + namespace + ORDER BY billable_series DESC + ), + total_metrics AS ( + SELECT SUM(billable_series) AS metrics_billable_series_total + FROM metrics_this_month + ), + total_traces AS ( + -- "usage" is a reserved keyword in MySQL. Quote it with backticks. + SELECT SUM(value) AS traces_usage_total + FROM traces + ), + usage_by_team AS ( + SELECT + COALESCE(teams.team, 'unaccounted') AS team, + 1 + 0 AS team_count, + -- Metrics + SUM(COALESCE(metrics_this_month.billable_series, 0)) AS metrics_billable_series, + -- Traces + SUM(COALESCE(traces.value, 0)) AS traces_usage + -- FROM teams + -- FULL OUTER JOIN metrics_this_month + FROM metrics_this_month + FULL OUTER JOIN teams + ON teams.namespace = metrics_this_month.namespace + FULL OUTER JOIN traces + ON teams.namespace = traces.namespace + GROUP BY + -- COALESCE(teams.team, 'unaccounted') + teams.team + ORDER BY metrics_billable_series DESC + ) + +SELECT * +FROM usage_by_team +CROSS JOIN total_metrics +CROSS JOIN total_traces` diff --git a/pkg/expr/sql/parser_test.go b/pkg/expr/sql/parser_test.go index 24303ce0178..17235684388 100644 --- a/pkg/expr/sql/parser_test.go +++ b/pkg/expr/sql/parser_test.go @@ -3,214 +3,131 @@ package sql import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestParse(t *testing.T) { - t.Skip() - sql := "select * from foo" - tables, err := TablesList((sql)) - assert.Nil(t, err) +func TestTablesList(t *testing.T) { + tests := []struct { + name string + sql string + expected []string + expectError bool + }{ + { + name: "simple select", + sql: "select * from foo", + expected: []string{"foo"}, + }, + { + name: "select with comma", + sql: "select * from foo,bar", + expected: []string{"bar", "foo"}, + }, + { + name: "select with multiple commas", + sql: "select * from foo,bar,baz", + expected: []string{"bar", "baz", "foo"}, + }, + { + name: "no table", + sql: "select 1 as 'n'", + expected: []string{}, + }, + { + name: "json array", + sql: "SELECT JSON_ARRAY(1, 2, 3) AS array_value", + expected: []string{}, + }, + { + name: "json extract", + sql: "SELECT JSON_EXTRACT(JSON_ARRAY(1, 2, 3), '$[0]') AS first_element;", + expected: []string{}, + }, + { + name: "json int array", + sql: "SELECT JSON_ARRAY(3, 2, 1) AS int_array;", + expected: []string{}, + }, + { + name: "subquery", + sql: "select * from (select * from people limit 1) AS subquery", + expected: []string{"people"}, + }, + { + name: "join", + sql: `select * from A + JOIN B ON A.name = B.name + LIMIT 10`, + expected: []string{"A", "B"}, + }, + { + name: "right join", + sql: `select * from A + RIGHT JOIN B ON A.name = B.name + LIMIT 10`, + expected: []string{"A", "B"}, + }, + { + name: "alias with join", + sql: `select * from A as X + RIGHT JOIN B ON A.name = X.name + LIMIT 10`, + expected: []string{"A", "B"}, + }, + { + name: "alias", + sql: "select * from A as X LIMIT 10", + expected: []string{"A"}, + }, + { + name: "error case", + sql: "select * from zzz aaa zzz", + expectError: true, + }, + { + name: "parens", + sql: `SELECT t1.Col1, + t2.Col1, + t3.Col1 + FROM table1 AS t1 + LEFT JOIN ( + table2 AS t2 + INNER JOIN table3 AS t3 ON t3.Col1 = t2.Col1 + ) ON t2.Col1 = t1.Col1;`, + expected: []string{"table1", "table2", "table3"}, + }, + { + name: "with clause", + sql: `WITH top_products AS ( + SELECT * FROM products + ORDER BY price DESC + LIMIT 5 + ) + SELECT name, price + FROM top_products;`, + expected: []string{"products", "top_products"}, + }, + { + name: "with quote", + sql: "select *,'junk' from foo", + expected: []string{"foo"}, + }, + { + name: "with quote 2", + sql: "SELECT json_serialize_sql('SELECT 1')", + expected: []string{}, + }, + } - assert.Equal(t, "foo", tables[0]) -} - -func TestParseWithComma(t *testing.T) { - t.Skip() - sql := "select * from foo,bar" - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, "bar", tables[0]) - assert.Equal(t, "foo", tables[1]) -} - -func TestParseWithCommas(t *testing.T) { - t.Skip() - sql := "select * from foo,bar,baz" - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, "bar", tables[0]) - assert.Equal(t, "baz", tables[1]) - assert.Equal(t, "foo", tables[2]) -} - -func TestArray(t *testing.T) { - t.Skip() - sql := "SELECT array_value(1, 2, 3)" - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, 0, len(tables)) -} - -func TestArray2(t *testing.T) { - t.Skip() - sql := "SELECT array_value(1, 2, 3)[2]" - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, 0, len(tables)) -} - -func TestXxx(t *testing.T) { - t.Skip() - sql := "SELECT [3, 2, 1]::INT[3];" - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, 0, len(tables)) -} - -func TestParseSubquery(t *testing.T) { - t.Skip() - sql := "select * from (select * from people limit 1)" - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, 1, len(tables)) - assert.Equal(t, "people", tables[0]) -} - -func TestJoin(t *testing.T) { - t.Skip() - sql := `select * from A - JOIN B ON A.name = B.name - LIMIT 10` - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, 2, len(tables)) - assert.Equal(t, "A", tables[0]) - assert.Equal(t, "B", tables[1]) -} - -func TestRightJoin(t *testing.T) { - t.Skip() - sql := `select * from A - RIGHT JOIN B ON A.name = B.name - LIMIT 10` - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, 2, len(tables)) - assert.Equal(t, "A", tables[0]) - assert.Equal(t, "B", tables[1]) -} - -func TestAliasWithJoin(t *testing.T) { - t.Skip() - sql := `select * from A as X - RIGHT JOIN B ON A.name = X.name - LIMIT 10` - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, 2, len(tables)) - assert.Equal(t, "A", tables[0]) - assert.Equal(t, "B", tables[1]) -} - -func TestAlias(t *testing.T) { - t.Skip() - sql := `select * from A as X LIMIT 10` - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, 1, len(tables)) - assert.Equal(t, "A", tables[0]) -} - -func TestError(t *testing.T) { - t.Skip() - sql := `select * from zzz aaa zzz` - _, err := TablesList((sql)) - assert.NotNil(t, err) -} - -func TestParens(t *testing.T) { - t.Skip() - sql := `SELECT t1.Col1, - t2.Col1, - t3.Col1 - FROM table1 AS t1 - LEFT JOIN ( - table2 AS t2 - INNER JOIN table3 AS t3 ON t3.Col1 = t2.Col1 - ) ON t2.Col1 = t1.Col1;` - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, 3, len(tables)) - assert.Equal(t, "table1", tables[0]) - assert.Equal(t, "table2", tables[1]) - assert.Equal(t, "table3", tables[2]) -} - -func TestWith(t *testing.T) { - t.Skip() - sql := `WITH - - current_month AS ( - select - distinct "Month(ISO)" as mth - from A - ORDER BY mth DESC - LIMIT 1 - ), - - last_month_bill AS ( - select - CAST ( - sum( - CAST(BillableSeries AS INTEGER) - ) AS INTEGER - ) AS BillableSeries, - "Month(ISO)", - label_namespace - -- , B.activeseries_count - from A - JOIN current_month - ON current_month.mth = A."Month(ISO)" - JOIN B - ON B.namespace = A.label_namespace - GROUP BY - label_namespace, - "Month(ISO)" - ORDER BY BillableSeries DESC - ) - - SELECT - last_month_bill.*, - BEE.activeseries_count - FROM last_month_bill - JOIN BEE - ON BEE.namespace = last_month_bill.label_namespace` - - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, 5, len(tables)) - assert.Equal(t, "A", tables[0]) - assert.Equal(t, "B", tables[1]) - assert.Equal(t, "BEE", tables[2]) -} - -func TestWithQuote(t *testing.T) { - t.Skip() - sql := "select *,'junk' from foo" - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, "foo", tables[0]) -} - -func TestWithQuote2(t *testing.T) { - t.Skip() - sql := "SELECT json_serialize_sql('SELECT 1')" - tables, err := TablesList((sql)) - assert.Nil(t, err) - - assert.Equal(t, 0, len(tables)) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tables, err := TablesList(tc.sql) + if tc.expectError { + require.NotNil(t, err, "expected error for SQL: %s", tc.sql) + } else { + require.Nil(t, err, "unexpected error for SQL: %s", tc.sql) + require.Equal(t, tc.expected, tables, "mismatched tables for SQL: %s", tc.sql) + } + }) + } } diff --git a/pkg/expr/sql_command.go b/pkg/expr/sql_command.go index 9f0e1faf3e8..60d23f6e82f 100644 --- a/pkg/expr/sql_command.go +++ b/pkg/expr/sql_command.go @@ -93,11 +93,10 @@ func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.V rsp := mathexp.Results{} - db := sql.NewInMemoryDB() - var frame = &data.Frame{} + db := sql.DB{} logger.Debug("Executing query", "query", gr.query, "frames", len(allFrames)) - err := db.QueryFramesInto(gr.refID, gr.query, allFrames, frame) + frame, err := db.QueryFrames(ctx, gr.refID, gr.query, allFrames) if err != nil { logger.Error("Failed to query frames", "error", err.Error()) rsp.Error = err @@ -105,12 +104,11 @@ func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.V } logger.Debug("Done Executing query", "query", gr.query, "rows", frame.Rows()) - frame.RefID = gr.refID - if frame.Rows() == 0 { rsp.Values = mathexp.Values{ mathexp.NoData{Frame: frame}, } + return rsp, nil } rsp.Values = mathexp.Values{ diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index f86bf0fc75f..95a27508b5a 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -368,8 +368,7 @@ func (b *QueryAPIBuilder) handleExpressions(ctx context.Context, req parsedReque if !ok { dr, ok := qdr.Responses[refId] if ok { - allowLongFrames := false // TODO -- depends on input type and only if SQL? - _, res, err := b.converter.Convert(ctx, req.RefIDTypes[refId], dr.Frames, allowLongFrames) + _, res, err := b.converter.Convert(ctx, req.RefIDTypes[refId], dr.Frames) if err != nil { expressionsLogger.Error("error converting frames for expressions", "error", err) res.Error = err @@ -409,13 +408,12 @@ func (b *QueryAPIBuilder) convertQueryWithoutExpression(ctx context.Context, req if qdr == nil { return nil, errors.New("queryDataResponse is nil") } - allowLongFrames := false refID := req.Request.Queries[0].RefID if _, exist := qdr.Responses[refID]; !exist { return nil, fmt.Errorf("refID '%s' does not exist", refID) } frames := qdr.Responses[refID].Frames - _, results, err := b.converter.Convert(ctx, req.PluginId, frames, allowLongFrames) + _, results, err := b.converter.Convert(ctx, req.PluginId, frames) if err != nil { results.Error = err } diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 85bb0739e32..13520e2a02b 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -29,4 +29,5 @@ const ( grafanaDatabasesFrontend codeowner = "@grafana/databases-frontend" grafanaOSSBigTent codeowner = "@grafana/oss-big-tent" growthAndOnboarding codeowner = "@grafana/growth-and-onboarding" + grafanaDatasourcesCoreServicesSquad codeowner = "@grafana/grafana-datasources-core-services" ) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index bf5496b9c3c..5589a2cdb6b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1054,7 +1054,7 @@ var ( Description: "Enables using SQL and DuckDB functions as Expressions.", Stage: FeatureStageExperimental, FrontendOnly: false, - Owner: grafanaAppPlatformSquad, + Owner: grafanaDatasourcesCoreServicesSquad, }, { Name: "nodeGraphDotLayout", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 66125da1954..5ee9c0c2f1f 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -138,7 +138,7 @@ alertingSaveStateCompressed,experimental,@grafana/alerting-squad,false,false,fal scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false promQLScope,GA,@grafana/oss-big-tent,false,false,false logQLScope,privatePreview,@grafana/observability-logs,false,false,false -sqlExpressions,experimental,@grafana/grafana-app-platform-squad,false,false,false +sqlExpressions,experimental,@grafana/grafana-datasources-core-services,false,false,false nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,false,false,true groupToNestedTableTransformation,GA,@grafana/dataviz-squad,false,false,true newPDFRendering,GA,@grafana/sharing-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index bc2a152baaf..970b8edf66f 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3658,13 +3658,16 @@ { "metadata": { "name": "sqlExpressions", - "resourceVersion": "1718727528075", - "creationTimestamp": "2024-02-27T21:16:00Z" + "resourceVersion": "1738589190784", + "creationTimestamp": "2024-02-27T21:16:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-02-03 13:26:30.784245615 +0000 UTC" + } }, "spec": { "description": "Enables using SQL and DuckDB functions as Expressions.", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad" + "codeowner": "@grafana/grafana-datasources-core-services" } }, { diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 7e3edf8535b..2ac8214df01 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -117,6 +117,11 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/dlmiddlecote/sqlstats v1.0.2 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect + github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 // indirect + github.com/dolthub/go-mysql-server v0.19.0 // indirect + github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect + github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elazarl/goproxy v1.3.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect @@ -205,7 +210,6 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jeremywohl/flatten v1.0.1 // indirect github.com/jessevdk/go-flags v1.5.0 // indirect github.com/jhump/protoreflect v1.15.1 // indirect github.com/jmespath-community/go-jmespath v1.1.1 // indirect @@ -220,6 +224,7 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/lestrrat-go/strftime v1.0.4 // indirect github.com/lib/pq v1.10.9 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -286,6 +291,7 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect @@ -296,6 +302,7 @@ require ( github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tetratelabs/wazero v1.8.2 // indirect github.com/tjhop/slog-gokit v0.1.3 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect @@ -352,6 +359,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/mail.v2 v2.3.1 // indirect + gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.1 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index e74589ac323..3c8ae34e004 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -312,6 +312,16 @@ github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6 github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww= +github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= +github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM= +github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA= +github.com/dolthub/go-mysql-server v0.19.0 h1:NdcXyGt9v7m4sQOahU+ss++iyPy4Q3viuVvbnn3rUTQ= +github.com/dolthub/go-mysql-server v0.19.0/go.mod h1:elfIatfq2fkU5lqTBrTcpL0RcHZOgYPE8EzBD7yQFiY= +github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= +github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= +github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 h1:nzBnC0Rt1gFtscJEz4veYd/mazZEdbdmed+tujdaKOo= +github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= 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/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -654,8 +664,6 @@ github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs= -github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= @@ -725,6 +733,10 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8= +github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -1004,6 +1016,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= +github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= +github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o= github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs= @@ -1306,6 +1320,7 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1536,6 +1551,8 @@ gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc= +gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 172975106c5..17017755d74 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -210,6 +210,16 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww= +github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= +github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM= +github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA= +github.com/dolthub/go-mysql-server v0.19.0 h1:NdcXyGt9v7m4sQOahU+ss++iyPy4Q3viuVvbnn3rUTQ= +github.com/dolthub/go-mysql-server v0.19.0/go.mod h1:elfIatfq2fkU5lqTBrTcpL0RcHZOgYPE8EzBD7yQFiY= +github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= +github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= +github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 h1:nzBnC0Rt1gFtscJEz4veYd/mazZEdbdmed+tujdaKOo= +github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= 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/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -490,8 +500,6 @@ github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs= -github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= @@ -553,6 +561,8 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8= +github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -747,6 +757,8 @@ github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= @@ -788,6 +800,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= +github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= +github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o= github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs= @@ -1103,6 +1117,8 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= +gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc= +gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 52db2070a065d370f0543d1e5af416859af9912e Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 6 Feb 2025 13:35:40 +0100 Subject: [PATCH 384/894] Advisor: Expose check types as a resource (#100058) --- apps/advisor/kinds/check.cue | 4 + apps/advisor/kinds/checktype.cue | 26 ++ apps/advisor/kinds/manifest.cue | 1 + .../apis/advisor/v0alpha1/check_status_gen.go | 4 + .../advisor/v0alpha1/checktype_codec_gen.go | 28 ++ .../v0alpha1/checktype_metadata_gen.go | 28 ++ .../advisor/v0alpha1/checktype_object_gen.go | 266 ++++++++++++++++ .../advisor/v0alpha1/checktype_schema_gen.go | 34 ++ .../advisor/v0alpha1/checktype_spec_gen.go | 26 ++ .../advisor/v0alpha1/checktype_status_gen.go | 44 +++ .../apis/advisor/v0alpha1/zz_openapi_gen.go | 297 +++++++++++++++++- apps/advisor/pkg/apis/advisor_manifest.go | 21 +- apps/advisor/pkg/app/app.go | 12 + .../pkg/app/checks/datasourcecheck/check.go | 24 +- .../pkg/app/checks/plugincheck/check.go | 24 +- apps/advisor/pkg/app/checks/utils.go | 21 ++ .../checktyperegisterer.go | 82 +++++ .../checktyperegisterer_test.go | 169 ++++++++++ 18 files changed, 1080 insertions(+), 31 deletions(-) create mode 100644 apps/advisor/kinds/checktype.cue create mode 100644 apps/advisor/pkg/apis/advisor/v0alpha1/checktype_codec_gen.go create mode 100644 apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go create mode 100644 apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go create mode 100644 apps/advisor/pkg/apis/advisor/v0alpha1/checktype_schema_gen.go create mode 100644 apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go create mode 100644 apps/advisor/pkg/apis/advisor/v0alpha1/checktype_status_gen.go create mode 100644 apps/advisor/pkg/app/checks/utils.go create mode 100644 apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go create mode 100644 apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer_test.go diff --git a/apps/advisor/kinds/check.cue b/apps/advisor/kinds/check.cue index fdf641abdf3..a564f888e2f 100644 --- a/apps/advisor/kinds/check.cue +++ b/apps/advisor/kinds/check.cue @@ -28,6 +28,10 @@ check: { reason: string // Action to take to resolve the error action: string + // Step ID that the error is associated with + stepID: string + // Item ID that the error is associated with + itemID: string } #Report: { // Number of elements analyzed diff --git a/apps/advisor/kinds/checktype.cue b/apps/advisor/kinds/checktype.cue new file mode 100644 index 00000000000..6a4bfa4a0df --- /dev/null +++ b/apps/advisor/kinds/checktype.cue @@ -0,0 +1,26 @@ +package advisor + +checktype: { + kind: "CheckType" + pluralName: "CheckTypes" + current: "v0alpha1" + versions: { + "v0alpha1": { + codegen: { + frontend: false + backend: true + } + schema: { + #Step: { + title: string + description: string + stepID: string + } + spec: { + name: string + steps: [...#Step] + } + } + } + } +} diff --git a/apps/advisor/kinds/manifest.cue b/apps/advisor/kinds/manifest.cue index 6f96066967d..75a0eee05ef 100644 --- a/apps/advisor/kinds/manifest.cue +++ b/apps/advisor/kinds/manifest.cue @@ -5,5 +5,6 @@ manifest: { groupOverride: "advisor.grafana.app" kinds: [ check, + checktype, ] } diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go index ac73ee86ec4..edc67575fe8 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go @@ -10,6 +10,10 @@ type CheckReportError struct { Reason string `json:"reason"` // Action to take to resolve the error Action string `json:"action"` + // Step ID that the error is associated with + StepID string `json:"stepID"` + // Item ID that the error is associated with + ItemID string `json:"itemID"` } // NewCheckReportError creates a new CheckReportError object. diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_codec_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_codec_gen.go new file mode 100644 index 00000000000..9881ce9d96f --- /dev/null +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// CheckTypeJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type CheckTypeJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*CheckTypeJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*CheckTypeJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &CheckTypeJSONCodec{} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go new file mode 100644 index 00000000000..9998cffb37d --- /dev/null +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go @@ -0,0 +1,28 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type CheckTypeMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewCheckTypeMetadata creates a new CheckTypeMetadata object. +func NewCheckTypeMetadata() *CheckTypeMetadata { + return &CheckTypeMetadata{} +} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go new file mode 100644 index 00000000000..80bd02b1cc8 --- /dev/null +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go @@ -0,0 +1,266 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type CheckType struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + Spec CheckTypeSpec `json:"spec" yaml:"spec"` + CheckTypeStatus CheckTypeStatus `json:"status" yaml:"status"` +} + +func (o *CheckType) GetSpec() any { + return o.Spec +} + +func (o *CheckType) SetSpec(spec any) error { + cast, ok := spec.(CheckTypeSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *CheckType) GetSubresources() map[string]any { + return map[string]any{ + "status": o.CheckTypeStatus, + } +} + +func (o *CheckType) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.CheckTypeStatus, true + default: + return nil, false + } +} + +func (o *CheckType) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(CheckTypeStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type CheckTypeStatus", value) + } + o.CheckTypeStatus = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *CheckType) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *CheckType) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *CheckType) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *CheckType) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *CheckType) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *CheckType) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *CheckType) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *CheckType) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *CheckType) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *CheckType) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *CheckType) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *CheckType) DeepCopyObject() runtime.Object { + return o.Copy() +} + +// Interface compliance compile-time check +var _ resource.Object = &CheckType{} + +// +k8s:openapi-gen=true +type CheckTypeList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []CheckType `json:"items" yaml:"items"` +} + +func (o *CheckTypeList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *CheckTypeList) Copy() resource.ListObject { + cpy := &CheckTypeList{ + TypeMeta: o.TypeMeta, + Items: make([]CheckType, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*CheckType); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *CheckTypeList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *CheckTypeList) SetItems(items []resource.Object) { + o.Items = make([]CheckType, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*CheckType) + } +} + +// Interface compliance compile-time check +var _ resource.ListObject = &CheckTypeList{} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_schema_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_schema_gen.go new file mode 100644 index 00000000000..15b4a6fbb11 --- /dev/null +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaCheckType = resource.NewSimpleSchema("advisor.grafana.app", "v0alpha1", &CheckType{}, &CheckTypeList{}, resource.WithKind("CheckType"), + resource.WithPlural("checktypes"), resource.WithScope(resource.NamespacedScope)) + kindCheckType = resource.Kind{ + Schema: schemaCheckType, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &CheckTypeJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func CheckTypeKind() resource.Kind { + return kindCheckType +} + +// Schema returns a resource.SimpleSchema representation of CheckType +func CheckTypeSchema() *resource.SimpleSchema { + return schemaCheckType +} + +// Interface compliance checks +var _ resource.Schema = kindCheckType diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go new file mode 100644 index 00000000000..a0392a49c18 --- /dev/null +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go @@ -0,0 +1,26 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type CheckTypeStep struct { + Title string `json:"title"` + Description string `json:"description"` + StepID string `json:"stepID"` +} + +// NewCheckTypeStep creates a new CheckTypeStep object. +func NewCheckTypeStep() *CheckTypeStep { + return &CheckTypeStep{} +} + +// +k8s:openapi-gen=true +type CheckTypeSpec struct { + Name string `json:"name"` + Steps []CheckTypeStep `json:"steps"` +} + +// NewCheckTypeSpec creates a new CheckTypeSpec object. +func NewCheckTypeSpec() *CheckTypeSpec { + return &CheckTypeSpec{} +} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_status_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_status_gen.go new file mode 100644 index 00000000000..87db65316d0 --- /dev/null +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type CheckTypestatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State CheckTypeStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewCheckTypestatusOperatorState creates a new CheckTypestatusOperatorState object. +func NewCheckTypestatusOperatorState() *CheckTypestatusOperatorState { + return &CheckTypestatusOperatorState{} +} + +// +k8s:openapi-gen=true +type CheckTypeStatus struct { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]CheckTypestatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewCheckTypeStatus creates a new CheckTypeStatus object. +func NewCheckTypeStatus() *CheckTypeStatus { + return &CheckTypeStatus{} +} + +// +k8s:openapi-gen=true +type CheckTypeStatusOperatorStateState string + +const ( + CheckTypeStatusOperatorStateStateSuccess CheckTypeStatusOperatorStateState = "success" + CheckTypeStatusOperatorStateStateInProgress CheckTypeStatusOperatorStateState = "in_progress" + CheckTypeStatusOperatorStateStateFailed CheckTypeStatusOperatorStateState = "failed" +) diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go index 74ccb4861e6..10a2de58e84 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go @@ -12,13 +12,19 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.Check": schema_pkg_apis_advisor_v0alpha1_Check(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckList": schema_pkg_apis_advisor_v0alpha1_CheckList(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportError": schema_pkg_apis_advisor_v0alpha1_CheckReportError(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec": schema_pkg_apis_advisor_v0alpha1_CheckSpec(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckStatus": schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport": schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState": schema_pkg_apis_advisor_v0alpha1_CheckstatusOperatorState(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.Check": schema_pkg_apis_advisor_v0alpha1_Check(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckList": schema_pkg_apis_advisor_v0alpha1_CheckList(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportError": schema_pkg_apis_advisor_v0alpha1_CheckReportError(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec": schema_pkg_apis_advisor_v0alpha1_CheckSpec(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckStatus": schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckType": schema_pkg_apis_advisor_v0alpha1_CheckType(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeList": schema_pkg_apis_advisor_v0alpha1_CheckTypeList(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeSpec": schema_pkg_apis_advisor_v0alpha1_CheckTypeSpec(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStatus": schema_pkg_apis_advisor_v0alpha1_CheckTypeStatus(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStep": schema_pkg_apis_advisor_v0alpha1_CheckTypeStep(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypestatusOperatorState": schema_pkg_apis_advisor_v0alpha1_CheckTypestatusOperatorState(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport": schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState": schema_pkg_apis_advisor_v0alpha1_CheckstatusOperatorState(ref), } } @@ -147,8 +153,24 @@ func schema_pkg_apis_advisor_v0alpha1_CheckReportError(ref common.ReferenceCallb Format: "", }, }, + "stepID": { + SchemaProps: spec.SchemaProps{ + Description: "Step ID that the error is associated with", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "itemID": { + SchemaProps: spec.SchemaProps{ + Description: "Item ID that the error is associated with", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, }, - Required: []string{"severity", "reason", "action"}, + Required: []string{"severity", "reason", "action", "stepID", "itemID"}, }, }, } @@ -233,6 +255,265 @@ func schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref common.ReferenceCallback) } } +func schema_pkg_apis_advisor_v0alpha1_CheckType(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeSpec", "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_advisor_v0alpha1_CheckTypeList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckType"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckType", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_advisor_v0alpha1_CheckTypeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "steps": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStep"), + }, + }, + }, + }, + }, + }, + Required: []string{"name", "steps"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStep"}, + } +} + +func schema_pkg_apis_advisor_v0alpha1_CheckTypeStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "operatorStates": { + SchemaProps: spec.SchemaProps{ + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypestatusOperatorState"), + }, + }, + }, + }, + }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypestatusOperatorState"}, + } +} + +func schema_pkg_apis_advisor_v0alpha1_CheckTypeStep(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "stepID": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"title", "description", "stepID"}, + }, + }, + } +} + +func schema_pkg_apis_advisor_v0alpha1_CheckTypestatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} + func schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/apps/advisor/pkg/apis/advisor_manifest.go b/apps/advisor/pkg/apis/advisor_manifest.go index f62ea3addb8..8b39d626436 100644 --- a/apps/advisor/pkg/apis/advisor_manifest.go +++ b/apps/advisor/pkg/apis/advisor_manifest.go @@ -12,9 +12,12 @@ import ( ) var ( - rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"errors":{"description":"List of errors","items":{"properties":{"action":{"description":"Action to take to resolve the error","type":"string"},"reason":{"description":"Human readable reason for the error","type":"string"},"severity":{"description":"Severity of the error","enum":["high","low"],"type":"string"}},"required":["severity","reason","action"],"type":"object"},"type":"array"}},"required":["count","errors"],"type":"object"}},"required":["report"],"type":"object","x-kubernetes-preserve-unknown-fields":true}}`) - versionSchemaCheckv0alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaCheckv0alpha1, &versionSchemaCheckv0alpha1) + rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"errors":{"description":"List of errors","items":{"properties":{"action":{"description":"Action to take to resolve the error","type":"string"},"itemID":{"description":"Item ID that the error is associated with","type":"string"},"reason":{"description":"Human readable reason for the error","type":"string"},"severity":{"description":"Severity of the error","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the error is associated with","type":"string"}},"required":["severity","reason","action","stepID","itemID"],"type":"object"},"type":"array"}},"required":["count","errors"],"type":"object"}},"required":["report"],"type":"object","x-kubernetes-preserve-unknown-fields":true}}`) + versionSchemaCheckv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaCheckv0alpha1, &versionSchemaCheckv0alpha1) + rawSchemaCheckTypev0alpha1 = []byte(`{"spec":{"properties":{"name":{"type":"string"},"steps":{"items":{"properties":{"description":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID"],"type":"object"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object","x-kubernetes-preserve-unknown-fields":true}}`) + versionSchemaCheckTypev0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaCheckTypev0alpha1, &versionSchemaCheckTypev0alpha1) ) var appManifestData = app.ManifestData{ @@ -40,6 +43,18 @@ var appManifestData = app.ManifestData{ }, }, }, + + { + Kind: "CheckType", + Scope: "Namespaced", + Conversion: false, + Versions: []app.ManifestKindVersion{ + { + Name: "v0alpha1", + Schema: &versionSchemaCheckTypev0alpha1, + }, + }, + }, }, } diff --git a/apps/advisor/pkg/app/app.go b/apps/advisor/pkg/app/app.go index 554357caca2..7982661c3b9 100644 --- a/apps/advisor/pkg/app/app.go +++ b/apps/advisor/pkg/app/app.go @@ -11,6 +11,7 @@ import ( advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/apps/advisor/pkg/app/checktyperegisterer" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" ) @@ -70,6 +71,9 @@ func New(cfg app.Config) (app.App, error) { }, }, }, + { + Kind: advisorv0alpha1.CheckTypeKind(), + }, }, } @@ -83,6 +87,13 @@ func New(cfg app.Config) (app.App, error) { return nil, err } + // Save check types as resources + ctr, err := checktyperegisterer.New(cfg) + if err != nil { + return nil, err + } + a.AddRunnable(ctr) + return a, nil } @@ -95,6 +106,7 @@ func GetKinds() map[schema.GroupVersion][]resource.Kind { return map[schema.GroupVersion][]resource.Kind{ gv: { advisorv0alpha1.CheckKind(), + advisorv0alpha1.CheckTypeKind(), }, } } diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go index f7dbcc53024..88e10a72719 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -86,11 +86,13 @@ func (s *uidValidationStep) Run(ctx context.Context, obj *advisor.CheckSpec, ite // Data source UID validation err := util.ValidateUID(ds.UID) if err != nil { - dsErrs = append(dsErrs, advisor.CheckReportError{ - Severity: advisor.CheckReportErrorSeverityLow, - Reason: fmt.Sprintf("Invalid UID '%s' for data source %s", ds.UID, ds.Name), - Action: "Check the documentation for more information.", - }) + dsErrs = append(dsErrs, checks.NewCheckReportError( + advisor.CheckReportErrorSeverityLow, + fmt.Sprintf("Invalid UID '%s' for data source %s", ds.UID, ds.Name), + "Check the documentation for more information.", + s.ID(), + ds.UID, + )) } } return dsErrs, nil @@ -141,13 +143,15 @@ func (s *healthCheckStep) Run(ctx context.Context, obj *advisor.CheckSpec, items continue } if resp.Status != backend.HealthStatusOk { - dsErrs = append(dsErrs, advisor.CheckReportError{ - Severity: advisor.CheckReportErrorSeverityHigh, - Reason: fmt.Sprintf("Health check failed for %s", ds.Name), - Action: fmt.Sprintf( + dsErrs = append(dsErrs, checks.NewCheckReportError( + advisor.CheckReportErrorSeverityHigh, + fmt.Sprintf("Health check failed for %s", ds.Name), + fmt.Sprintf( "Go to the data source configuration"+ " and address the issues reported.", ds.UID), - }) + s.ID(), + ds.UID, + )) } } return dsErrs, nil diff --git a/apps/advisor/pkg/app/checks/plugincheck/check.go b/apps/advisor/pkg/app/checks/plugincheck/check.go index c64bc1e7579..c024b2608b0 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check.go @@ -97,11 +97,13 @@ func (s *deprecationStep) Run(ctx context.Context, _ *advisor.CheckSpec, items [ continue } if i.Status == "deprecated" { - errs = append(errs, advisor.CheckReportError{ - Severity: advisor.CheckReportErrorSeverityHigh, - Reason: fmt.Sprintf("Plugin deprecated: %s", p.ID), - Action: "Check the documentation for recommended steps.", - }) + errs = append(errs, checks.NewCheckReportError( + advisor.CheckReportErrorSeverityHigh, + fmt.Sprintf("Plugin deprecated: %s", p.ID), + "Check the documentation for recommended steps.", + s.ID(), + p.ID, + )) } } return errs, nil @@ -150,13 +152,15 @@ func (s *updateStep) Run(ctx context.Context, _ *advisor.CheckSpec, items []any) continue } if hasUpdate(p, info) { - errs = append(errs, advisor.CheckReportError{ - Severity: advisor.CheckReportErrorSeverityLow, - Reason: fmt.Sprintf("New version available for %s", p.ID), - Action: fmt.Sprintf( + errs = append(errs, checks.NewCheckReportError( + advisor.CheckReportErrorSeverityLow, + fmt.Sprintf("New version available for %s", p.ID), + fmt.Sprintf( "Go to the plugin admin page"+ " and upgrade to the latest version.", p.ID), - }) + s.ID(), + p.ID, + )) } } diff --git a/apps/advisor/pkg/app/checks/utils.go b/apps/advisor/pkg/app/checks/utils.go new file mode 100644 index 00000000000..a6c4089da44 --- /dev/null +++ b/apps/advisor/pkg/app/checks/utils.go @@ -0,0 +1,21 @@ +package checks + +import ( + advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" +) + +func NewCheckReportError( + severity advisor.CheckReportErrorSeverity, + reason string, + action string, + stepID string, + itemID string, +) advisor.CheckReportError { + return advisor.CheckReportError{ + Severity: severity, + Reason: reason, + Action: action, + StepID: stepID, + ItemID: itemID, + } +} diff --git a/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go new file mode 100644 index 00000000000..fc5ff150a18 --- /dev/null +++ b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go @@ -0,0 +1,82 @@ +package checktyperegisterer + +import ( + "context" + "fmt" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/k8s" + "github.com/grafana/grafana-app-sdk/resource" + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Runner is a "runnable" app used to be able to expose and API endpoint +// with the existing checks types. This does not need to be a CRUD resource, but it is +// the only way existing at the moment to expose the check types. +type Runner struct { + checkRegistry checkregistry.CheckService + client resource.Client +} + +// NewRunner creates a new Runner. +func New(cfg app.Config) (app.Runnable, error) { + // Read config + checkRegistry, ok := cfg.SpecificConfig.(checkregistry.CheckService) + if !ok { + return nil, fmt.Errorf("invalid config type") + } + + // Prepare storage client + clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{}) + client, err := clientGenerator.ClientFor(advisorv0alpha1.CheckTypeKind()) + if err != nil { + return nil, err + } + + return &Runner{ + checkRegistry: checkRegistry, + client: client, + }, nil +} + +func (r *Runner) Run(ctx context.Context) error { + for _, t := range r.checkRegistry.Checks() { + steps := t.Steps() + stepTypes := make([]advisorv0alpha1.CheckTypeStep, len(steps)) + for i, s := range steps { + stepTypes[i] = advisorv0alpha1.CheckTypeStep{ + Title: s.Title(), + Description: s.Description(), + StepID: s.ID(), + } + } + obj := &advisorv0alpha1.CheckType{ + ObjectMeta: metav1.ObjectMeta{ + Name: t.ID(), + Namespace: metav1.NamespaceDefault, + }, + Spec: advisorv0alpha1.CheckTypeSpec{ + Name: t.ID(), + Steps: stepTypes, + }, + } + id := obj.GetStaticMetadata().Identifier() + _, err := r.client.Create(ctx, id, obj, resource.CreateOptions{}) + if err != nil { + if errors.IsAlreadyExists(err) { + // Already exists, update + _, err = r.client.Update(ctx, id, obj, resource.UpdateOptions{}) + if err != nil { + return err + } else { + continue + } + } + return err + } + } + return nil +} diff --git a/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer_test.go b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer_test.go new file mode 100644 index 00000000000..e10ecab9876 --- /dev/null +++ b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer_test.go @@ -0,0 +1,169 @@ +package checktyperegisterer + +import ( + "context" + "errors" + "testing" + + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + k8sErrs "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestCheckTypesRegisterer_Run(t *testing.T) { + tests := []struct { + name string + checks []checks.Check + createFunc func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) + updateFunc func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.UpdateOptions) (resource.Object, error) + expectedErr error + }{ + { + name: "successful create", + checks: []checks.Check{ + &mockCheck{ + id: "check1", + steps: []checks.Step{ + &mockStep{id: "step1", title: "Step 1", description: "Description 1"}, + }, + }, + }, + createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { + return obj, nil + }, + updateFunc: nil, + expectedErr: nil, + }, + { + name: "create already exists, successful update", + checks: []checks.Check{ + &mockCheck{ + id: "check1", + steps: []checks.Step{ + &mockStep{id: "step1", title: "Step 1", description: "Description 1"}, + }, + }, + }, + createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { + return nil, k8sErrs.NewAlreadyExists(schema.GroupResource{}, obj.GetName()) + }, + updateFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.UpdateOptions) (resource.Object, error) { + return obj, nil + }, + expectedErr: nil, + }, + { + name: "create error", + checks: []checks.Check{ + &mockCheck{ + id: "check1", + steps: []checks.Step{ + &mockStep{id: "step1", title: "Step 1", description: "Description 1"}, + }, + }, + }, + createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { + return nil, errors.New("create error") + }, + updateFunc: nil, + expectedErr: errors.New("create error"), + }, + { + name: "update error", + checks: []checks.Check{ + &mockCheck{ + id: "check1", + steps: []checks.Step{ + &mockStep{id: "step1", title: "Step 1", description: "Description 1"}, + }, + }, + }, + createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { + return nil, k8sErrs.NewAlreadyExists(schema.GroupResource{}, obj.GetName()) + }, + updateFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.UpdateOptions) (resource.Object, error) { + return nil, errors.New("update error") + }, + expectedErr: errors.New("update error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Runner{ + checkRegistry: &mockCheckRegistry{checks: tt.checks}, + client: &mockClient{ + createFunc: tt.createFunc, + updateFunc: tt.updateFunc, + }, + } + err := r.Run(context.Background()) + if err != nil { + if tt.expectedErr == nil { + t.Errorf("unexpected error: %v", err) + } else if err.Error() != tt.expectedErr.Error() { + t.Errorf("expected error: %v, got: %v", tt.expectedErr, err) + } + } + }) + } +} + +type mockCheckRegistry struct { + checks []checks.Check +} + +func (m *mockCheckRegistry) Checks() []checks.Check { + return m.checks +} + +type mockCheck struct { + checks.Check + + id string + steps []checks.Step +} + +func (m *mockCheck) ID() string { + return m.id +} + +func (m *mockCheck) Steps() []checks.Step { + return m.steps +} + +type mockStep struct { + checks.Step + + id string + title string + description string +} + +func (m *mockStep) ID() string { + return m.id +} + +func (m *mockStep) Title() string { + return m.title +} + +func (m *mockStep) Description() string { + return m.description +} + +type mockClient struct { + resource.Client + + createFunc func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) + updateFunc func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.UpdateOptions) (resource.Object, error) +} + +func (m *mockClient) Create(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { + return m.createFunc(ctx, id, obj, opts) +} + +func (m *mockClient) Update(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.UpdateOptions) (resource.Object, error) { + return m.updateFunc(ctx, id, obj, opts) +} From 57e30633e9f34a2850201824eb623d67a2e1ba8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Thu, 6 Feb 2025 15:02:34 +0100 Subject: [PATCH 385/894] Docs: Add a note on query caching for Cloudwatch datasource (#100180) --- .../sources/administration/data-source-management/_index.md | 4 ++++ docs/sources/datasources/aws-cloudwatch/_index.md | 6 +++++- .../datasources/aws-cloudwatch/query-editor/index.md | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/sources/administration/data-source-management/_index.md b/docs/sources/administration/data-source-management/_index.md index 71c5d87d625..ce91fc46eae 100644 --- a/docs/sources/administration/data-source-management/_index.md +++ b/docs/sources/administration/data-source-management/_index.md @@ -110,6 +110,10 @@ By reducing the number of queries and requests sent to data sources, caching can Query caching works for Grafana's [built-in data sources]({{< relref "../../datasources/#built-in-core-data-sources" >}}), and [backend data source plugins](https://grafana.com/grafana/plugins/?type=datasource) that extend the `DataSourceWithBackend` class in the plugins SDK. +{{% admonition type="note" %}} +Logs Insights for the CloudWatch data source does not support query caching due to the way logs are requested from AWS. +{{% /admonition %}} + To verify that a data source works with query caching, follow the [instructions below](#enable-and-configure-query-caching) to **Enable and Configure query caching**. If caching is enabled in Grafana but the Caching tab is not visible for the given data source, then query caching is not available for that data source. {{% admonition type="note" %}} diff --git a/docs/sources/datasources/aws-cloudwatch/_index.md b/docs/sources/datasources/aws-cloudwatch/_index.md index 6e5d3a0618e..414e770d2bc 100644 --- a/docs/sources/datasources/aws-cloudwatch/_index.md +++ b/docs/sources/datasources/aws-cloudwatch/_index.md @@ -356,6 +356,10 @@ The CloudWatch data source can query data from both CloudWatch metrics and Cloud For details, see the [query editor documentation]({{< relref "./query-editor" >}}). +## Query caching + +When you enable [query and resource caching]({{< relref "/administration/data-source-management/#query-and-resource-caching" >}}), Grafana temporarily stores the results of data source queries and resource requests. Query caching is available in CloudWatch Metrics in Grafana Cloud and Grafana Enterprise. It is not available in CloudWatch Logs Insights due to how query results are polled from AWS. + ## Use template variables Instead of hard-coding details such as server, application, and sensor names in metric queries, you can use variables. @@ -438,7 +442,7 @@ For more information, refer to the AWS documentation for [Service Quotas](https: The CloudWatch plugin enables you to monitor and troubleshoot applications across multiple regional accounts. Using cross-account observability, you can seamlessly search, visualize and analyze metrics and logs without worrying about account boundaries. -To use this feature, configure in the [AWS console under Cloudwatch Settings](https://aws.amazon.com/blogs/aws/new-amazon-cloudwatch-cross-account-observability/), a monitoring and source account, and then add the necessary IAM permissions as described above. +To use this feature, configure in the [AWS console under CloudWatch Settings](https://aws.amazon.com/blogs/aws/new-amazon-cloudwatch-cross-account-observability/), a monitoring and source account, and then add the necessary IAM permissions as described above. ## CloudWatch Logs data protection diff --git a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md index b65bd308e77..05b5eb3a7f4 100644 --- a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md +++ b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md @@ -226,7 +226,7 @@ The label field allows you to override the default name of the metric legend usi ## Query CloudWatch Logs The logs query editor helps you write CloudWatch Logs Query Language queries across defined regions and log groups. -It supports querying Cloudwatch logs with Logs Insights Query Language, OpenSearch PPL and OpenSearch SQL. +It supports querying CloudWatch logs with Logs Insights Query Language, OpenSearch PPL and OpenSearch SQL. ### Create a CloudWatch Logs query @@ -237,7 +237,7 @@ It supports querying Cloudwatch logs with Logs Insights Query Language, OpenSear Region and log groups are mandatory fields when querying with Logs Insights QL and OpenSearch PPL. Log group selection is not necessary when querying with OpenSearch SQL. However, selecting log groups simplifies writing logs queries by populating syntax suggestions with discovered log group fields. {{< /admonition >}} -1. Use the main input area to write your logs query. AWS Cloudwatch only supports a subset of OpenSearch SQL and PPL commands. To find out more about the syntax supported, consult [Amazon CloudWatch Logs documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData_Languages.html) +1. Use the main input area to write your logs query. AWS CloudWatch only supports a subset of OpenSearch SQL and PPL commands. To find out more about the syntax supported, consult [Amazon CloudWatch Logs documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData_Languages.html) #### Querying Log groups with OpenSearch SQL From 98e3237ce27455700f802cd4dc7318e3cdd1f55c Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Thu, 6 Feb 2025 14:05:52 +0000 Subject: [PATCH 386/894] LBAC for data sources: Update to indicate that we have `experimental` and `GA` (#100187) update confusing docs --- .../data-source-management/teamlbac/_index.md | 9 ++------- .../teamlbac/configure-teamlbac-for-prometheus/index.md | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/sources/administration/data-source-management/teamlbac/_index.md b/docs/sources/administration/data-source-management/teamlbac/_index.md index 170104fab71..74470c792bf 100644 --- a/docs/sources/administration/data-source-management/teamlbac/_index.md +++ b/docs/sources/administration/data-source-management/teamlbac/_index.md @@ -20,7 +20,7 @@ Label-Based Access Control (LBAC) allows fine-grained access control to data sou ## Supported Data Sources -LBAC for data sources is currently available for `Loki, Prometheus` with basic authentication. Support for additional data sources may be added in future updates. +LBAC for data sources is currently generally available for `Loki` and in **experimental** for `Prometheus`. Support for additional data sources may be added in future updates. **LBAC for data sources offers:** @@ -28,11 +28,6 @@ LBAC for data sources is currently available for `Loki, Prometheus` with basic a - Simplified data source management by consolidating multiple sources into one. - Dashboard reuse across teams with tailored access. -{{< admonition type="note" >}} -LBAC rules is available for **private preview** in Grafana Cloud. -Report any unexpected behavior to the Grafana Support team. -{{< /admonition >}} - You can configure user access based upon team memberships using `LogQL`. LBAC for data sources controls access to logs or metrics depending on the rules set for each team. @@ -57,7 +52,7 @@ This flexibility allows teams to use the same data source for multiple use cases ## Before you begin -To be able to use LBAC for data sources, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance. +To be able to use LBAC for data sources metrics, you need to enable the feature toggle `teamHttpHeadersMimir` on your Grafana instance. ## Limitations diff --git a/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-prometheus/index.md b/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-prometheus/index.md index aa54d970e6f..1cc06175900 100644 --- a/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-prometheus/index.md +++ b/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-prometheus/index.md @@ -22,7 +22,7 @@ You cannot configure LBAC rules for Grafana-provisioned data sources from the UI ## Before you begin -To be able to use LBAC for data sources rules, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance. Contact support to enable the feature toggle for you. +To be able to use LBAC for data sources rules, you need to enable the feature toggle `teamHttpHeadersMimir` on your Grafana instance. Contact support to enable the feature toggle for you. - Be sure that you have the permission setup to create a Prometheus tenant in Grafana Cloud - Be sure that you have admin data source permissions for Grafana. From d3ce9e1fe2240d213815f514cfbf7eb7ad9fd36f Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Thu, 6 Feb 2025 08:24:30 -0600 Subject: [PATCH 387/894] Docs: adding actions_allow_post_url example to plugin docs (#96157) --- .../administration/plugin-management/index.md | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/sources/administration/plugin-management/index.md b/docs/sources/administration/plugin-management/index.md index 09566ccf250..21e048ed9ea 100644 --- a/docs/sources/administration/plugin-management/index.md +++ b/docs/sources/administration/plugin-management/index.md @@ -66,7 +66,7 @@ To prevent users from seeing an app plugin, refer to [these permissions scenario ## Plugin catalog -The Grafana plugin catalog allows you to browse and manage plugins from within Grafana. Only Grafana server administrators and Organization administrators can access and use the plugin catalog. For more information about Grafana roles and permissions, refer to [Roles and permissions]({{< relref "../administration/roles-and-permissions" >}}). +The Grafana plugin catalog allows you to browse and manage plugins from within Grafana. Only Grafana server administrators and Organization administrators can access and use the plugin catalog. For more information about Grafana roles and permissions, refer to [Roles and permissions]({{< relref "../roles-and-permissions" >}}). The following access rules apply depending on the user role: @@ -236,6 +236,32 @@ WARN[06-01|16:45:59] Running an unsigned plugin pluginID= If you're developing a plugin, then you can enable development mode to allow all unsigned plugins. {{% /admonition %}} +## Integrate plugins + +You can configure your Grafana instance to let the frontends of installed plugins directly communicate locally with the backends of other installed plugins. By default, you can only communicate with plugin backends remotely. You can use this configuration to, for example, enable a [canvas panel](https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/canvas/) to call an application resource API that is permitted by the `actions_allow_post_url` option. + +To enable backend communication between plugins: + +1. Set the plugins you want to communicate with. In your configuration file (`grafana.ini` or `custom.ini` depending on your operating system) remove the semicolon to enable and then set the following configuration option: + + ``` + actions_allow_post_url= + ``` + + This is a comma-separated list that uses glob matching. + + - To allow access to all plugins that have a backend: + + ``` + actions_allow_post_url=/api/plugins/* + ``` + + - To access to the backend of only one plugin: + + ``` + actions_allow_post_url=/api/plugins/ + ``` + ## Plugin Frontend Sandbox {{% admonition type="caution" %}} From ae33d4903657a2b08a9e7ca74b5f4f14edf69b20 Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Thu, 6 Feb 2025 15:29:54 +0100 Subject: [PATCH 388/894] Advisor: Assert new fields (#100199) --- apps/advisor/pkg/app/checks/plugincheck/check_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/advisor/pkg/app/checks/plugincheck/check_test.go b/apps/advisor/pkg/app/checks/plugincheck/check_test.go index 53450ce7bc5..29dc7ccaabd 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check_test.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check_test.go @@ -44,6 +44,8 @@ func TestRun(t *testing.T) { Severity: advisor.CheckReportErrorSeverityHigh, Reason: "Plugin deprecated: plugin1", Action: "Check the documentation for recommended steps.", + StepID: "deprecation", + ItemID: "plugin1", }, }, }, @@ -63,6 +65,8 @@ func TestRun(t *testing.T) { Severity: advisor.CheckReportErrorSeverityLow, Reason: "New version available for plugin2", Action: "Go to the plugin admin page and upgrade to the latest version.", + StepID: "update", + ItemID: "plugin2", }, }, }, @@ -82,6 +86,8 @@ func TestRun(t *testing.T) { Severity: advisor.CheckReportErrorSeverityLow, Reason: "New version available for plugin2", Action: "Go to the plugin admin page and upgrade to the latest version.", + StepID: "update", + ItemID: "plugin2", }, }, }, From f1eac34b54bceb70e2bca5454360b006cf3a0c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Feb 2025 15:30:54 +0100 Subject: [PATCH 389/894] Dashboard: Simpify is empty handling (#100189) * Dashboard: Simpify is empty handling * remove unused imports * Update * Update * Update * add code for other layouts --------- Co-authored-by: Victor Marin --- .../dashboard-scene/scene/DashboardScene.tsx | 2 -- .../scene/DashboardSceneRenderer.tsx | 6 +----- .../DefaultGridLayoutManager.tsx | 13 ++++++++++++ .../ResponsiveGridLayoutManager.tsx | 20 ++++++++++++++++++- .../scene/layout-rows/RowsLayoutManager.tsx | 10 ++++++++++ .../transformSaveModelSchemaV2ToScene.ts | 17 +--------------- .../transformSaveModelToScene.ts | 17 +--------------- 7 files changed, 45 insertions(+), 40 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 29c9380039c..14f289bf559 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -128,8 +128,6 @@ export interface DashboardSceneState extends SceneObjectState { editPanel?: PanelEditor; /** Scene object that handles the current drawer or modal */ overlay?: SceneObject; - /** The dashboard doesn't have panels */ - isEmpty?: boolean; /** Kiosk mode */ kioskMode?: KioskMode; /** Share view */ diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index 794cbe2b2d8..e30068924e7 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -5,7 +5,6 @@ import { PageLayoutType } from '@grafana/data'; import { SceneComponentProps } from '@grafana/scenes'; import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; -import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty'; import { useSelector } from 'app/types'; import { DashboardEditPaneSplitter } from '../edit-pane/DashboardEditPaneSplitter'; @@ -15,7 +14,7 @@ import { PanelSearchLayout } from './PanelSearchLayout'; import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner'; export function DashboardSceneRenderer({ model }: SceneComponentProps) { - const { controls, overlay, editview, editPanel, isEmpty, viewPanelScene, panelSearch, panelsPerRow, isEditing } = + const { controls, overlay, editview, editPanel, viewPanelScene, panelSearch, panelsPerRow, isEditing } = model.useState(); const { type } = useParams(); const location = useLocation(); @@ -56,9 +55,6 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps - {isEmpty && ( - - )} ); diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index c7389618108..a1416c5deb0 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -9,9 +9,11 @@ import { sceneUtils, SceneComponentProps, SceneGridItemLike, + useSceneObjectState, } from '@grafana/scenes'; import { GRID_COLUMN_COUNT } from 'app/core/constants'; import { t } from 'app/core/internationalization'; +import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty'; import { isClonedKey, joinCloneKeys } from '../../utils/clone'; import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; @@ -22,6 +24,7 @@ import { NEW_PANEL_WIDTH, getVizPanelKeyForPanelId, getGridItemKeyForPanelId, + getDashboardSceneFor, } from '../../utils/utils'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; @@ -447,6 +450,16 @@ export class DefaultGridLayoutManager } public static Component = ({ model }: SceneComponentProps) => { + const { children } = useSceneObjectState(model.state.grid, { shouldActivateOrKeepAlive: true }); + const dashboard = getDashboardSceneFor(model); + + // If we are top level layout and have no children, show empty state + if (model.parent === dashboard && children.length === 0) { + return ( + + ); + } + return ; }; } diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index 91fcbec0a48..f936d8617eb 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -1,8 +1,16 @@ import { SelectableValue } from '@grafana/data'; -import { SceneComponentProps, SceneCSSGridLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { + SceneComponentProps, + SceneCSSGridLayout, + SceneObjectBase, + SceneObjectState, + useSceneObjectState, + VizPanel, +} from '@grafana/scenes'; import { Select } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty'; import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelId } from '../../utils/utils'; @@ -140,6 +148,16 @@ export class ResponsiveGridLayoutManager public activateRepeaters(): void {} public static Component = ({ model }: SceneComponentProps) => { + const { children } = useSceneObjectState(model.state.layout, { shouldActivateOrKeepAlive: true }); + const dashboard = getDashboardSceneFor(model); + + // If we are top level layout and have no children, show empty state + if (model.parent === dashboard && children.length === 0) { + return ( + + ); + } + return ; }; } 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 dc9154d3b2b..1aafc0adeb2 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -12,8 +12,10 @@ import { } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; +import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty'; import { isClonedKey } from '../../utils/clone'; +import { getDashboardSceneFor } from '../../utils/utils'; import { DashboardScene } from '../DashboardScene'; import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; @@ -195,6 +197,14 @@ export class RowsLayoutManager extends SceneObjectBase i public static Component = ({ model }: SceneComponentProps) => { const { rows } = model.useState(); const styles = useStyles2(getStyles); + const dashboard = getDashboardSceneFor(model); + + // If we are top level layout and have no children, show empty state + if (model.parent === dashboard && rows.length === 0) { + return ( + + ); + } return (
diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 75be7ae3a42..fd82cc7c161 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -88,7 +88,7 @@ import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior import { RowActions } from '../scene/layout-default/row-actions/RowActions'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState'; -import { getDashboardSceneFor, getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils'; +import { getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils'; import { GRID_ROW_HEIGHT } from './const'; import { SnapshotVariable } from './custom-variables/SnapshotVariable'; @@ -181,7 +181,6 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo { - if (n.children.length !== p.children.length || n.children !== p.children) { - getDashboardSceneFor(grid).setState({ isEmpty: n.children.length === 0 }); - } - }); - - return () => { - sub.unsubscribe(); - }; -} - function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined { if (!panel.spec.data?.spec.queries?.length) { return undefined; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 77960f558ce..b5ca18ea717 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -50,7 +50,7 @@ import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState'; import { DashboardInteractions } from '../utils/interactions'; -import { getDashboardSceneFor, getVizPanelKeyForPanelId } from '../utils/utils'; +import { getVizPanelKeyForPanelId } from '../utils/utils'; import { createVariablesForDashboard, createVariablesForSnapshot } from '../utils/variables'; import { getAngularPanelMigrationHandler } from './angularMigration'; @@ -267,7 +267,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, grid: new SceneGridLayout({ isLazy: !(dto.preload || contextSrv.user.authenticatedBy === 'render'), children: createSceneObjectsForPanels(oldModel.panels), - $behaviors: [trackIfEmpty], }), }), $timeRange: new SceneTimeRange({ @@ -426,20 +425,6 @@ export const convertOldSnapshotToScenesSnapshot = (panel: PanelModel) => { } }; -function trackIfEmpty(grid: SceneGridLayout) { - getDashboardSceneFor(grid).setState({ isEmpty: grid.state.children.length === 0 }); - - const sub = grid.subscribeToState((n, p) => { - if (n.children.length !== p.children.length || n.children !== p.children) { - getDashboardSceneFor(grid).setState({ isEmpty: n.children.length === 0 }); - } - }); - - return () => { - sub.unsubscribe(); - }; -} - function getDashboardInteractionCallback(uid: string, title: string) { return (e: SceneInteractionProfileEvent) => { let interactionType = ''; From 4c52abb6b47bd4dadebbb705b8f922bf7e84e6b3 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 6 Feb 2025 07:33:06 -0700 Subject: [PATCH 390/894] Dashboard Schema V2: Introduce __legacyStringValue and deprecate string type for query prop in QueryVariableSpec (#99716) * Introduce __legacyStringValue and deprecate string type for query * Fix tests * Fix tests * remove default * kind should default to default ds if variable doesn't have ds field * lint * getDefaultDataSourceRef should not return undefined --- .../dashboard/v2alpha0/dashboard.schema.cue | 2 +- .../src/schema/dashboard/v2alpha0/examples.ts | 8 +++++++- .../src/schema/dashboard/v2alpha0/types.gen.ts | 4 ++-- ...ansformSceneToSaveModelSchemaV2.test.ts.snap | 8 +++++++- .../sceneVariablesSetToVariables.test.ts | 7 ++++++- .../sceneVariablesSetToVariables.ts | 11 ++++++++--- .../transformSaveModelSchemaV2ToScene.ts | 9 +++++---- .../transformSceneToSaveModelSchemaV2.test.ts | 5 ++++- .../transformSceneToSaveModelSchemaV2.ts | 17 ++++++++--------- .../serialization/transformToV2TypesUtils.ts | 3 +++ .../dashboard-scene/v2schema/test-helpers.ts | 2 +- .../dashboard/api/ResponseTransformers.test.ts | 3 ++- .../dashboard/api/ResponseTransformers.ts | 16 ++++++++++++---- 13 files changed, 66 insertions(+), 29 deletions(-) diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 170094040c1..e49511ddddd 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -651,7 +651,7 @@ QueryVariableSpec: { skipUrlSync: bool | *false description?: string datasource?: DataSourceRef - query: string | DataQueryKind | *"" + query: DataQueryKind regex: string | *"" sort: VariableSort definition?: string diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts index 340ffeeef12..40b091f29dd 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts @@ -344,7 +344,13 @@ export const handyTestingSchema: DashboardV2Spec = { multi: true, name: 'queryVar', options: [], - query: 'query1', + query: { + kind: 'prometheus', + spec: { + expr: 'test-query', + refId: 'A', + }, + }, refresh: 'onDashboardLoad', regex: 'regex1', skipUrlSync: false, diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index 0e41bb7fedc..641cb9173cb 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -960,7 +960,7 @@ export interface QueryVariableSpec { skipUrlSync: boolean; description?: string; datasource?: DataSourceRef; - query: string | DataQueryKind; + query: DataQueryKind; regex: string; sort: VariableSort; definition?: string; @@ -977,7 +977,7 @@ export const defaultQueryVariableSpec = (): QueryVariableSpec => ({ hide: "dontHide", refresh: "never", skipUrlSync: false, - query: "", + query: defaultDataQueryKind(), regex: "", sort: "disabled", options: [], diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap index a62ba3c1daf..7695bba0ab6 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap @@ -222,7 +222,13 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model "multi": true, "name": "queryVar", "options": [], - "query": "query1", + "query": { + "kind": "prometheus", + "spec": { + "expr": "label_values(node_boot_time_seconds)", + "refId": "A", + }, + }, "refresh": "onDashboardLoad", "regex": "regex1", "skipUrlSync": false, diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts index 2cbb9a03550..d7514139230 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -775,7 +775,12 @@ describe('sceneVariablesSetToVariables', () => { "multi": true, "name": "test", "options": [], - "query": "query", + "query": { + "kind": "fake-std", + "spec": { + "__legacyStringValue": "query", + }, + }, "refresh": "onDashboardLoad", "regex": "", "skipUrlSync": false, diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index bb9c410c7ec..c7d86f4c910 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -27,6 +27,7 @@ import { transformVariableRefreshToEnum, transformVariableHideToEnum, transformSortVariableToEnum, + LEGACY_STRING_VALUE_KEY, } from './transformToV2TypesUtils'; /** * Converts a SceneVariables object into an array of VariableModel objects. @@ -269,16 +270,20 @@ export function sceneVariablesSetToSchemaV2Variables( if (transformVariableRefreshToEnum(variable.state.refresh) === 'never' || keepQueryOptions) { options = variableValueOptionsToVariableOptions(variable.state); } - //query: DataQueryKind | string; const query = variable.state.query; let dataQuery: DataQueryKind | string; if (typeof query !== 'string') { dataQuery = { - kind: getDataQueryKind(query), + kind: variable.state.datasource?.type ?? getDataQueryKind(query), spec: getDataQuerySpec(query), }; } else { - dataQuery = query; + dataQuery = { + kind: variable.state.datasource?.type ?? getDataQueryKind(query), + spec: { + [LEGACY_STRING_VALUE_KEY]: query, + }, + }; } const queryVariable: QueryVariableKind = { kind: 'QueryVariable', diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index fd82cc7c161..61ed7495a8c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -100,6 +100,7 @@ import { transformVariableHideToEnumV1, transformVariableRefreshToEnumV1, } from './transformToV1TypesUtils'; +import { LEGACY_STRING_VALUE_KEY } from './transformToV2TypesUtils'; const DEFAULT_DATASOURCE = 'default'; @@ -635,12 +636,12 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S } function getDataQueryForVariable(variable: QueryVariableKind) { - return typeof variable.spec.query !== 'string' - ? { + return LEGACY_STRING_VALUE_KEY in variable.spec.query.spec + ? (variable.spec.query.spec[LEGACY_STRING_VALUE_KEY] ?? '') + : { ...variable.spec.query.spec, refId: variable.spec.query.spec.refId ?? 'A', - } - : (variable.spec.query ?? ''); + }; } export function getCurrentValueForOldIntervalModel(variable: IntervalVariableKind, intervals: string[]): string { diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index 43121f54d8b..c87a4a0a687 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -225,7 +225,10 @@ describe('transformSceneToSaveModelSchemaV2', () => { hide: VariableHideV1.hideLabel, value: 'value1', text: 'text1', - query: 'query1', + query: { + expr: 'label_values(node_boot_time_seconds)', + refId: 'A', + }, definition: 'definition1', datasource: { uid: 'datasource1', type: 'prometheus' }, sort: VariableSortV1.alphabeticalDesc, diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 67cae374214..928ce92733f 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -403,8 +403,11 @@ function getVizPanelQueries(vizPanel: VizPanel): PanelQueryKind[] { return queries; } -export function getDataQueryKind(query: SceneDataQuery): string { - // If the query has a datasource, use the datasource type, otherwise return empty kind +export function getDataQueryKind(query: SceneDataQuery | string): string { + if (typeof query === 'string') { + return getDefaultDataSourceRef()?.type ?? ''; + } + return query.datasource?.type ?? getDefaultDataSourceRef()?.type ?? ''; } @@ -616,19 +619,15 @@ export function getAnnotationQueryKind(annotationQuery: AnnotationQuery): string } } -export function getDefaultDataSourceRef(): DataSourceRef | undefined { +export function getDefaultDataSourceRef(): DataSourceRef { // we need to return the default datasource configured in the BootConfig const defaultDatasource = config.bootData.settings.defaultDatasource; // get default datasource type - const dsList = config.bootData.settings.datasources ?? {}; + const dsList = config.bootData.settings.datasources; const ds = dsList[defaultDatasource]; - if (ds) { - return { type: ds.meta.id, uid: ds.name }; // in the datasource list from bootData "id" is the type - } - - return undefined; + return { type: ds.meta.id, uid: ds.name }; // in the datasource list from bootData "id" is the type } // Function to know if the dashboard transformed is a valid DashboardV2Spec diff --git a/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts b/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts index ca7c056d752..582b6b4cfcf 100644 --- a/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts +++ b/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts @@ -19,6 +19,9 @@ import { FieldColorModeId as FieldColorModeIdV2, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; +// used for QueryVariableKind's query prop - in schema V2 we've deprecated string type and support only DataQuery +export const LEGACY_STRING_VALUE_KEY = '__legacyStringValue'; + export function transformCursorSynctoEnum(cursorSync?: DashboardCursorSyncV1): DashboardCursorSync { switch (cursorSync) { case 0: diff --git a/public/app/features/dashboard-scene/v2schema/test-helpers.ts b/public/app/features/dashboard-scene/v2schema/test-helpers.ts index 9c37f1df5d5..9f901bf67e4 100644 --- a/public/app/features/dashboard-scene/v2schema/test-helpers.ts +++ b/public/app/features/dashboard-scene/v2schema/test-helpers.ts @@ -53,7 +53,7 @@ export function validateVariable< } if (sceneVariable instanceof QueryVariable && variableKind.kind === 'QueryVariable') { expect(sceneVariable?.state.datasource).toBe(variableKind.spec.datasource); - expect(sceneVariable?.state.query).toBe(variableKind.spec.query); + expect(sceneVariable?.state.query).toEqual(variableKind.spec.query.spec); } if (sceneVariable instanceof CustomVariable && variableKind.kind === 'CustomVariable') { expect(sceneVariable?.state.query).toBe(variableKind.spec.query); diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index 3a677bf658e..1dfbd14eb04 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -19,6 +19,7 @@ import { } from 'app/features/apiserver/types'; import { getDefaultDataSourceRef } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2'; import { + LEGACY_STRING_VALUE_KEY, transformVariableHideToEnum, transformVariableRefreshToEnum, } from 'app/features/dashboard-scene/serialization/transformToV2TypesUtils'; @@ -915,7 +916,7 @@ describe('ResponseTransformers', () => { expect(v2.spec.datasource).toEqual(v1.datasource); if (typeof v1.query === 'string') { - expect(v2.spec.query).toEqual(v1.query); + expect(v2.spec.query.spec[LEGACY_STRING_VALUE_KEY]).toEqual(v1.query); } else { expect(v2.spec.query).toEqual({ kind: v1.datasource?.type, diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 535265f606d..1b72d6b3c08 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -62,6 +62,7 @@ import { transformVariableRefreshToEnumV1, } from 'app/features/dashboard-scene/serialization/transformToV1TypesUtils'; import { + LEGACY_STRING_VALUE_KEY, transformCursorSynctoEnum, transformDataTopic, transformSortVariableToEnum, @@ -504,8 +505,12 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables'] let query = v.query || {}; if (typeof query === 'string') { - console.error('Query variable query is a string. It needs to extend DataQuery.'); - query = {}; + console.warn( + 'Query variable query is a string which is deprecated in the schema v2. It should extend DataQuery' + ); + query = { + [LEGACY_STRING_VALUE_KEY]: query, + }; } const qv: QueryVariableKind = { @@ -527,7 +532,7 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables'] query: { kind: v.datasource?.type || getDefaultDatasourceType(), spec: { - ...query, + ...v.query, }, }, }, @@ -708,7 +713,10 @@ function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] { ...commonProperties, current: v.spec.current, options: v.spec.options, - query: typeof v.spec.query === 'string' ? v.spec.query : v.spec.query.spec, + query: + LEGACY_STRING_VALUE_KEY in v.spec.query.spec + ? v.spec.query.spec[LEGACY_STRING_VALUE_KEY] + : v.spec.query.spec, datasource: v.spec.datasource, sort: transformSortVariableToEnumV1(v.spec.sort), refresh: transformVariableRefreshToEnumV1(v.spec.refresh), From 21bfdd445f54e62a006a9f6552f5465d1a4fb1a3 Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Thu, 6 Feb 2025 08:01:11 -0700 Subject: [PATCH 391/894] Image Renderer: Minor refactor cleanup (#100089) --- pkg/services/rendering/rendering.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index 744f5e09e18..89370542de1 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -424,7 +424,7 @@ func (rs *RenderingService) getNewFilePath(rt RenderType) (string, error) { // getGrafanaCallbackURL creates a URL to send to the image rendering as callback for rendering a Grafana resource func (rs *RenderingService) getGrafanaCallbackURL(path string) string { - if rs.Cfg.RendererUrl != "" { + if rs.Cfg.RendererUrl != "" || rs.Cfg.RendererCallbackUrl != "" { // The backend rendering service can potentially be remote. // So we need to use the root_url to ensure the rendering service // can reach this Grafana instance. @@ -433,11 +433,6 @@ func (rs *RenderingService) getGrafanaCallbackURL(path string) string { return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path) } - if rs.Cfg.RendererCallbackUrl != "" { - // &render=1 signals to the legacy redirect layer to - return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path) - } - protocol := rs.Cfg.Protocol switch protocol { case setting.HTTPScheme: From 05ea450dd28606e104f8c36a042237015084a16c Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 6 Feb 2025 16:01:50 +0100 Subject: [PATCH 392/894] Drone: Add apps directory for backend tests (#100204) --- .drone.yml | 4 +++- scripts/drone/events/pr.star | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index b00daa6bcaf..f48a891415e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -489,6 +489,7 @@ trigger: - public/app/plugins/**/plugin.json - docs/sources/setup-grafana/configure-grafana/feature-toggles/** - devenv/** + - apps/** type: docker volumes: - host: @@ -598,6 +599,7 @@ trigger: - public/app/plugins/**/plugin.json - devenv/** - .bingo/** + - apps/** type: docker volumes: - host: @@ -5599,6 +5601,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: abd48dfdec08a719b414fba06b06dfc97560df31d81751cd833b5fdf5b7f9937 +hmac: e1f198d994216d163d6a710aef2f6091572eb624081c550a5db42208ef37827b ... diff --git a/scripts/drone/events/pr.star b/scripts/drone/events/pr.star index 9a57ec4cc53..7d42325bef3 100644 --- a/scripts/drone/events/pr.star +++ b/scripts/drone/events/pr.star @@ -116,6 +116,7 @@ def pr_pipelines(): "public/app/plugins/**/plugin.json", "docs/sources/setup-grafana/configure-grafana/feature-toggles/**", "devenv/**", + "apps/**", ], ), ver_mode, @@ -134,6 +135,7 @@ def pr_pipelines(): "public/app/plugins/**/plugin.json", "devenv/**", ".bingo/**", + "apps/**", ], ), ver_mode, From 1bf53e7a5fb9020d6d7b64fed360e9e4c6dfdab9 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 6 Feb 2025 08:33:18 -0700 Subject: [PATCH 393/894] Dashboard Schema V2: E2E setup (#99843) * basic setup * update CODEOWNERS * update name * add temp test that ensures we are loading schema V2 json in the UI * update language * test with yarn cache and combine steps * revert combine * remove commented out code * Run current dashboard suite, make workflow optional * make job always succeed * Remove temp v2 suite * don't run on draft PRs * command for old arch --- .github/CODEOWNERS | 1 + .github/workflows/run-schema-v2-e2e.yml | 44 +++++++++++++++++++++++++ e2e/cypress/support/e2e.js | 5 +++ e2e/run-suite | 19 +++++++++++ package.json | 3 +- 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/run-schema-v2-e2e.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cd0bde5129f..8d87433f7b9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -791,6 +791,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/publish-kinds-release.yml @grafana/platform-monitoring /.github/workflows/verify-kinds.yml @grafana/platform-monitoring /.github/workflows/dashboards-issue-add-label.yml @grafana/dashboards-squad +/.github/workflows/run-schema-v2-e2e.yml @grafana/dashboards-squad /.github/workflows/ephemeral-instances-pr-comment.yml @grafana/grafana-backend-services-squad /.github/workflows/create-security-patch-from-security-mirror.yml @grafana/grafana-developer-enablement-squad /.github/workflows/core-plugins-build-and-release.yml @grafana/plugins-platform-frontend @grafana/plugins-platform-backend diff --git a/.github/workflows/run-schema-v2-e2e.yml b/.github/workflows/run-schema-v2-e2e.yml new file mode 100644 index 00000000000..8b55aa4c430 --- /dev/null +++ b/.github/workflows/run-schema-v2-e2e.yml @@ -0,0 +1,44 @@ +name: Run dashboard schema v2 e2e + +on: + push: + branches: + - main + pull_request: + branches: + - '**' + +env: + ARCH: linux-amd64 + +jobs: + dashboard-schema-v2-e2e: + runs-on: ubuntu-latest + continue-on-error: true + if: github.event.pull_request.draft == false + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Pin Go version to mod file + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - run: go version + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'yarn' + - name: Install dependencies + run: yarn install --immutable + - name: Build grafana + run: make build + - name: Install Cypress dependencies + uses: cypress-io/github-action@v6 + with: + runTests: false + - name: Run dashboard scenes e2e + run: yarn e2e:schema-v2 || echo "Test failed but marking as success since schema V2 is behind a feature flag and should not block PRs" + + - name: Always succeed # This is a workaround to make the job pass even if the previous step fails + if: failure() + run: exit 0 \ No newline at end of file diff --git a/e2e/cypress/support/e2e.js b/e2e/cypress/support/e2e.js index 3e03d0f4344..ddd50a58844 100644 --- a/e2e/cypress/support/e2e.js +++ b/e2e/cypress/support/e2e.js @@ -50,4 +50,9 @@ beforeEach(() => { cy.logToConsole('disabling dashboardScene feature toggle in localstorage'); cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=false'); } + + if (Cypress.env('useV2DashboardsAPI')) { + cy.logToConsole('enabling v2 dashboards API in localstorage'); + cy.setLocalStorage('grafana.featureToggles', 'useV2DashboardsAPI=true'); + } }); diff --git a/e2e/run-suite b/e2e/run-suite index 11569b43982..bc0eebd1567 100755 --- a/e2e/run-suite +++ b/e2e/run-suite @@ -28,6 +28,7 @@ declare -A env=( testFilesForSingleSuite="*.spec.ts" rootForEnterpriseSuite="./e2e/extensions-suite" rootForOldArch="./e2e/old-arch" +rootForDashboardsSchemaV2="./e2e/dashboards-suite" declare -A cypressConfig=( [screenshotsFolder]=./e2e/"${args[0]}"/screenshots @@ -111,6 +112,24 @@ case "$1" in cypressConfig[video]=${args[1]} env[DISABLE_SCENES]=true ;; + "dashboards-schema-v2") + env[useV2DashboardsAPI]=true + cypressConfig[specPattern]=$rootForDashboardsSchemaV2/$testFilesForSingleSuite + cypressConfig[video]=false + case "$2" in + "debug") + echo -e "Debug mode" + env[SLOWMO]=1 + PARAMS="--no-exit" + enterpriseSuite=$(basename "${args[2]}") + ;; + "dev") + echo "Dev mode" + CMD="cypress open" + enterpriseSuite=$(basename "${args[2]}") + ;; + esac + ;; "enterprise-smtp") env[SMTP_PLUGIN_ENABLED]=true cypressConfig[specPattern]=./e2e/extensions/enterprise/smtp-suite/$testFilesForSingleSuite diff --git a/package.json b/package.json index d87d9050539..5d19e8cf0d2 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "build:stats": "NODE_ENV=production webpack --progress --config scripts/webpack/webpack.stats.js", "dev": "NODE_ENV=dev nx exec -- webpack --config scripts/webpack/webpack.dev.js", "e2e": "./e2e/start-and-run-suite", - "e2e:scenes": "./e2e/start-and-run-suite scenes", + "e2e:old-arch": "./e2e/start-and-run-suite old-arch", + "e2e:schema-v2": "./e2e/start-and-run-suite dashboards-schema-v2", "e2e:debug": "./e2e/start-and-run-suite debug", "e2e:dev": "./e2e/start-and-run-suite dev", "e2e:benchmark:live": "./e2e/start-and-run-suite benchmark live", From 126396399e2fe485da63fde580d3003fbbf9e451 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:40:07 +0100 Subject: [PATCH 394/894] Folder+Dashboard: Resolve parent folders as service in search (#100185) Resolve parent folders as service to guarantee that we can fetch the title --- pkg/services/dashboards/service/dashboard_service.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index efbd0787a45..ac0a3504f9f 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1249,13 +1249,17 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb finalResults := make([]dashboards.DashboardSearchProjection, len(response.Hits)) // Create a small runtime cache for folders to avoid extra calls to the folder service foldersMap := make(map[string]*folder.Folder) + serviceCtx, serviceIdent := identity.WithServiceIdentity(ctx, query.OrgId) for i, hit := range response.Hits { f, ok := foldersMap[hit.Folder] if !ok { - f, err = dr.folderService.Get(ctx, &folder.GetFolderQuery{ + // We can get search result where user don't have access to parents. If that happens this thi + // will fail if we call it as the requesting user. To resolve this we call this as the service so we can + // garantuee that we can fetch the parent. + f, err = dr.folderService.Get(serviceCtx, &folder.GetFolderQuery{ UID: &hit.Folder, OrgID: query.OrgId, - SignedInUser: query.SignedInUser, + SignedInUser: serviceIdent, }) if err != nil { return nil, err From 0035ed8a5e070385b50099328bd93a665045564d Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Thu, 6 Feb 2025 11:05:30 -0500 Subject: [PATCH 395/894] Add log line for unexpected queries in ds querier (#100147) --- pkg/registry/apis/query/query.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index 95a27508b5a..f3675514a25 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -160,6 +160,7 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O // Actually run the query rsp, err := b.execute(ctx, req) if err != nil { + b.log.Error("hit unexpected error while executing query, this will show as an unhandled k8s status error", "err", err) responder.Error(err) return } From f8509273cbe9838d60efd7693bf6c56d7902e61e Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 6 Feb 2025 16:14:38 +0000 Subject: [PATCH 396/894] Slider: Fix text input box being too wide (#100138) * Fix Input width from className not being respected * Also use width prop in Slider --- packages/grafana-ui/src/components/Input/Input.tsx | 10 ++++++---- packages/grafana-ui/src/components/Slider/Slider.tsx | 1 + packages/grafana-ui/src/components/Slider/styles.ts | 1 - 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx index 09b113cf76b..563adfa9772 100644 --- a/packages/grafana-ui/src/components/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -60,19 +60,21 @@ export const Input = forwardRef((props, ref) => { // if a better solution is found. const isInAutoSizeInput = useContext(AutoSizeInputContext); const accessoriesWidth = (prefixRect.width || 0) + (suffixRect.width || 0); - const finalWidth = isInAutoSizeInput && width ? width + accessoriesWidth / 8 : width; + const autoSizeWidth = isInAutoSizeInput && width ? width + accessoriesWidth / 8 : undefined; const theme = useTheme2(); // Don't pass the width prop, as this causes an unnecessary amount of Emotion calls when auto sizing - const styles = getInputStyles({ theme, invalid: !!invalid }); + const styles = getInputStyles({ theme, invalid: !!invalid, width: autoSizeWidth ? undefined : width }); const suffix = suffixProp || (loading && ); return (
{!!addonBefore &&
{addonBefore}
} @@ -130,7 +132,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }: css({ label: 'input-wrapper', display: 'flex', - width: width ? theme.spacing(width) : '100%', // Not used in Input, as this causes performance issues with auto sizing + width: width ? theme.spacing(width) : '100%', height: theme.spacing(theme.components.height.md), borderRadius: theme.shape.radius.default, '&:hover': { diff --git a/packages/grafana-ui/src/components/Slider/Slider.tsx b/packages/grafana-ui/src/components/Slider/Slider.tsx index 5da049a3d0c..c677d402dc9 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.tsx @@ -109,6 +109,7 @@ export const Slider = ({ Date: Thu, 6 Feb 2025 17:16:30 +0100 Subject: [PATCH 397/894] Authz: client cache (#100195) * Reduce client permissions cache for authz client * Adjust server cache ttl --- pkg/services/authz/client.go | 12 ++++++++++++ pkg/services/authz/rbac/service.go | 10 +++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pkg/services/authz/client.go b/pkg/services/authz/client.go index 412a4e2acb9..506155df380 100644 --- a/pkg/services/authz/client.go +++ b/pkg/services/authz/client.go @@ -124,6 +124,10 @@ func newInProcLegacyClient(server *rbac.Service, tracer tracing.Tracer) (authlib authzlib.WithGrpcConnectionClientOption(channel), authzlib.WithDisableAccessTokenClientOption(), authzlib.WithTracerClientOption(tracer), + authzlib.WithCacheClientOption(cache.NewLocalCache(cache.Config{ + Expiry: 30 * time.Second, + CleanupInterval: 2 * time.Minute, + })), ) } @@ -147,6 +151,10 @@ func newGrpcLegacyClient(authCfg *Cfg, tracer tracing.Tracer) (authlib.AccessCli grpc.WithStreamInterceptor(clientInterceptor.StreamClientInterceptor), ), authzlib.WithTracerClientOption(tracer), + authzlib.WithCacheClientOption(cache.NewLocalCache(cache.Config{ + Expiry: 30 * time.Second, + CleanupInterval: 2 * time.Minute, + })), // TODO: remove this once access tokens are supported on-prem authzlib.WithDisableAccessTokenClientOption(), ) @@ -181,6 +189,10 @@ func newCloudLegacyClient(authCfg *Cfg, tracer tracing.Tracer) (authlib.AccessCl grpc.WithUnaryInterceptor(clientInterceptor.UnaryClientInterceptor), grpc.WithStreamInterceptor(clientInterceptor.StreamClientInterceptor), ), + authzlib.WithCacheClientOption(cache.NewLocalCache(cache.Config{ + Expiry: 30 * time.Second, + CleanupInterval: 2 * time.Minute, + })), authzlib.WithTracerClientOption(tracer), ) if err != nil { diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index 26bd352903c..349ccd53558 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -31,10 +31,10 @@ import ( ) const ( - shortCacheTTL = 1 * time.Minute - shortCleanupInterval = 5 * time.Minute - longCacheTTL = 5 * time.Minute - longCleanupInterval = 10 * time.Minute + shortCacheTTL = 30 * time.Second + shortCleanupInterval = 2 * time.Minute + longCacheTTL = 2 * time.Minute + longCleanupInterval = 4 * time.Minute ) type Service struct { @@ -82,7 +82,7 @@ func NewService( idCache: newCacheWrap[store.UserIdentifiers](cache, logger, longCacheTTL), permCache: newCacheWrap[map[string]bool](cache, logger, shortCacheTTL), teamCache: newCacheWrap[[]int64](cache, logger, shortCacheTTL), - basicRoleCache: newCacheWrap[store.BasicRole](cache, logger, longCacheTTL), + basicRoleCache: newCacheWrap[store.BasicRole](cache, logger, shortCacheTTL), folderCache: newCacheWrap[map[string]FolderNode](cache, logger, shortCacheTTL), sf: new(singleflight.Group), } From ccb0e9222afe2acf20efda0ffb38314616b3b5ac Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Thu, 6 Feb 2025 11:29:43 -0500 Subject: [PATCH 398/894] Alerting: Upgrade grafana/alerting to use EmbeddedContents (#99983) * Upgrade grafana/alerting to include EmbeddedContents for email images --- go.mod | 2 +- go.sum | 4 +-- go.work.sum | 3 +++ pkg/services/ngalert/notifier/email_test.go | 28 ++++++++++++++------- pkg/services/ngalert/notifier/sender.go | 28 ++++++++++++++------- pkg/storage/unified/apistore/go.mod | 2 +- pkg/storage/unified/apistore/go.sum | 4 +-- pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/go.sum | 4 +-- 9 files changed, 50 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 82a3b4de7e3..079a83598d0 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group 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-20250130152446-d49e2e0b7d65 // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index efdc65bf13c..5c2c8b5537c 100644 --- a/go.sum +++ b/go.sum @@ -1508,8 +1508,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 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/alerting v0.0.0-20250130152446-d49e2e0b7d65 h1:dmsycYQzl5JexuV8UxQpT3B79maSvhiIahid4/tezAM= -github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 h1:J3PIK9OL3ZHPypYHlcK+nBREwYL3ROZ3fJyNMsTYlpk= +github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 h1:0C0a1FEkecxDk60su4uuOozqBzqz/4nmfNFSxXnCViY= github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= diff --git a/go.work.sum b/go.work.sum index 14d84965e7b..9fc55608511 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1516,6 +1516,9 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20250115195200-209e052dba64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 h1:J3PIK9OL3ZHPypYHlcK+nBREwYL3ROZ3fJyNMsTYlpk= +github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:V63rh3udd7sqXJeaG+nGUmViwVnM/bY6t8U9Tols2GU= github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= diff --git a/pkg/services/ngalert/notifier/email_test.go b/pkg/services/ngalert/notifier/email_test.go index beb0251e17d..70e2a8543af 100644 --- a/pkg/services/ngalert/notifier/email_test.go +++ b/pkg/services/ngalert/notifier/email_test.go @@ -222,16 +222,26 @@ func (e emailSender) SendWebhook(ctx context.Context, cmd *receivers.SendWebhook } func (e emailSender) SendEmail(ctx context.Context, cmd *receivers.SendEmailSettings) error { + sendEmailCommand := notifications.SendEmailCommand{ + To: cmd.To, + SingleEmail: cmd.SingleEmail, + Template: cmd.Template, + Subject: cmd.Subject, + Data: cmd.Data, + ReplyTo: cmd.ReplyTo, + EmbeddedFiles: cmd.EmbeddedFiles, + } + if len(cmd.EmbeddedContents) > 0 { + sendEmailCommand.EmbeddedContents = make([]notifications.EmbeddedContent, len(cmd.EmbeddedContents)) + for i, ec := range cmd.EmbeddedContents { + sendEmailCommand.EmbeddedContents[i] = notifications.EmbeddedContent{ + Name: ec.Name, + Content: ec.Content, + } + } + } return e.ns.SendEmailCommandHandlerSync(ctx, ¬ifications.SendEmailCommandSync{ - SendEmailCommand: notifications.SendEmailCommand{ - To: cmd.To, - SingleEmail: cmd.SingleEmail, - Template: cmd.Template, - Subject: cmd.Subject, - Data: cmd.Data, - ReplyTo: cmd.ReplyTo, - EmbeddedFiles: cmd.EmbeddedFiles, - }, + SendEmailCommand: sendEmailCommand, }) } diff --git a/pkg/services/ngalert/notifier/sender.go b/pkg/services/ngalert/notifier/sender.go index be2f58b523d..67e68b2abea 100644 --- a/pkg/services/ngalert/notifier/sender.go +++ b/pkg/services/ngalert/notifier/sender.go @@ -27,15 +27,25 @@ func (s sender) SendWebhook(ctx context.Context, cmd *receivers.SendWebhookSetti } func (s sender) SendEmail(ctx context.Context, cmd *receivers.SendEmailSettings) error { + sendEmailCommand := notifications.SendEmailCommand{ + To: cmd.To, + SingleEmail: cmd.SingleEmail, + Template: cmd.Template, + Subject: cmd.Subject, + Data: cmd.Data, + ReplyTo: cmd.ReplyTo, + EmbeddedFiles: cmd.EmbeddedFiles, + } + if len(cmd.EmbeddedContents) > 0 { + sendEmailCommand.EmbeddedContents = make([]notifications.EmbeddedContent, len(cmd.EmbeddedContents)) + for i, ec := range cmd.EmbeddedContents { + sendEmailCommand.EmbeddedContents[i] = notifications.EmbeddedContent{ + Name: ec.Name, + Content: ec.Content, + } + } + } return s.ns.SendEmailCommandHandlerSync(ctx, ¬ifications.SendEmailCommandSync{ - SendEmailCommand: notifications.SendEmailCommand{ - To: cmd.To, - SingleEmail: cmd.SingleEmail, - Template: cmd.Template, - Subject: cmd.Subject, - Data: cmd.Data, - ReplyTo: cmd.ReplyTo, - EmbeddedFiles: cmd.EmbeddedFiles, - }, + SendEmailCommand: sendEmailCommand, }) } diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 2ac8214df01..58e48dafc0d 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -175,7 +175,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65 // indirect + github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 // indirect github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 3c8ae34e004..05f1f0f36b5 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -557,8 +557,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/alerting v0.0.0-20250130152446-d49e2e0b7d65 h1:dmsycYQzl5JexuV8UxQpT3B79maSvhiIahid4/tezAM= -github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 h1:J3PIK9OL3ZHPypYHlcK+nBREwYL3ROZ3fJyNMsTYlpk= +github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 h1:0C0a1FEkecxDk60su4uuOozqBzqz/4nmfNFSxXnCViY= github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 53a5f528af5..0b5cbb0b2db 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -115,7 +115,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65 // indirect + github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 17017755d74..96686090205 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -413,8 +413,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 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-20250130152446-d49e2e0b7d65 h1:dmsycYQzl5JexuV8UxQpT3B79maSvhiIahid4/tezAM= -github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 h1:J3PIK9OL3ZHPypYHlcK+nBREwYL3ROZ3fJyNMsTYlpk= +github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 h1:0C0a1FEkecxDk60su4uuOozqBzqz/4nmfNFSxXnCViY= github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= From 1467d4b3e3a783cefbd53dff3b525da65eb6909c Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:30:47 -0300 Subject: [PATCH 399/894] [search] Legacy search fallback support legacy query params (#99765) * add support for deleted query param * support tag query param in modes 2 and below * handle dashboardIds * hhandle dashboardUIDs * handle folderUIDs query param * handle page query param when hitting legacy storage * handle sort query param * handle type query param * re-enable search fallback * remove folder search workaround and fix /api/search to return both folders and dashboards when no title or type is provided --------- Co-authored-by: Stephanie Hingtgen --- pkg/apis/dashboard/v0alpha1/register.go | 1 + .../dashboard/legacysearcher/search_client.go | 169 ++++- pkg/registry/apis/dashboard/search.go | 48 +- pkg/registry/apis/dashboard/search_test.go | 7 +- pkg/services/apiserver/client/client.go | 10 +- .../dashboards/service/dashboard_service.go | 71 +- .../service/dashboard_service_test.go | 13 + pkg/services/search/service.go | 15 - pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/resource.pb.go | 645 +++++++++--------- pkg/storage/unified/resource/resource.proto | 50 +- pkg/storage/unified/resource/search.go | 33 + pkg/storage/unified/resource/search_client.go | 26 +- 13 files changed, 646 insertions(+), 444 deletions(-) diff --git a/pkg/apis/dashboard/v0alpha1/register.go b/pkg/apis/dashboard/v0alpha1/register.go index 9423f9ef195..e163aa01257 100644 --- a/pkg/apis/dashboard/v0alpha1/register.go +++ b/pkg/apis/dashboard/v0alpha1/register.go @@ -14,6 +14,7 @@ import ( const ( GROUP = "dashboard.grafana.app" VERSION = "v0alpha1" + RESOURCE = "dashboards" APIVERSION = GROUP + "/" + VERSION ) diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index 17aa5aa16a9..bd2e4a896a8 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -2,12 +2,18 @@ package legacysearcher import ( "context" + "encoding/json" "fmt" + "strconv" "strings" claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apis/dashboard" + folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/storage/unified/resource" "google.golang.org/grpc" @@ -28,29 +34,93 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour return nil, err } - if req.Query == "*" { - req.Query = "" + // the "*"s will be added in the k8s handler in dashboard_service.go in order to make search work + // in modes 3+. These "*"s will break the legacy sql query so we need to remove them here + if strings.Contains(req.Query, "*") { + req.Query = strings.ReplaceAll(req.Query, "*", "") } // TODO add missing support for the following query params: - // - tag - // - starred (won't support) - // - page (check) - // - type - // - sort - // - deleted + // - folderIds (won't support, must use folderUIDs) // - permission - // - dashboardIds - // - dashboardUIDs - // - folderIds - // - folderUIDs - // - sort (default by title) query := &dashboards.FindPersistedDashboardsQuery{ - Title: req.Query, - Limit: req.Limit, - // FolderUIDs: req.FolderUIDs, - Type: searchstore.TypeDashboard, + Title: req.Query, + Limit: req.Limit, + Page: req.Page, SignedInUser: user, + IsDeleted: req.IsDeleted, + } + + var queryType string + if req.Options.Key.Resource == dashboard.DASHBOARD_RESOURCE { + queryType = searchstore.TypeDashboard + } else if req.Options.Key.Resource == folderv0alpha1.RESOURCE { + queryType = searchstore.TypeFolder + } else { + return nil, fmt.Errorf("bad type request") + } + + if len(req.Federated) > 1 { + return nil, fmt.Errorf("bad type request") + } + + if len(req.Federated) == 1 && + ((req.Federated[0].Resource == dashboard.DASHBOARD_RESOURCE && queryType == searchstore.TypeFolder) || + (req.Federated[0].Resource == folderv0alpha1.RESOURCE && queryType == searchstore.TypeDashboard)) { + queryType = "" // makes the legacy store search across both + } + + if queryType != "" { + query.Type = queryType + } + + // technically, there exists the ability to register multiple ways of sorting using the legacy database + // see RegisterSortOption in pkg/services/search/sorting.go + // however, it doesn't look like we are taking advantage of that. And since by default the legacy + // sql will sort by title ascending, we only really need to handle the "alpha-desc" case + if req.SortBy != nil { + for _, sort := range req.SortBy { + if sort.Field == "title" && sort.Desc { + query.Sort = search.SortAlphaDesc + } + } + } + // handle deprecated dashboardIds query param + for _, field := range req.Options.Labels { + if field.Key == utils.LabelKeyDeprecatedInternalID { + values := field.GetValues() + dashboardIds := make([]int64, len(values)) + for i, id := range values { + if n, err := strconv.ParseInt(id, 10, 64); err == nil { + dashboardIds[i] = n + } + } + + query.DashboardIds = dashboardIds + } + } + + for _, field := range req.Options.Fields { + switch field.Key { + case resource.SEARCH_FIELD_TAGS: + query.Tags = field.GetValues() + case resource.SEARCH_FIELD_NAME: + query.DashboardUIDs = field.GetValues() + query.DashboardIds = nil + case resource.SEARCH_FIELD_FOLDER: + vals := field.GetValues() + folders := make([]string, len(vals)) + + for i, val := range vals { + if val == "" { + folders[i] = "general" + } else { + folders[i] = val + } + } + + query.FolderUIDs = folders + } } // TODO need to test this @@ -75,26 +145,73 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour Columns: []*resource.ResourceTableColumnDefinition{ searchFields.Field(resource.SEARCH_FIELD_TITLE), searchFields.Field(resource.SEARCH_FIELD_FOLDER), - // searchFields.Field(resource.SEARCH_FIELD_TAGS), + searchFields.Field(resource.SEARCH_FIELD_TAGS), }, }, } - for _, dashboard := range res { + hits := formatQueryResult(res) + + for _, dashboard := range hits { + tags, err := json.Marshal(dashboard.Tags) + if err != nil { + return nil, err + } + list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{ - Key: &resource.ResourceKey{ - Namespace: "default", - Group: "dashboard.grafana.app", - Resource: "dashboards", - Name: dashboard.UID, - }, - Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID)}, // TODO add tag + Key: getResourceKey(dashboard, req.Options.Key.Namespace), + Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), tags}, }) } return list, nil } +func getResourceKey(item *dashboards.DashboardSearchProjection, namespace string) *resource.ResourceKey { + if item.IsFolder { + return &resource.ResourceKey{ + Namespace: namespace, + Group: folderv0alpha1.GROUP, + Resource: folderv0alpha1.RESOURCE, + Name: item.UID, + } + } + + return &resource.ResourceKey{ + Namespace: namespace, + Group: dashboard.GROUP, + Resource: dashboard.DASHBOARD_RESOURCE, + Name: item.UID, + } +} + +func formatQueryResult(res []dashboards.DashboardSearchProjection) []*dashboards.DashboardSearchProjection { + hitList := make([]*dashboards.DashboardSearchProjection, 0) + hits := make(map[string]*dashboards.DashboardSearchProjection) + + for _, item := range res { + key := fmt.Sprintf("%s-%d", item.UID, item.OrgID) + hit, exists := hits[key] + if !exists { + hit = &dashboards.DashboardSearchProjection{ + UID: item.UID, + Title: item.Title, + FolderUID: item.FolderUID, + Tags: []string{}, + IsFolder: item.IsFolder, + } + hitList = append(hitList, hit) + hits[key] = hit + } + + if len(item.Term) > 0 { + hit.Tags = append(hit.Tags, item.Term) + } + } + + return hitList +} + func (c *DashboardSearchClient) GetStats(ctx context.Context, req *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) { info, err := claims.ParseNamespace(req.Namespace) if err != nil { diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index fc170a36783..4d9b8137c85 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -18,7 +18,9 @@ import ( "k8s.io/kube-openapi/pkg/validation/spec" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apis/dashboard" dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/builder" dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" @@ -199,7 +201,7 @@ func (s *SearchHandler) DoSortable(w http.ResponseWriter, r *http.Request) { const rootFolder = "general" -//nolint:gocyclo +// nolint:gocyclo func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { ctx, span := s.tracer.Start(r.Context(), "dashboard.search") defer span.End() @@ -231,6 +233,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { Query: queryParams.Get("query"), Limit: int64(limit), Offset: int64(offset), + Page: int64(offset), // on modes 0-2 (legacy) we use "Page" instead of "Offset" Explain: queryParams.Has("explain") && queryParams.Get("explain") != "false", } fields := []string{"title", "folder", "tags"} @@ -262,10 +265,10 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { switch len(types) { case 0: // When no type specified, search for dashboards - searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), "dashboards") + searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), dashboard.DASHBOARD_RESOURCE) // Currently a search query is across folders and dashboards - if searchRequest.Query != "" { - federate, err = asResourceKey(user.GetNamespace(), "folders") + if err == nil { + federate, err = asResourceKey(user.GetNamespace(), folderv0alpha1.RESOURCE) } case 1: searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), types[0]) @@ -301,8 +304,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { } // The facet term fields - facets, ok := queryParams["facet"] - if ok { + if facets, ok := queryParams["facet"]; ok { searchRequest.Facet = make(map[string]*resource.ResourceSearchRequest_Facet) for _, v := range facets { searchRequest.Facet[v] = &resource.ResourceSearchRequest_Facet{ @@ -313,8 +315,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { } // The tags filter - tags, ok := queryParams["tag"] - if ok { + if tags, ok := queryParams["tag"]; ok { searchRequest.Options.Fields = []*resource.Requirement{{ Key: "tags", Operator: "=", @@ -323,8 +324,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { } // The names filter - names, ok := queryParams["name"] - if ok { + if names, ok := queryParams["name"]; ok { if searchRequest.Options.Fields == nil { searchRequest.Options.Fields = []*resource.Requirement{} } @@ -366,30 +366,10 @@ func (s *SearchHandler) write(w http.ResponseWriter, obj any) { // Given a namespace and type convert it to a search key func asResourceKey(ns string, k string) (*resource.ResourceKey, error) { - if ns == "" { - return nil, apierrors.NewBadRequest("missing namespace") + key, err := resource.AsResourceKey(ns, k) + if err != nil { + return nil, apierrors.NewBadRequest(err.Error()) } - switch k { - case "folders", "folder": - return &resource.ResourceKey{ - Namespace: ns, - Group: "folder.grafana.app", - Resource: "folders", - }, nil - case "dashboards", "dashboard": - return &resource.ResourceKey{ - Namespace: ns, - Group: dashboardv0alpha1.GROUP, - Resource: "dashboards", - }, nil - // NOT really supported in the dashboard search UI, but useful for manual testing - case "playlist", "playlists": - return &resource.ResourceKey{ - Namespace: ns, - Group: "playlist.grafana.app", - Resource: "playlists", - }, nil - } - return nil, apierrors.NewBadRequest("unknown resource type") + return key, nil } diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index 83e46a6e6c2..d48a6e7a802 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -9,19 +9,17 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) -/* -Search Fallback was returning both Folders and Dashboards which resulted -in issues with rendering the Folder UI. Also, filters are not implemented -yet. For those reasons, we will be disabling Search Fallback for now func TestSearchFallback(t *testing.T) { t.Run("should hit legacy search handler on mode 0", func(t *testing.T) { mockClient := &MockClient{} @@ -179,7 +177,6 @@ func TestSearchFallback(t *testing.T) { } }) } -*/ func TestSearchHandler(t *testing.T) { // Create a mock client diff --git a/pkg/services/apiserver/client/client.go b/pkg/services/apiserver/client/client.go index 2f45e426bc3..9aaf573b7c8 100644 --- a/pkg/services/apiserver/client/client.go +++ b/pkg/services/apiserver/client/client.go @@ -179,10 +179,12 @@ func (h *k8sHandler) Search(ctx context.Context, orgID int64, in *resource.Resou in.Options = &resource.ListOptions{} } - in.Options.Key = &resource.ResourceKey{ - Namespace: h.GetNamespace(orgID), - Group: h.gvr.Group, - Resource: h.gvr.Resource, + if in.Options.Key == nil { + in.Options.Key = &resource.ResourceKey{ + Namespace: h.GetNamespace(orgID), + Group: h.gvr.Group, + Resource: h.gvr.Resource, + } } return h.searcher.Search(ctx, in) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index ac0a3504f9f..f6b732b426f 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -27,7 +27,9 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apis/dashboard" + dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" @@ -93,7 +95,7 @@ func ProvideDashboardServiceImpl( restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper, ) (*DashboardServiceImpl, error) { - k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore, userService) + k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore, userService) dashSvc := &DashboardServiceImpl{ cfg: cfg, @@ -1266,7 +1268,7 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb } foldersMap[hit.Folder] = f } - finalResults[i] = dashboards.DashboardSearchProjection{ + result := dashboards.DashboardSearchProjection{ ID: hit.Field.GetNestedInt64(search.DASHBOARD_LEGACY_ID), UID: hit.Name, OrgID: query.OrgId, @@ -1277,6 +1279,12 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb FolderTitle: f.Title, Tags: hit.Tags, } + + if hit.Resource == folderv0alpha1.RESOURCE { + result.IsFolder = true + } + + finalResults[i] = result } return finalResults, nil @@ -1644,7 +1652,7 @@ func (dr *DashboardServiceImpl) listDashboardsThroughK8s(ctx context.Context, or return dashboards, nil } -func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (*v0alpha1.SearchResults, error) { +func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (*dashboardv0alpha1.SearchResults, error) { request := &resource.ResourceSearchRequest{ Options: &resource.ListOptions{ Fields: []*resource.Requirement{}, @@ -1722,8 +1730,55 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex request.Options.Fields = append(request.Options.Fields, req...) } - if query.Limit > 0 { - request.Limit = query.Limit + if query.IsDeleted { + request.IsDeleted = query.IsDeleted + } + + if query.Limit < 1 { + query.Limit = 1000 + } + + if query.Page < 1 { + query.Page = 1 + } + + request.Limit = query.Limit + request.Page = query.Page + request.Offset = query.Page - 1 // bleve's offset is 0 indexed + + namespace := dr.k8sclient.GetNamespace(query.OrgId) + var err error + var federate *resource.ResourceKey + switch query.Type { + case "": + // When no type specified, search for dashboards + request.Options.Key, err = resource.AsResourceKey(namespace, dashboard.DASHBOARD_RESOURCE) + // Currently a search query is across folders and dashboards + if err == nil { + federate, err = resource.AsResourceKey(namespace, folderv0alpha1.RESOURCE) + } + case searchstore.TypeDashboard, searchstore.TypeAnnotation: + request.Options.Key, err = resource.AsResourceKey(namespace, dashboard.DASHBOARD_RESOURCE) + case searchstore.TypeFolder, searchstore.TypeAlertFolder: + request.Options.Key, err = resource.AsResourceKey(namespace, folderv0alpha1.RESOURCE) + default: + err = fmt.Errorf("bad type request") + } + + if err != nil { + return nil, err + } + + if federate != nil { + request.Federated = []*resource.ResourceKey{federate} + } + + // technically, there exists the ability to register multiple ways of sorting using the legacy database + // see RegisterSortOption in pkg/services/search/sorting.go + // however, it doesn't look like we are taking advantage of that. And since by default the legacy + // sql will sort by title ascending, we only really need to handle the "alpha-desc" case + if query.Sort.Name == "alpha-desc" { + request.SortBy = append(request.SortBy, &resource.ResourceSearchRequest_Sort{Field: resource.SEARCH_FIELD_TITLE, Desc: true}) } res, err := dr.k8sclient.Search(ctx, query.OrgId, request) @@ -1770,7 +1825,7 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex var mu sync.Mutex g, ctx := errgroup.WithContext(ctx) for _, h := range searchResults.Hits { - func(hit v0alpha1.DashboardHit) { + func(hit dashboardv0alpha1.DashboardHit) { g.Go(func() error { out, err := dr.k8sclient.Get(ctx, hit.Name, query.OrgId, v1.GetOptions{}) if err != nil { @@ -2000,7 +2055,7 @@ func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, names finalObj.Object["spec"] = obj finalObj.SetName(uid) finalObj.SetNamespace(namespace) - finalObj.SetGroupVersionKind(v0alpha1.DashboardResourceInfo.GroupVersionKind()) + finalObj.SetGroupVersionKind(dashboardv0alpha1.DashboardResourceInfo.GroupVersionKind()) meta, err := utils.MetaAccessor(&finalObj) if err != nil { diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 58cc555bfd3..5e9167c1f50 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -333,6 +333,7 @@ func TestGetDashboard(t *testing.T) { OrgID: 1, } ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", @@ -538,6 +539,7 @@ func TestGetProvisionedDashboardData(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled and get from relevant org", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", @@ -636,6 +638,7 @@ func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled and get from whatever org it is in", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", @@ -725,6 +728,7 @@ func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", @@ -812,6 +816,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled, delete across all orgs, but only delete file based provisioned dashboards", func(t *testing.T) { _, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid", OrgID: 1}).Return(nil).Once() fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid3", OrgID: 2}).Return(nil).Once() @@ -1044,6 +1049,7 @@ func TestGetDashboardsByPluginID(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(uidUnstructured, nil) k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool { @@ -1263,6 +1269,7 @@ func TestDeleteDashboard(t *testing.T) { t.Run("If UID is not passed in, it should retrieve that first", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() fakeStore.On("CleanupAfterDelete", mock.Anything, mock.Anything).Return(nil).Once() fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() @@ -1394,6 +1401,7 @@ func TestSearchDashboards(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ @@ -1513,6 +1521,7 @@ func TestGetDashboards(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, "uid1", mock.Anything, mock.Anything, mock.Anything).Return(uid1Unstructured, nil) k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything, mock.Anything).Return(uid2Unstructured, nil) k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil) @@ -1595,6 +1604,7 @@ func TestGetDashboardUIDByID(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ @@ -1892,6 +1902,7 @@ func TestCountInFolders(t *testing.T) { t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(dashs, nil).Once() result, err := service.CountInFolders(ctx, 1, []string{"folder1"}, &user.SignedInUser{}) require.NoError(t, err) @@ -1906,6 +1917,7 @@ func TestSearchDashboardsThroughK8sRaw(t *testing.T) { query := &dashboards.FindPersistedDashboardsQuery{ OrgId: 1, } + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ @@ -1972,6 +1984,7 @@ func TestSearchProvisionedDashboardsThroughK8sRaw(t *testing.T) { }, "spec": map[string]any{}, }} + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ Columns: []*resource.ResourceTableColumnDefinition{ diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go index 1b48756ea07..137cf29db12 100644 --- a/pkg/services/search/service.go +++ b/pkg/services/search/service.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/search/model" - "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -114,20 +113,6 @@ func (s *SearchService) SearchHandler(ctx context.Context, query *Query) (model. dashboardQuery.Sort = sortOpt } - // if folders are stored in unified storage, we need to use the folder service to query for folders - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) && (query.Type == searchstore.TypeFolder || query.Type == searchstore.TypeAlertFolder) { - hits, err := s.folderService.SearchFolders(ctx, folder.SearchFoldersQuery{ - OrgID: query.OrgId, - UIDs: query.FolderUIDs, - IDs: query.FolderIds, - Title: query.Title, - Limit: query.Limit, - SignedInUser: query.SignedInUser, - }) - - return sortedHits(hits), err - } - hits, err := s.dashboardService.SearchDashboards(ctx, &dashboardQuery) if err != nil { return nil, err diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 0b5cbb0b2db..450b1f37813 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -17,6 +17,7 @@ require ( github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible github.com/grafana/grafana-plugin-sdk-go v0.263.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d + github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/prometheus/client_golang v1.20.5 @@ -120,7 +121,6 @@ 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/pkg/apiserver v0.0.0-20250121113133-e747350fee2d // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/grafana/sqlds/v4 v4.1.3 // indirect diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index 9a41f4c4e5a..0e72212de6f 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -1814,7 +1814,9 @@ type ResourceSearchRequest struct { // the return fields (empty will return everything) Fields []string `protobuf:"bytes,8,rep,name=fields,proto3" json:"fields,omitempty"` // explain each result (added to the each row) - Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"` + Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"` + IsDeleted bool `protobuf:"varint,10,opt,name=is_deleted,json=isDeleted,proto3" json:"is_deleted,omitempty"` + Page int64 `protobuf:"varint,11,opt,name=page,proto3" json:"page,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1912,6 +1914,20 @@ func (x *ResourceSearchRequest) GetExplain() bool { return false } +func (x *ResourceSearchRequest) GetIsDeleted() bool { + if x != nil { + return x.IsDeleted + } + return false +} + +func (x *ResourceSearchRequest) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + type ResourceSearchResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Error details @@ -3810,7 +3826,7 @@ var file_resource_proto_rawDesc = []byte{ 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x22, 0xbb, 0x04, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, + 0x22, 0xee, 0x04, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, @@ -3833,321 +3849,324 @@ var file_resource_proto_rawDesc = []byte{ 0x61, 0x63, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, - 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x14, - 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, + 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x1a, 0x5f, 0x0a, - 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3b, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x61, - 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xea, - 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, - 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x69, 0x74, 0x73, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x69, 0x74, - 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x73, 0x74, - 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x41, 0x0a, - 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, - 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, - 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, - 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, - 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, - 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, - 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46, + 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, + 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, + 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, 0x43, + 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, + 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x74, 0x65, 0x72, - 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, - 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, - 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a, 0x46, 0x61, 0x63, - 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, - 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x78, 0x0a, 0x1c, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, - 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xda, 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, - 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, - 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, - 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, - 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x1a, 0x9f, 0x01, 0x0a, 0x04, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x2d, 0x0a, 0x06, 0x6f, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, - 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, - 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, - 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, - 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, - 0x6c, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x22, 0x51, 0x0a, 0x1d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, - 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x94, 0x02, 0x0a, 0x1e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, - 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, - 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, - 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x1a, 0x77, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, - 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, - 0x74, 0x6f, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x2e, 0x0a, 0x12, - 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, 0x0a, - 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, - 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, - 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, - 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, - 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x87, 0x02, 0x0a, 0x0d, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x41, 0x0a, 0x07, - 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, - 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x12, - 0x2e, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12, - 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, - 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, - 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xf1, 0x04, 0x0a, 0x1d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, - 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x04, 0x74, 0x79, - 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, - 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, - 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x41, 0x72, 0x72, 0x61, 0x79, 0x12, 0x20, 0x0a, - 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x52, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, - 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x72, 0x6f, - 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, - 0x69, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x1a, - 0xae, 0x01, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x23, - 0x0a, 0x0d, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x74, 0x65, 0x78, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x66, 0x72, 0x65, 0x65, 0x54, 0x65, 0x78, 0x74, - 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, 0x62, 0x6c, 0x65, - 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x74, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x07, 0x6e, 0x6f, 0x74, 0x4e, 0x75, 0x6c, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x64, - 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x22, 0x95, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x10, - 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0b, 0x0a, - 0x07, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, - 0x54, 0x33, 0x32, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x10, 0x04, - 0x12, 0x09, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x44, - 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x06, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, 0x45, 0x10, - 0x07, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x08, - 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x09, 0x12, 0x0a, 0x0a, 0x06, - 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0a, 0x22, 0x94, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x12, 0x27, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, - 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, - 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, - 0x64, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x69, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x22, 0xd3, 0x01, 0x0a, 0x0e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, - 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1c, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x52, 0x50, 0x43, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, - 0x48, 0x54, 0x54, 0x50, 0x10, 0x01, 0x22, 0xc1, 0x01, 0x0a, 0x0f, 0x50, 0x75, 0x74, 0x42, 0x6c, - 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x73, - 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, - 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x22, 0x98, 0x01, 0x0a, 0x0e, 0x47, - 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, 0x6d, - 0x75, 0x73, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6d, 0x75, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, - 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, 0x89, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, - 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, - 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, - 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xad, 0x03, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, - 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, - 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0xa9, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, - 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x32, 0xe8, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, - 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x6b, 0x0a, 0x16, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, - 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, - 0x12, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, + 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x61, + 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, + 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, + 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, + 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a, + 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, + 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x78, + 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, + 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, + 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xda, 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, - 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, - 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x26, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, - 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x8b, 0x01, - 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x62, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x50, - 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, - 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x47, - 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, - 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, 0x0a, 0x0b, 0x44, - 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, - 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x69, 0x74, + 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, + 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, + 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x1a, 0x9f, 0x01, 0x0a, 0x04, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x2d, 0x0a, 0x06, + 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, + 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, + 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, + 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, + 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x51, 0x0a, 0x1d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x94, 0x02, 0x0a, 0x1e, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x77, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, + 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, + 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, - 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, - 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, + 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, + 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, + 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, + 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x87, 0x02, + 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, + 0x41, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, + 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, + 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x52, 0x04, 0x72, 0x6f, + 0x77, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, + 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, + 0x6e, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74, + 0x65, 0x6d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xf1, 0x04, 0x0a, 0x1d, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, + 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, + 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x61, 0x72, 0x72, 0x61, + 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x41, 0x72, 0x72, 0x61, 0x79, + 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x52, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, + 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70, + 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, + 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, + 0x74, 0x79, 0x1a, 0xae, 0x01, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, + 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x74, + 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x66, 0x72, 0x65, 0x65, 0x54, + 0x65, 0x78, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, 0x62, 0x6c, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, + 0x62, 0x6c, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x74, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6e, 0x6f, 0x74, 0x4e, 0x75, 0x6c, 0x6c, 0x12, 0x23, + 0x0a, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x22, 0x95, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, + 0x12, 0x0b, 0x0a, 0x07, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x02, 0x12, 0x09, 0x0a, + 0x05, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x36, + 0x34, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, 0x05, 0x12, 0x0a, + 0x0a, 0x06, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x06, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, + 0x54, 0x45, 0x10, 0x07, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x49, 0x4d, + 0x45, 0x10, 0x08, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x09, 0x12, + 0x0a, 0x0a, 0x06, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0a, 0x22, 0x94, 0x01, 0x0a, 0x10, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, + 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x22, 0x64, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, + 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x69, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x22, 0xd3, 0x01, 0x0a, 0x0e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x6d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1c, 0x0a, 0x06, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x52, 0x50, 0x43, 0x10, 0x00, 0x12, + 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x01, 0x22, 0xc1, 0x01, 0x0a, 0x0f, 0x50, 0x75, + 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, + 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, + 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, + 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x22, 0x98, 0x01, + 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x28, + 0x0a, 0x10, 0x6d, 0x75, 0x73, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6d, 0x75, 0x73, 0x74, 0x50, 0x72, + 0x6f, 0x78, 0x79, 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, 0x89, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, + 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, + 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, + 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xad, 0x03, 0x0a, 0x0d, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, + 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x52, 0x65, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, + 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, + 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0xa9, 0x01, 0x0a, 0x0d, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, + 0x74, 0x61, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xe8, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x6b, 0x0a, 0x16, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, + 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x32, 0x8b, 0x01, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x62, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e, + 0x0a, 0x07, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, + 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, + 0x0a, 0x07, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, + 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, + 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, + 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, + 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index ae7d067ff5a..2624e36dc94 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -225,9 +225,9 @@ enum ResourceVersionMatch { } message ListRequest { - enum Source { + enum Source { STORE = 0; // the standard place - HISTORY = 1; + HISTORY = 1; TRASH = 2; } @@ -248,7 +248,7 @@ message ListRequest { ListOptions options = 5; // Select values from history or trash - Source source = 6; + Source source = 6; } message ListResponse { @@ -329,7 +329,7 @@ message ResourceStatsRequest { // NOTE, this query may need to federate across a few storage instances repeated string kinds = 2; - // Limit the stats within a folder (not recursive!) + // Limit the stats within a folder (not recursive!) string folder = 3; } @@ -356,7 +356,7 @@ message ResourceSearchRequest { string field = 1; bool desc = 2; // defaults to ascending } - + message Facet { string field = 1; int64 limit = 2; @@ -370,8 +370,8 @@ message ResourceSearchRequest { // To search additional resource types, add additional keys to this list // NOTE: queries will only support federation across kinds with common fields - repeated ResourceKey federated = 2; - + repeated ResourceKey federated = 2; + // When a query exists, it is parsed and used to influence // query string for chosen implementation (currently just bleve) // The score is only relevant when a query exists @@ -394,6 +394,10 @@ message ResourceSearchRequest { // explain each result (added to the each row) bool explain = 9; + + bool is_deleted = 10; + + int64 page = 11; } message ResourceSearchResponse { @@ -406,7 +410,7 @@ message ResourceSearchResponse { // Top term stats repeated TermFacet terms = 4; // numeric range - // date range facets + // date range facets } message TermFacet { @@ -444,7 +448,7 @@ message ListRepositoryObjectsRequest { // Namespace (tenant) string namespace = 2; - + // The name of the repository string name = 3; } @@ -456,16 +460,16 @@ message ListRepositoryObjectsResponse { // Hash for the resource string path = 2; - + // Verification hash from the origin string hash = 3; - + // Change time from the origin int64 time = 5; // Title inside the payload string title = 6; - + // The name of the folder in metadata string folder = 7; } @@ -484,7 +488,7 @@ message ListRepositoryObjectsResponse { message CountRepositoryObjectsRequest { // Namespace (tenant) string namespace = 1; - + // The name of the repository // empty to count across all repositories string name = 2; @@ -559,16 +563,16 @@ message ResourceTableColumnDefinition { STRING = 1; BOOLEAN = 2; INT32 = 3; - INT64 = 4; - FLOAT = 5; - DOUBLE = 6; - DATE = 7; - DATE_TIME = 8; - BINARY = 9; + INT64 = 4; + FLOAT = 5; + DOUBLE = 6; + DATE = 7; + DATE_TIME = 8; + BINARY = 9; OBJECT = 10; // map[string]any } - // These values are not part of standard k8s format + // These values are not part of standard k8s format // however these are useful when indexing and analyzing results message Properties { // All values in this columns should be unique @@ -618,7 +622,7 @@ message ResourceTableRow { // The resource version for the given values int64 resource_version = 2; - // Cells will be as wide as the column definitions array + // Cells will be as wide as the column definitions array // Numeric values will be encoded using big endian bytes // All arrays will be JSON encoded repeated bytes cells = 3; @@ -638,7 +642,7 @@ message ResourceTableRow { message RestoreRequest { // Full key must be set ResourceKey key = 1; - + // The resource version to restore int64 resource_version = 2; } @@ -759,7 +763,7 @@ service ResourceIndex { rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse); } -// Query repository info from the search index. +// Query repository info from the search index. // Results access control is based on access to the repository *not* the items service RepositoryIndex { // Describe how many resources of each type exist within a repository diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index f0d18960dc5..dc990d499eb 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -10,6 +10,8 @@ import ( "sync" "time" + dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/hashicorp/golang-lru/v2/expirable" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -651,3 +653,34 @@ func (s *builderCache) get(ctx context.Context, key NamespacedResource) (Documen } return s.defaultBuilder, nil } + +// AsResourceKey converts the given namespace and type to a search key +func AsResourceKey(ns string, t string) (*ResourceKey, error) { + if ns == "" { + return nil, fmt.Errorf("missing namespace") + } + switch t { + case "folders", "folder": + return &ResourceKey{ + Namespace: ns, + Group: folderv0alpha1.GROUP, + Resource: folderv0alpha1.RESOURCE, + }, nil + case "dashboards", "dashboard": + return &ResourceKey{ + Namespace: ns, + Group: dashboardv0alpha1.GROUP, + Resource: dashboardv0alpha1.RESOURCE, + }, nil + + // NOT really supported in the dashboard search UI, but useful for manual testing + case "playlist", "playlists": + return &ResourceKey{ + Namespace: ns, + Group: "playlist.grafana.app", + Resource: "playlists", + }, nil + } + + return nil, fmt.Errorf("unknown resource type") +} diff --git a/pkg/storage/unified/resource/search_client.go b/pkg/storage/unified/resource/search_client.go index a9c9c95ef9d..43e0faa7b1e 100644 --- a/pkg/storage/unified/resource/search_client.go +++ b/pkg/storage/unified/resource/search_client.go @@ -1,24 +1,20 @@ package resource import ( + "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/setting" ) -// Search Fallback was returning both Folders and Dashboards which resulted -// in issues with rendering the Folder UI. Also, filters are not implemented -// yet. For those reasons, we will be disabling Search Fallback for now func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient ResourceIndexClient, legacyClient ResourceIndexClient) ResourceIndexClient { - // config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey] - // if !ok { - // return legacyClient - // } + config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey] + if !ok { + return legacyClient + } - // switch config.DualWriterMode { - // case rest.Mode0, rest.Mode1, rest.Mode2: - // return legacyClient - // default: - // return unifiedClient - // } - - return unifiedClient + switch config.DualWriterMode { + case rest.Mode0, rest.Mode1, rest.Mode2: + return legacyClient + default: + return unifiedClient + } } From 677060862c2ebe8001f55c3e5bdb8f1583632609 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:34:52 +0100 Subject: [PATCH 400/894] Combobox: Fix list not being virtualized initially in some cases (#100188) * Combobox: Set arbitrary initial max size * Remove ? * Set initial values to 0 --- .../src/components/Combobox/useComboboxFloat.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts index 83eb2631a62..f5803afdf2e 100644 --- a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts +++ b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts @@ -22,7 +22,10 @@ export const useComboboxFloat = (items: Array>, const inputRef = useRef(null); const floatingRef = useRef(null); const scrollRef = useRef(null); - const [popoverMaxSize, setPopoverMaxSize] = useState<{ width: number; height: number } | undefined>(undefined); + const [popoverMaxSize, setPopoverMaxSize] = useState<{ width: number; height: number }>({ + width: 0, + height: 0, + }); // set initial values to prevent infinite size, briefly removing the list virtualization const scrollbarWidth = useMemo(() => getScrollbarWidth(), []); @@ -72,10 +75,10 @@ export const useComboboxFloat = (items: Array>, const floatStyles = { ...floatingStyles, width: longestItemWidth, - maxWidth: popoverMaxSize?.width, + maxWidth: popoverMaxSize.width, minWidth: inputRef.current?.offsetWidth, - maxHeight: popoverMaxSize?.height, + maxHeight: popoverMaxSize.height, }; return { inputRef, floatingRef, scrollRef, floatStyles }; From 2b3ccfe5b95dd93167cf4c732551f3a0c8b0aaea Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Thu, 6 Feb 2025 18:37:28 +0200 Subject: [PATCH 401/894] DynamicDashboards: Revert unnecessary empty page on new layouts (#100218) Revert unnecessary empty page on new layouts --- .../ResponsiveGridLayoutManager.tsx | 20 +------------------ .../scene/layout-rows/RowsLayoutManager.tsx | 10 ---------- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index f936d8617eb..91fcbec0a48 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -1,16 +1,8 @@ import { SelectableValue } from '@grafana/data'; -import { - SceneComponentProps, - SceneCSSGridLayout, - SceneObjectBase, - SceneObjectState, - useSceneObjectState, - VizPanel, -} from '@grafana/scenes'; +import { SceneComponentProps, SceneCSSGridLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; import { Select } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; -import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty'; import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelId } from '../../utils/utils'; @@ -148,16 +140,6 @@ export class ResponsiveGridLayoutManager public activateRepeaters(): void {} public static Component = ({ model }: SceneComponentProps) => { - const { children } = useSceneObjectState(model.state.layout, { shouldActivateOrKeepAlive: true }); - const dashboard = getDashboardSceneFor(model); - - // If we are top level layout and have no children, show empty state - if (model.parent === dashboard && children.length === 0) { - return ( - - ); - } - return ; }; } 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 1aafc0adeb2..dc9154d3b2b 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -12,10 +12,8 @@ import { } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; -import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty'; import { isClonedKey } from '../../utils/clone'; -import { getDashboardSceneFor } from '../../utils/utils'; import { DashboardScene } from '../DashboardScene'; import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; @@ -197,14 +195,6 @@ export class RowsLayoutManager extends SceneObjectBase i public static Component = ({ model }: SceneComponentProps) => { const { rows } = model.useState(); const styles = useStyles2(getStyles); - const dashboard = getDashboardSceneFor(model); - - // If we are top level layout and have no children, show empty state - if (model.parent === dashboard && rows.length === 0) { - return ( - - ); - } return (
From a9ce930634dc707efe33f7225ce21f57787ab623 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 6 Feb 2025 18:09:43 +0100 Subject: [PATCH 402/894] Alerting: Promote alertingSaveStateCompressed flag to public preview (#99935) --- .../set-up/performance-limitations/index.md | 8 +++++++- .../configure-grafana/feature-toggles/index.md | 2 +- pkg/services/featuremgmt/registry.go | 3 ++- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.json | 16 ++++++++++------ 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/sources/alerting/set-up/performance-limitations/index.md b/docs/sources/alerting/set-up/performance-limitations/index.md index 69becbad450..b8524e4c8a7 100644 --- a/docs/sources/alerting/set-up/performance-limitations/index.md +++ b/docs/sources/alerting/set-up/performance-limitations/index.md @@ -61,7 +61,13 @@ For more information, refer to [this GitHub issue](https://github.com/grafana/gr If you have a high number of alert instances, it can happen that the load on the database gets very high, as each state transition of an alert instance is saved in the database. -This can be prevented by writing to the database periodically. For this the feature flag `alertingSaveStatePeriodic` needs +### Compressed alert state + +When the `alertingSaveStateCompressed` feature toggle is enabled, Grafana saves the alert rule state in a compressed form, reducing database overhead for alerts with many instances. + +### Save state periodically + +High load can be also prevented by writing to the database periodically. For this the feature flag `alertingSaveStatePeriodic` needs to be enabled. By default, it saves the states every 5 minutes to the database and on each shutdown. The periodic interval can also be configured using the `state_periodic_save_interval` configuration flag. During this process, Grafana deletes all existing alert instances from the database and then writes the entire current set of instances back in batches in a single transacton. 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 9cd7fe7e1a7..13646ef8948 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -114,6 +114,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `canvasPanelPanZoom` | Allow pan and zoom in canvas panel | | `regressionTransformation` | Enables regression analysis transformation | | `onPremToCloudMigrations` | Enable the Grafana Migration Assistant, which helps you easily migrate on-prem resources, such as dashboards, folders, and data source configurations, to your Grafana Cloud stack. | +| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage | | `ssoSettingsSAML` | Use the new SSO Settings API to configure the SAML connector | | `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars | | `ssoSettingsLDAP` | Use the new SSO Settings API to configure LDAP | @@ -188,7 +189,6 @@ Experimental features might be changed or removed without prior notice. | `tableSharedCrosshair` | Enables shared crosshair in table panel | | `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | | `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | -| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage | | `scopeApi` | In-development feature flag for the scope api using the app platform. | | `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | | `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5589a2cdb6b..ac2eaffc940 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1023,9 +1023,10 @@ var ( { Name: "alertingSaveStateCompressed", Description: "Enables the compressed protobuf-based alert state storage", - Stage: FeatureStageExperimental, + Stage: FeatureStagePublicPreview, FrontendOnly: false, Owner: grafanaAlertingSquad, + Expression: "false", }, { Name: "scopeApi", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 5ee9c0c2f1f..e88c9c3d176 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -134,7 +134,7 @@ newFolderPicker,experimental,@grafana/grafana-frontend-platform,false,false,true jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false onPremToCloudMigrations,preview,@grafana/grafana-operator-experience-squad,false,false,false alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false -alertingSaveStateCompressed,experimental,@grafana/alerting-squad,false,false,false +alertingSaveStateCompressed,preview,@grafana/alerting-squad,false,false,false scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false promQLScope,GA,@grafana/oss-big-tent,false,false,false logQLScope,privatePreview,@grafana/observability-logs,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 970b8edf66f..607d0581270 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -373,23 +373,27 @@ { "metadata": { "name": "alertingSaveStateCompressed", - "resourceVersion": "1737472824047", + "resourceVersion": "1738604435531", "creationTimestamp": "2025-01-17T18:17:20Z", "annotations": { - "grafana.app/updatedTimestamp": "2025-01-21 15:20:24.047499 +0000 UTC" + "grafana.app/updatedTimestamp": "2025-02-03 17:40:35.53174 +0000 UTC" } }, "spec": { "description": "Enables the compressed protobuf-based alert state storage", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "stage": "preview", + "codeowner": "@grafana/alerting-squad", + "expression": "false" } }, { "metadata": { "name": "alertingSaveStatePeriodic", - "resourceVersion": "1718727528075", - "creationTimestamp": "2024-01-23T16:03:30Z" + "resourceVersion": "1738604155684", + "creationTimestamp": "2024-01-23T16:03:30Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-02-03 17:35:55.684572 +0000 UTC" + } }, "spec": { "description": "Writes the state periodically to the database, asynchronous to rule evaluation", From f4426e22bfc419b21d7c45dba9911d835c7999ce Mon Sep 17 00:00:00 2001 From: Sam Jewell <2903904+samjewell@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:34:35 +0000 Subject: [PATCH 403/894] SQL Expressions: Bump go-mysql-server dependency to `main` (#100222) Bump go-mysql-server dependency to `main` This is so we receive a few bug fixes - fixes to these issues: - https://github.com/dolthub/dolt/issues/8807 - https://github.com/dolthub/dolt/issues/8735 - https://github.com/dolthub/dolt/issues/8724 --- go.mod | 4 +- go.sum | 8 +- go.work.sum | 123 +++++++++++++++++++++++++++- pkg/storage/unified/apistore/go.mod | 4 +- pkg/storage/unified/apistore/go.sum | 8 +- pkg/storage/unified/resource/go.sum | 8 +- 6 files changed, 137 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 079a83598d0..800bd9c565f 100644 --- a/go.mod +++ b/go.mod @@ -41,8 +41,8 @@ require ( github.com/centrifugal/centrifuge v0.33.3 // @grafana/grafana-app-platform-squad github.com/crewjam/saml v0.4.13 // @grafana/identity-access-team github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/grafana-backend-group - github.com/dolthub/go-mysql-server v0.19.0 // @grafana/grafana-datasources-core-services - github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 // @grafana/grafana-datasources-core-services + github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7 // @grafana/grafana-datasources-core-services + github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa // @grafana/grafana-datasources-core-services github.com/fatih/color v1.17.0 // @grafana/grafana-backend-group github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage diff --git a/go.sum b/go.sum index 5c2c8b5537c..294f7fd4289 100644 --- a/go.sum +++ b/go.sum @@ -1061,14 +1061,14 @@ github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1G github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM= github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA= -github.com/dolthub/go-mysql-server v0.19.0 h1:NdcXyGt9v7m4sQOahU+ss++iyPy4Q3viuVvbnn3rUTQ= -github.com/dolthub/go-mysql-server v0.19.0/go.mod h1:elfIatfq2fkU5lqTBrTcpL0RcHZOgYPE8EzBD7yQFiY= +github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7 h1:b9ygz2zMlMv2AcJ9mLL0b60tJ9Qd+bzfsAY5ZDTu9VY= +github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7/go.mod h1:jYEJ8tNkA7K3k39X8iMqaX3MSMmViRgh222JSLHDgVc= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= -github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 h1:nzBnC0Rt1gFtscJEz4veYd/mazZEdbdmed+tujdaKOo= -github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= +github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa h1:kyoPzxViSXAyqfO0Mab7Qo1UogFIrxZKKyBU6kBOl+E= +github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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= diff --git a/go.work.sum b/go.work.sum index 9fc55608511..ff30236f289 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1044,6 +1044,7 @@ github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4s github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/DataDog/sketches-go v1.4.6 h1:acd5fb+QdUzGrosfNLwrIhqyrbMORpvBy7mE+vHlT3I= github.com/DataDog/sketches-go v1.4.6/go.mod h1:7Y8GN8Jf66DLyDhc94zuWA3uHEt/7ttt8jHOBWWrSOg= @@ -1062,6 +1063,7 @@ github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSg 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= github.com/IBM/ibm-cos-sdk-go v1.11.0/go.mod h1:FnWOym0CvrPM0nHoXvceClOEvGVXecPpmVIO5RFjlFk= +github.com/IBM/sarama v1.43.1/go.mod h1:GG5q1RURtDNPz8xxJs3mgX6Ytak8Z9eLhAkJPObe2xE= github.com/IBM/sarama v1.43.2 h1:HABeEqRUh32z8yzY2hGB/j8mHSzC/HA9zlEjqFNCzSw= github.com/IBM/sarama v1.43.2/go.mod h1:Kyo4WkF24Z+1nz7xeVUFWIuKVV8RS3wM8mkvPKMdXFQ= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= @@ -1141,14 +1143,20 @@ github.com/apache/arrow/go/v14 v14.0.2 h1:N8OkaJEOfI3mEZt07BIkvo4sC6XDbL+48MBPWO github.com/apache/arrow/go/v14 v14.0.2/go.mod h1:u3fgh3EdgN/YQ8cVQRguVW3R+seMybFg8QBQ5LU+eBY= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.17.0/go.mod h1:OLxhMRJxomX+1I/KUw03qoV3mMz16BwaKI+d4fPBx7Q= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.51.25/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1 h1:w/fPGB0t5rWwA43mux4e9ozFSH5zF1moQemlA131PWc= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= @@ -1193,8 +1201,11 @@ 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/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= 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= @@ -1225,6 +1236,7 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyY github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cncf/xds/go v0.0.0-20230428030218-4003588d1b74/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -1232,6 +1244,7 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c h1:2zRrJWIt/f9c9HhNHAgrRgq0San5gRRUJTBXLkchal0= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= @@ -1252,8 +1265,10 @@ github.com/coreos/etcd v3.3.27+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf h1:GOPo6vn/vTN+3IwZBvXX0y5doJfSC7My0cdzelyOCsQ= github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/couchbase/ghistogram v0.1.0 h1:b95QcQTCzjTUocDXp/uMgSNQi8oj1tGwnJ4bODWZnps= @@ -1262,6 +1277,7 @@ github.com/couchbase/moss v0.2.0 h1:VCYrMzFwEryyhRSeI+/b3tRBSeTpi/8gn5Kf6dxqn+o= github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -1300,6 +1316,8 @@ github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWE github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA= +github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b h1:Yqiad0+sloMPdd/0Fg22actpFx0dekpzt1xJmVNVkU0= github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I= github.com/digitalocean/godo v1.113.0/go.mod h1:Z2mTP848Vi3IXXl5YbPekUgr4j4tOePomA+OE1Ag98w= @@ -1328,6 +1346,7 @@ github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8 github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f h1:/jEs7lulqVO2u1+XI5rW4oFwIIusxuDOVKD9PAzlW2E= github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f/go.mod h1:nDRkX7PHq+p39AD5/usv3KZMerxZTYU/9rfLS5IDspU= github.com/drone/signal v1.0.0 h1:NrnM2M/4yAuU/tXs6RP1a1ZfxnaHwYkd0kJurA1p6uI= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/eapache/go-resiliency v1.6.0 h1:CqGDTLtpwuWKn6Nj3uNUdflaq+/kIPsg0gfNzHton30= @@ -1347,6 +1366,7 @@ github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQ github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 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.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= @@ -1370,6 +1390,8 @@ github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c h1:yKN46XJHYC github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c/go.mod h1:L92h+dgwElEyUuShEwjbiHjseW410WIcNz+Bjutc8YQ= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 h1:cZqz+yOJ/R64LcKjNQOdARott/jP7BnUQ9Ah7KaZCvw= github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= @@ -1399,6 +1421,7 @@ github.com/go-fonts/liberation v0.3.2/go.mod h1:N0QsDLVUQPy3UYg9XAc3Uh3UDMp2Z7M1 github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= @@ -1451,11 +1474,13 @@ github.com/gocraft/dbr/v2 v2.7.2/go.mod h1:5bCqyIXO5fYn3jEp/L06QF4K1siFdhxChMjdN github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= @@ -1480,6 +1505,7 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/pprof v0.0.0-20240416155748-26353dc0451f/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= @@ -1512,13 +1538,14 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20250115195200-209e052dba64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 h1:J3PIK9OL3ZHPypYHlcK+nBREwYL3ROZ3fJyNMsTYlpk= -github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:V63rh3udd7sqXJeaG+nGUmViwVnM/bY6t8U9Tols2GU= github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= @@ -1527,6 +1554,8 @@ github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/ github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak= github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90= github.com/grafana/grafana-app-sdk v0.29.0/go.mod h1:XLt308EmK6kvqPlzjUyXxbwZKEk2vur/eiypUNDay5I= +github.com/grafana/grafana-azure-sdk-go/v2 v2.1.5/go.mod h1:i0uiuu9/sMFBJnpFbjvviH0KOZzdWkti9Q9Ck1HkFWM= +github.com/grafana/grafana-plugin-sdk-go v0.262.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= github.com/grafana/grafana/apps/advisor v0.0.0-20250121115006-c1eac9f9973f/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= github.com/grafana/grafana/pkg/promlib v0.0.7/go.mod h1:rnwJXCA2xRwb7F27NB35iO/JsLL/H/+eVXECk/hrEhQ= github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= @@ -1535,6 +1564,7 @@ github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= @@ -1546,18 +1576,26 @@ github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1 github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hamba/avro/v2 v2.27.0 h1:IAM4lQ0VzUIKBuo4qlAiLKfqALSrFC+zi1iseTtbBKU= github.com/hamba/avro/v2 v2.27.0/go.mod h1:jN209lopfllfrz7IGoZErlDz+AyUJ3vrBePQFZwYf5I= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/api v1.14.0/go.mod h1:bcaw5CSZ7NE9qfOfKCI1xb7ZKjzu/MyvQkCLTfqLqxQ= github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.10.0/go.mod h1:yPkX5Q6CsxTFMjQQDJwzeNmUUF5NUGGbrDsv9wTb8cw= github.com/hashicorp/consul/sdk v0.16.0/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack/v2 v2.1.1 h1:xQEY9yB2wnHitoSzk/B9UjXWRQ67QKu5AOm8aFp8N3I= github.com/hashicorp/go-msgpack/v2 v2.1.1/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= @@ -1566,6 +1604,8 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.4.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/nomad/api v0.0.0-20240418183417-ea5f2f6748c7 h1:pjE59CS2C9Bg+Xby0ROrnZSSBWtKwx3Sf9gqsrvIFSA= github.com/hashicorp/nomad/api v0.0.0-20240418183417-ea5f2f6748c7/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE= github.com/hashicorp/raft v1.7.0 h1:4u24Qn6lQ6uwziM++UgsyiT64Q8GyRn43CV41qPiz1o= @@ -1573,6 +1613,7 @@ github.com/hashicorp/raft v1.7.0/go.mod h1:N1sKh6Vn47mrWvEArQgILTyng8GoDRNYlgKyK github.com/hashicorp/raft-wal v0.4.1 h1:aU8XZ6x8R9BAIB/83Z1dTDtXvDVmv9YVYeXxd/1QBSA= github.com/hashicorp/raft-wal v0.4.1/go.mod h1:A6vP5o8hGOs1LHfC1Okh9xPwWDcmb6Vvuz/QyqUXlOE= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.10.0/go.mod h1:bXN03oZc5xlH46k/K1qTrpXb9ERKyY1/i/N5mxvgrZw= github.com/heroku/x v0.0.61 h1:yfoAAtnFWSFZj+UlS+RZL/h8QYEp1R4wHVEg0G+Hwh4= github.com/heroku/x v0.0.61/go.mod h1:C7xYbpMdond+s6L5VpniDUSVPRwm3kZum1o7XiD5ZHk= github.com/hetznercloud/hcloud-go/v2 v2.7.2 h1:UlE7n1GQZacCfyjv9tDVUN7HZfOXErPIfM/M039u9A0= @@ -1580,6 +1621,7 @@ github.com/hetznercloud/hcloud-go/v2 v2.7.2/go.mod h1:49tIV+pXRJTUC7fbFZ03s45LKq github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/hudl/fargo v1.4.0 h1:ZDDILMbB37UlAVLlWcJ2Iz1XuahZZTDZfdCKeclfq2s= github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= @@ -1592,6 +1634,7 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs= github.com/influxdata/influxdb v1.7.7 h1:UvNzAPfBrKMENVbQ4mr4ccA9sW+W1Ihl0Yh1s0BiVAg= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/tdigest v0.0.2-0.20210216194612-fc98d27c9e8b h1:i44CesU68ZBRvtCjBi3QSosCIKrjmMbYlQMFAwVLds4= @@ -1622,12 +1665,14 @@ github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= github.com/jmattheis/goverter v1.4.0 h1:SrboBYMpGkj1XSgFhWwqzdP024zIa1+58YzUm+0jcBE= github.com/jmattheis/goverter v1.4.0/go.mod h1:iVIl/4qItWjWj2g3vjouGoYensJbRqDHpzlEVMHHFeY= +github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jon-whit/go-grpc-prometheus v1.4.0 h1:/wmpGDJcLXuEjXryWhVYEGt9YBRhtLwFEN7T+Flr8sw= github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg5ax6YQEe1I0f6vtBuao= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a h1:sfe532Ipn7GX0V6mHdynBk393rDmqgI0QmjLK7ct7TU= github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a/go.mod h1:dNKs71rs2VJGBAmttu7fouEsRQlRjxy0p1Sx+T5wbpY= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= @@ -1660,7 +1705,9 @@ github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHz github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw2e+eeNT/SbGySq8ajECXJ9e4fPoLhY= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= @@ -1696,6 +1743,9 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b h1:11UHH39z1RhZ5dc4y4r/4koJo6IYFgTRMe/LlwRTEw0= github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b/go.mod h1:WZxr2/6a/Ar9bMDc2rN/LJrE/hF6bXE4LPyDSIxwAfg= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/linode/linodego v1.32.0 h1:OmZzB3iON6uu84VtLFf64uKmAQqJJarvmsVguroioPI= github.com/linode/linodego v1.32.0/go.mod h1:y8GDP9uLVH4jTB9qyrgw79qfKdYJmNCGUOJmfuiOcmI= github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= @@ -1706,12 +1756,16 @@ github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbi github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJOMrYnQZJu7OB7ANSF4MYri2fTEGIsRLz6LwI4xE= github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/matryer/moq v0.3.3 h1:pScMH9VyrdT4S93yiLpVyU8rCDqGQr24uOyBxmktG5Q= github.com/matryer/moq v0.3.3/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= @@ -1739,6 +1793,7 @@ github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mithrandie/readline-csvq v1.3.0 h1:VTJEOGouJ8j27jJCD4kBBbNTxM0OdBvE1aY1tMhlqE8= github.com/mithrandie/readline-csvq v1.3.0/go.mod h1:FKyYqDgf/G4SNov7SMFXRWO6LQLXIOeTog/NB97FZl0= @@ -1756,15 +1811,24 @@ github.com/mostynb/go-grpc-compression v1.2.3 h1:42/BKWMy0KEJGSdWvzqIyOZ95YcR9mL github.com/mostynb/go-grpc-compression v1.2.3/go.mod h1:AghIxF3P57umzqM9yz795+y1Vjs47Km/Y2FE6ouQ7Lg= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU= github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= github.com/nats-io/jwt/v2 v2.0.3 h1:i/O6cmIsjpcQyWDYNcq2JyZ3/VTF8SJ4JWluI5OhpvI= github.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY= +github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg= github.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g= +github.com/nats-io/nats-server/v2 v2.8.4/go.mod h1:8zZa+Al3WsESfmgSs98Fi06dRWLH5Bnq90m5bKD/eT4= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= +github.com/nats-io/nats.go v1.15.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= github.com/nats-io/nats.go v1.34.0 h1:fnxnPCNiwIG5w08rlMcEKTUw4AV/nKyGCOJE8TdhSPk= github.com/nats-io/nats.go v1.34.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= @@ -1779,6 +1843,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= @@ -1797,6 +1863,7 @@ github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3Ro github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= @@ -1816,6 +1883,7 @@ github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRah github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.102.0 h1:R70PpK14trQfL/Vj5oAiGRqX09s2gOWuf6t1Ae5fevQ= @@ -1863,6 +1931,12 @@ github.com/openfga/api/proto v0.0.0-20240905181937-3583905f61a6/go.mod h1:gil5LB github.com/openfga/api/proto v0.0.0-20240906203051-102620ef2a66/go.mod h1:gil5LBD8tSdFQbUkCQdnXsoeU9kDJdJgbGdHkgJfcd0= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20240926131254-992b301a003f/go.mod h1:ll/hN6kS4EE6B/7J/PbZqac9Nuv7ZHpI+Jfh36JLrbs= github.com/openfga/openfga v1.6.2/go.mod h1:jzbEpheazf6MFjtanQt1rpxexSRzfa9057F7JlkMv2I= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= @@ -1872,6 +1946,7 @@ github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnY github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/parquet-go/parquet-go v0.23.0 h1:dyEU5oiHCtbASyItMCD2tXtT2nPmoPbKpqf0+nnGrmk= github.com/parquet-go/parquet-go v0.23.0/go.mod h1:MnwbUcFHU6uBYMymKAlPPAw9yh3kE1wWl6Gl1uLdkNk= github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= @@ -1883,6 +1958,7 @@ github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2 github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ulk9xVsepYy9ZY= github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= @@ -1918,6 +1994,7 @@ github.com/prometheus/prometheus v0.52.0 h1:f7kHJgr7+zShpWdTCeKqbCWR7nKTScgLYQwR github.com/prometheus/prometheus v0.52.0/go.mod h1:3z74cVsmVH0iXOR5QBjB7Pa6A0KJeEAK5A6UsmAFb1g= github.com/prometheus/statsd_exporter v0.26.0 h1:SQl3M6suC6NWQYEzOvIv+EF6dAMYEqIuZy+o4H9F5Ig= github.com/prometheus/statsd_exporter v0.26.0/go.mod h1:GXFLADOmBTVDrHc7b04nX8ooq3azG61pnECNqT7O5DM= +github.com/rabbitmq/amqp091-go v1.2.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM= github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= @@ -1936,6 +2013,7 @@ github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF github.com/sagikazarmark/crypt v0.19.0 h1:WMyLTjHBo64UvNcWqpzY3pbZTYgnemZU8FBZigKc42E= github.com/sagikazarmark/crypt v0.19.0/go.mod h1:c6vimRziqqERhtSe0MhIvzE1w54FrCHtrXb5NH/ja78= github.com/samuel/go-zookeeper v0.0.0-20190810000440-0ceca61e4d75 h1:cA+Ubq9qEVIQhIWvP2kNuSZ2CmnfBJFSRq+kO1pu2cc= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.26 h1:F+GIVtGqCFxPxO46ujf8cEOP574MBoRm3gNbPXECbxs= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.26/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= @@ -1953,21 +2031,26 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v1.7.1 h1:UJcjSAI3aUKx52kfcfhblgyhZceouhvvs3OYdWgn+PY= github.com/shoenig/test v1.7.1/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= 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/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e h1:mOtuXaRAbVZsxAHVdPR3IjfmN8T1h2iczJLynhLybf8= github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -1998,6 +2081,7 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU= github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= @@ -2011,6 +2095,7 @@ github.com/twmb/franz-go/plugin/kprom v1.1.0 h1:grGeIJbm4llUBF8jkDjTb/b8rKllWSXj github.com/twmb/franz-go/plugin/kprom v1.1.0/go.mod h1:cTDrPMSkyrO99LyGx3AtiwF9W6+THHjZrkDE2+TEBIU= github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vburenin/ifacemaker v1.2.1 h1:3Vq8B/bfBgjWTkv+jDg4dVL1KHt3k1K4lO7XRxYA2sk= @@ -2041,6 +2126,7 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= github.com/ydb-platform/ydb-go-genproto v0.0.0-20240528144234-5d5a685e41f7 h1:nL8XwD6fSst7xFUirkaWJmE7kM0CdWRYgu6+YQer1d4= github.com/ydb-platform/ydb-go-genproto v0.0.0-20240528144234-5d5a685e41f7/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= @@ -2064,6 +2150,9 @@ go.einride.tech/aip v0.66.0 h1:XfV+NQX6L7EOYK11yoHHFtndeaWh3KbD9/cN/6iWEt8= go.einride.tech/aip v0.66.0/go.mod h1:qAhMsfT7plxBX+Oy7Huol6YUvZ0ZzdUz26yZsQwfl1M= go.einride.tech/aip v0.68.0 h1:4seM66oLzTpz50u4K1zlJyOXQ3tCzcJN7I22tKkjipw= go.einride.tech/aip v0.68.0/go.mod h1:7y9FF8VtPWqpxuAxl0KQWqaULxW4zFIesD6zF5RIHHg= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -2072,6 +2161,7 @@ go.etcd.io/gofail v0.1.0 h1:XItAMIhOojXFQMgrxjnd2EIIHun/d5qL0Pf7FzVTkFg= go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opentelemetry.io/collector v0.102.1 h1:M/ciCcReQsSDYG9bJ2Qwqk7pQILDJ2bM/l0MdeCAvJE= go.opentelemetry.io/collector v0.102.1/go.mod h1:yF1lDRgL/Eksb4/LUnkMjvLvHHpi6wqBVlzp+dACnPM= go.opentelemetry.io/collector/component v0.102.1 h1:66z+LN5dVCXhvuVKD1b56/3cYLK+mtYSLIwlskYA9IQ= @@ -2227,7 +2317,9 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= @@ -2235,11 +2327,15 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -2247,6 +2343,7 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= @@ -2264,6 +2361,7 @@ golang.org/x/exp v0.0.0-20220328175248-053ad81199eb/go.mod h1:lgLbSvA5ygNOMpwM/9 golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= @@ -2291,6 +2389,7 @@ golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -2299,6 +2398,7 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= @@ -2332,6 +2432,7 @@ golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 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-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2344,6 +2445,7 @@ golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -2373,12 +2475,17 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -2427,8 +2534,11 @@ google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SB google.golang.org/api v0.196.0/go.mod h1:g9IL21uGkYgvQ5BZg6BAtoGJQIm8r6EgaAbpNey5wBE= google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw= google.golang.org/api v0.211.0/go.mod h1:XOloB4MXFH4UTlQSGuNUxw0UT74qdENK8d6JNsXKLi0= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190926190326-7ee9db18f195/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= google.golang.org/genproto v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= @@ -2550,6 +2660,9 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= @@ -2581,6 +2694,8 @@ 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= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= @@ -2605,11 +2720,13 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= +k8s.io/apiserver v0.32.0/go.mod h1:HFh+dM1/BE/Hm4bS4nTXHVfN6Z6tFIZPi649n83b4Ag= k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= k8s.io/code-generator v0.32.0 h1:s0lNN8VSWny8LBz5t5iy7MCdgwdOhdg7vAGVxvS+VWU= k8s.io/code-generator v0.32.0/go.mod h1:b7Q7KMZkvsYFy72A79QYjiv4aTz3GvW0f1T3UfhFq4s= k8s.io/code-generator v0.32.1 h1:4lw1kFNDuFYXquTkB7Sl5EwPMUP2yyW9hh6BnFfRZFY= k8s.io/code-generator v0.32.1/go.mod h1:zaILfm00CVyP/6/pJMJ3zxRepXkxyDfUV5SNG4CjZI4= +k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6 h1:4s3/R4+OYYYUKptXPhZKjQ04WJ6EhQQVFdjOFvCazDk= k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks= k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -2623,6 +2740,7 @@ k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kms v0.32.0/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= @@ -2677,3 +2795,4 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ih sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 58e48dafc0d..5b5d4913d87 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -119,9 +119,9 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 // indirect - github.com/dolthub/go-mysql-server v0.19.0 // indirect + github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7 // indirect github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect - github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 // 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.3.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 05f1f0f36b5..0ec94d6bec8 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -316,12 +316,12 @@ github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1G github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM= github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA= -github.com/dolthub/go-mysql-server v0.19.0 h1:NdcXyGt9v7m4sQOahU+ss++iyPy4Q3viuVvbnn3rUTQ= -github.com/dolthub/go-mysql-server v0.19.0/go.mod h1:elfIatfq2fkU5lqTBrTcpL0RcHZOgYPE8EzBD7yQFiY= +github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7 h1:b9ygz2zMlMv2AcJ9mLL0b60tJ9Qd+bzfsAY5ZDTu9VY= +github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7/go.mod h1:jYEJ8tNkA7K3k39X8iMqaX3MSMmViRgh222JSLHDgVc= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= -github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 h1:nzBnC0Rt1gFtscJEz4veYd/mazZEdbdmed+tujdaKOo= -github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= +github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa h1:kyoPzxViSXAyqfO0Mab7Qo1UogFIrxZKKyBU6kBOl+E= +github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= 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/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 96686090205..71ad28afea0 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -214,12 +214,12 @@ github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1G github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM= github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA= -github.com/dolthub/go-mysql-server v0.19.0 h1:NdcXyGt9v7m4sQOahU+ss++iyPy4Q3viuVvbnn3rUTQ= -github.com/dolthub/go-mysql-server v0.19.0/go.mod h1:elfIatfq2fkU5lqTBrTcpL0RcHZOgYPE8EzBD7yQFiY= +github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7 h1:b9ygz2zMlMv2AcJ9mLL0b60tJ9Qd+bzfsAY5ZDTu9VY= +github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7/go.mod h1:jYEJ8tNkA7K3k39X8iMqaX3MSMmViRgh222JSLHDgVc= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= -github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 h1:nzBnC0Rt1gFtscJEz4veYd/mazZEdbdmed+tujdaKOo= -github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= +github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa h1:kyoPzxViSXAyqfO0Mab7Qo1UogFIrxZKKyBU6kBOl+E= +github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= 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/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= From c8609b8a6165eded00aea0c7dc7bf26702c4528f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 6 Feb 2025 22:23:00 +0100 Subject: [PATCH 404/894] feat(unified-storage): keep tags on reduce of dashboards (#100230) --- pkg/registry/apis/dashboard/large.go | 2 +- pkg/registry/apis/dashboard/large_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/registry/apis/dashboard/large.go b/pkg/registry/apis/dashboard/large.go index 88a5eb2d56d..9530b247d2f 100644 --- a/pkg/registry/apis/dashboard/large.go +++ b/pkg/registry/apis/dashboard/large.go @@ -31,7 +31,7 @@ func NewDashboardLargeObjectSupport(scheme *runtime.Scheme) *apistore.BasicLarge dash.Spec = spec dash.SetManagedFields(nil) // this could be bigger than the object! - keep := []string{"title", "description", "schemaVersion"} + keep := []string{"title", "description", "tags", "schemaVersion"} for _, k := range keep { v, ok := old[k] if ok { diff --git a/pkg/registry/apis/dashboard/large_test.go b/pkg/registry/apis/dashboard/large_test.go index a1f1094a935..45510304a65 100644 --- a/pkg/registry/apis/dashboard/large_test.go +++ b/pkg/registry/apis/dashboard/large_test.go @@ -54,7 +54,8 @@ func TestLargeDashboardSupport(t *testing.T) { require.NoError(t, err) require.JSONEq(t, `{ "schemaVersion": 33, - "title": "Panel tests - All panels" + "title": "Panel tests - All panels", + "tags": ["gdev","panel-tests","all-panels"] }`, string(small)) // Now make it big again From 8e3327a446f9a9319f61aa55e28c66d7587c3ee2 Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:23:51 -0700 Subject: [PATCH 405/894] Chore: Update grabpl version to v3.1.2 (#100157) baldm0mma/ update grabpl version --- .drone.yml | 18 +++++++++--------- scripts/drone/variables.star | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.drone.yml b/.drone.yml index f48a891415e..fe72be97168 100644 --- a/.drone.yml +++ b/.drone.yml @@ -646,7 +646,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.1/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.2/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1099,7 +1099,7 @@ steps: path: /github-app - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.1/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.2/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2065,7 +2065,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.1/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.2/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2594,7 +2594,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.1/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.2/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3152,7 +3152,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.1/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.2/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3372,7 +3372,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.1/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.2/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3504,7 +3504,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.1/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.2/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4869,7 +4869,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.1/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.2/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -5601,6 +5601,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: e1f198d994216d163d6a710aef2f6091572eb624081c550a5db42208ef37827b +hmac: a11d1b70585920d1086c0b98ef6f797365e91e5b31799061609cb6dbead074fa ... diff --git a/scripts/drone/variables.star b/scripts/drone/variables.star index 01147f6df17..6eeb6cfa03c 100644 --- a/scripts/drone/variables.star +++ b/scripts/drone/variables.star @@ -2,7 +2,7 @@ global variables """ -grabpl_version = "v3.1.1" +grabpl_version = "v3.1.2" golang_version = "1.23.5" # nodejs_version should match what's in ".nvmrc", but without the v prefix. From 74b2b5fb190dd298e341b6171038374fcbe39418 Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:58:56 -0700 Subject: [PATCH 406/894] Chore: Update drone.yml signature (#100236) baldm0mma/update drone signature --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index fe72be97168..12e432f896a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -5601,6 +5601,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: a11d1b70585920d1086c0b98ef6f797365e91e5b31799061609cb6dbead074fa +hmac: 39572225832c2de6e648b6a4d66a36d212777390bf6fd4643f9cfaad21182df3 ... From d16f2315a4fa95bbb8a4f94192cb1be0aad3097a Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Thu, 6 Feb 2025 18:26:25 -0600 Subject: [PATCH 407/894] Explore metrics: Always check that custom var is present for otel dep env migration (#100233) --- .../trails/migrations/otelDeploymentEnvironment.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/public/app/features/trails/migrations/otelDeploymentEnvironment.ts b/public/app/features/trails/migrations/otelDeploymentEnvironment.ts index 969c95bf662..517ce88ce2c 100644 --- a/public/app/features/trails/migrations/otelDeploymentEnvironment.ts +++ b/public/app/features/trails/migrations/otelDeploymentEnvironment.ts @@ -32,8 +32,15 @@ export function migrateOtelDeploymentEnvironment(trail: DataTrail, urlParams: Ur ) { return; } - // if there is no dep env, does not need to be migrated - if (!deploymentEnv) { + + // check that there is a deployment environment variable value to migrate + // in some cases the deployment environment may not present + // but due to this change it is now always present and the value is undefined + // https://github.com/grafana/scenes/pull/1033 + if ( + !deploymentEnv || + (Array.isArray(deploymentEnv) && deploymentEnv.length > 0 && deploymentEnv[0] === 'undefined') + ) { return; } From d62c490af56154cf8a7e9b6a097d74ec97913d94 Mon Sep 17 00:00:00 2001 From: "Arati R." <33031346+suntala@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:29:25 +0100 Subject: [PATCH 408/894] UniStore Big Objects: Fix spec rebuilding (#100183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix big object spec rebuilding and associated test --------- Co-authored-by: Jean-Philippe Quémémer --- pkg/registry/apis/dashboard/large.go | 11 ++++++++++- pkg/registry/apis/dashboard/large_test.go | 10 ++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/pkg/registry/apis/dashboard/large.go b/pkg/registry/apis/dashboard/large.go index 9530b247d2f..e54b4d252cb 100644 --- a/pkg/registry/apis/dashboard/large.go +++ b/pkg/registry/apis/dashboard/large.go @@ -51,7 +51,16 @@ func NewDashboardLargeObjectSupport(scheme *runtime.Scheme) *apistore.BasicLarge if err != nil { return err } - return json.Unmarshal(blob, &dash.Spec) + + if err := json.Unmarshal(blob, &dash.Spec); err != nil { + return fmt.Errorf("failed to unmarshal blob into spec: %w", err) + } + + if err := scheme.Convert(dash, obj, nil); err != nil { + return fmt.Errorf("failed to update original object: %w", err) + } + + return nil }, } } diff --git a/pkg/registry/apis/dashboard/large_test.go b/pkg/registry/apis/dashboard/large_test.go index 45510304a65..c5d65fa8f5f 100644 --- a/pkg/registry/apis/dashboard/large_test.go +++ b/pkg/registry/apis/dashboard/large_test.go @@ -59,11 +59,17 @@ func TestLargeDashboardSupport(t *testing.T) { }`, string(small)) // Now make it big again - err = largeObject.RebuildSpec(dash, f) + rehydratedDash := &dashboardv0alpha1.Dashboard{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + err = largeObject.RebuildSpec(rehydratedDash, f) require.NoError(t, err) // check that all panels exist again - panels, found, err = unstructured.NestedSlice(dash.Spec.Object, "panels") + panels, found, err = unstructured.NestedSlice(rehydratedDash.Spec.Object, "panels") require.NoError(t, err) require.True(t, found) require.Len(t, panels, expectedPanelCount) From d196b789e2edd6a5a893363ad9e19c91e14b0710 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Fri, 7 Feb 2025 04:44:17 -0500 Subject: [PATCH 409/894] SQL Expressions: Add more SQLNodes and funcs to allow list (#100227) sql_expr: Add more Nodes and funcs to allow list --- pkg/expr/sql/parser_allow.go | 17 ++++++++++++++++- pkg/expr/sql/parser_allow_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/pkg/expr/sql/parser_allow.go b/pkg/expr/sql/parser_allow.go index 8a2ad436920..54072049253 100644 --- a/pkg/expr/sql/parser_allow.go +++ b/pkg/expr/sql/parser_allow.go @@ -54,7 +54,13 @@ func allowedNode(node sqlparser.SQLNode) (b bool) { case *sqlparser.AliasedExpr, *sqlparser.AliasedTableExpr: return - case *sqlparser.BinaryExpr: + case *sqlparser.AndExpr, *sqlparser.OrExpr: + return + + case *sqlparser.BinaryExpr, *sqlparser.UnaryExpr: + return + + case sqlparser.BoolVal: return case sqlparser.ColIdent, *sqlparser.ColName, sqlparser.Columns: @@ -87,6 +93,9 @@ func allowedNode(node sqlparser.SQLNode) (b bool) { case *sqlparser.Select, sqlparser.SelectExprs: return + case *sqlparser.SetOp: + return + case *sqlparser.StarExpr: return @@ -102,6 +111,9 @@ func allowedNode(node sqlparser.SQLNode) (b bool) { case *sqlparser.Over: return + case *sqlparser.ParenExpr: + return + case *sqlparser.Subquery: return @@ -124,6 +136,9 @@ func allowedFunction(f *sqlparser.FuncExpr) (b bool) { b = true // so don't have to return true in every case but default switch strings.ToLower(f.Name.String()) { + case "if": + return + case "sum", "avg", "count", "min", "max": return diff --git a/pkg/expr/sql/parser_allow_test.go b/pkg/expr/sql/parser_allow_test.go index aeeb1a40c69..98dd3277173 100644 --- a/pkg/expr/sql/parser_allow_test.go +++ b/pkg/expr/sql/parser_allow_test.go @@ -17,6 +17,11 @@ func TestAllowQuery(t *testing.T) { q: example_metrics_query, err: nil, }, + { + name: "an example from todd", + q: example_argo_commit_example, + err: nil, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -78,3 +83,29 @@ SELECT * FROM usage_by_team CROSS JOIN total_metrics CROSS JOIN total_traces` + +var example_argo_commit_example = `WITH +gh AS + (SELECT Count(*) AS commits + FROM + (SELECT * + FROM oss_repo + UNION ALL SELECT * + FROM ent_repo) AS ent_repos), +argo_success AS + (SELECT IF(argo.status = 'Succeeded', argo.value, 0) AS value FROM argo), +argo_failure AS + (SELECT IF(argo.status = 'Failed', argo.value, 0) AS value FROM argo) +SELECT IF(env.value > 1, TRUE, workflows.runs < 1 OR gh.commits < 1) AS status, + gh.commits AS 'merged commits to main (OSS + enterprise)', + drone.value AS 'enterprise downstream publish', + workflows.runs AS 'github trigger instant workflow runs today', + argo_success.value AS 'argo success', + argo_failure.value AS 'argo failure', + (env.value - 1) AS 'new dev instant deployments' +FROM drone, + env, + gh, + argo_success, + argo_failure, + workflows;` From 9da423045ec58f9e187bc6675e4a2d366feb6105 Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Fri, 7 Feb 2025 10:49:27 +0100 Subject: [PATCH 410/894] Advisor: Enable frontend code generation for the `checktype` kind (#100208) nit: enabling frontend code generation for the `checktype` --- apps/advisor/kinds/checktype.cue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/advisor/kinds/checktype.cue b/apps/advisor/kinds/checktype.cue index 6a4bfa4a0df..aa31430ad2e 100644 --- a/apps/advisor/kinds/checktype.cue +++ b/apps/advisor/kinds/checktype.cue @@ -7,7 +7,7 @@ checktype: { versions: { "v0alpha1": { codegen: { - frontend: false + frontend: true backend: true } schema: { From 6dc98dbbcc05eb67e18b0a9c398a3f8eaa20b2e0 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Fri, 7 Feb 2025 04:51:55 -0500 Subject: [PATCH 411/894] SQL Expressions: Add str_to_date function and unskip test (#100226) --- pkg/expr/sql/db_test.go | 14 ++++---------- pkg/expr/sql/parser_allow.go | 3 +++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/pkg/expr/sql/db_test.go b/pkg/expr/sql/db_test.go index 258507121c2..e648407e41a 100644 --- a/pkg/expr/sql/db_test.go +++ b/pkg/expr/sql/db_test.go @@ -148,27 +148,21 @@ func TestQueryFramesNumericSelect(t *testing.T) { } func TestQueryFramesDateTimeSelect(t *testing.T) { - t.Skip("need a fix in go-mysql-server, and then handle the datetime strings (or figure out why strings and not time.Time)") expectedFrame := &data.Frame{ RefID: "a", Name: "a", Fields: []*data.Field{ - data.NewField("ts", nil, []time.Time{}), + data.NewField("ts", nil, []*time.Time{ + p(time.Date(2025, 2, 3, 3, 0, 0, 0, time.UTC)), + }), }, } db := DB{} - // It doesn't like the T in the time string qry := `SELECT str_to_date('2025-02-03T03:00:00','%Y-%m-%dT%H:%i:%s') as ts` - // This comes back as a string, which needs to be dealt with? - //qry := `SELECT str_to_date('2025-02-03-03:00:00','%Y-%m-%d-%H:%i:%s') as ts` - - // This is a datetime(6), need to deal with that as well - //qry := `SELECT current_timestamp() as ts` - - f, err := db.QueryFrames(context.Background(), "b", qry, []*data.Frame{}) + f, err := db.QueryFrames(context.Background(), "a", qry, nil) require.NoError(t, err) if diff := cmp.Diff(expectedFrame, f, data.FrameTestCompareOptions()...); diff != "" { diff --git a/pkg/expr/sql/parser_allow.go b/pkg/expr/sql/parser_allow.go index 54072049253..3b5d553af08 100644 --- a/pkg/expr/sql/parser_allow.go +++ b/pkg/expr/sql/parser_allow.go @@ -145,6 +145,9 @@ func allowedFunction(f *sqlparser.FuncExpr) (b bool) { case "coalesce": return + case "str_to_date": + return + default: return false } From e291140be3ea25ba862aedb73c68440d2eac8ad3 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Fri, 7 Feb 2025 10:57:26 +0100 Subject: [PATCH 412/894] Advisor: Run check steps in parallel (#100200) --- .../pkg/app/checks/datasourcecheck/check.go | 106 +++++++-------- .../app/checks/datasourcecheck/check_test.go | 30 +++-- apps/advisor/pkg/app/checks/ifaces.go | 4 +- .../pkg/app/checks/plugincheck/check.go | 124 +++++++++--------- .../pkg/app/checks/plugincheck/check_test.go | 10 +- apps/advisor/pkg/app/checks/utils.go | 4 +- apps/advisor/pkg/app/utils.go | 52 ++++++-- apps/advisor/pkg/app/utils_test.go | 50 ++++++- 8 files changed, 225 insertions(+), 155 deletions(-) diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go index 88e10a72719..e946f2e2819 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/util" - "k8s.io/klog/v2" ) type check struct { @@ -76,26 +75,23 @@ func (s *uidValidationStep) Description() string { return "Check if the UID of each data source is valid." } -func (s *uidValidationStep) Run(ctx context.Context, obj *advisor.CheckSpec, items []any) ([]advisor.CheckReportError, error) { - dsErrs := []advisor.CheckReportError{} - for _, i := range items { - ds, ok := i.(*datasources.DataSource) - if !ok { - return nil, fmt.Errorf("invalid item type %T", i) - } - // Data source UID validation - err := util.ValidateUID(ds.UID) - if err != nil { - dsErrs = append(dsErrs, checks.NewCheckReportError( - advisor.CheckReportErrorSeverityLow, - fmt.Sprintf("Invalid UID '%s' for data source %s", ds.UID, ds.Name), - "Check the documentation for more information.", - s.ID(), - ds.UID, - )) - } +func (s *uidValidationStep) Run(ctx context.Context, obj *advisor.CheckSpec, i any) (*advisor.CheckReportError, error) { + ds, ok := i.(*datasources.DataSource) + if !ok { + return nil, fmt.Errorf("invalid item type %T", i) } - return dsErrs, nil + // Data source UID validation + err := util.ValidateUID(ds.UID) + if err != nil { + return checks.NewCheckReportError( + advisor.CheckReportErrorSeverityLow, + fmt.Sprintf("Invalid UID '%s' for data source %s", ds.UID, ds.Name), + "Check the documentation for more information.", + s.ID(), + ds.UID, + ), nil + } + return nil, nil } type healthCheckStep struct { @@ -115,46 +111,38 @@ func (s *healthCheckStep) ID() string { return "health-check" } -func (s *healthCheckStep) Run(ctx context.Context, obj *advisor.CheckSpec, items []any) ([]advisor.CheckReportError, error) { - dsErrs := []advisor.CheckReportError{} - for _, i := range items { - ds, ok := i.(*datasources.DataSource) - if !ok { - return nil, fmt.Errorf("invalid item type %T", i) - } - - // Health check execution - requester, err := identity.GetRequester(ctx) - if err != nil { - return nil, err - } - pCtx, err := s.PluginContextProvider.GetWithDataSource(ctx, ds.Type, requester, ds) - if err != nil { - klog.ErrorS(err, "Error creating plugin context", "datasource", ds.Name) - continue - } - req := &backend.CheckHealthRequest{ - PluginContext: pCtx, - Headers: map[string]string{}, - } - resp, err := s.PluginClient.CheckHealth(ctx, req) - if err != nil { - fmt.Println("Error checking health", err) - continue - } - if resp.Status != backend.HealthStatusOk { - dsErrs = append(dsErrs, checks.NewCheckReportError( - advisor.CheckReportErrorSeverityHigh, - fmt.Sprintf("Health check failed for %s", ds.Name), - fmt.Sprintf( - "Go to the data source configuration"+ - " and address the issues reported.", ds.UID), - s.ID(), - ds.UID, - )) - } +func (s *healthCheckStep) Run(ctx context.Context, obj *advisor.CheckSpec, i any) (*advisor.CheckReportError, error) { + ds, ok := i.(*datasources.DataSource) + if !ok { + return nil, fmt.Errorf("invalid item type %T", i) } - return dsErrs, nil + + // Health check execution + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + pCtx, err := s.PluginContextProvider.GetWithDataSource(ctx, ds.Type, requester, ds) + if err != nil { + return nil, fmt.Errorf("failed to get plugin context: %w", err) + } + req := &backend.CheckHealthRequest{ + PluginContext: pCtx, + Headers: map[string]string{}, + } + resp, err := s.PluginClient.CheckHealth(ctx, req) + if err != nil || resp.Status != backend.HealthStatusOk { + return checks.NewCheckReportError( + advisor.CheckReportErrorSeverityHigh, + fmt.Sprintf("Health check failed for %s", ds.Name), + fmt.Sprintf( + "Go to the data source configuration"+ + " and address the issues reported.", ds.UID), + s.ID(), + ds.UID, + ), nil + } + return nil, nil } type pluginContextProvider interface { diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go index c360f96ad14..5cfe595de67 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go @@ -35,9 +35,13 @@ func TestCheck_Run(t *testing.T) { assert.NoError(t, err) errs := []advisor.CheckReportError{} for _, step := range check.Steps() { - stepErrs, err := step.Run(ctx, &advisor.CheckSpec{}, items) - assert.NoError(t, err) - errs = append(errs, stepErrs...) + for _, item := range items { + stepErr, err := step.Run(ctx, &advisor.CheckSpec{}, item) + assert.NoError(t, err) + if stepErr != nil { + errs = append(errs, *stepErr) + } + } } assert.NoError(t, err) @@ -65,9 +69,13 @@ func TestCheck_Run(t *testing.T) { assert.NoError(t, err) errs := []advisor.CheckReportError{} for _, step := range check.Steps() { - stepErrs, err := step.Run(ctx, &advisor.CheckSpec{}, items) - assert.NoError(t, err) - errs = append(errs, stepErrs...) + for _, item := range items { + stepErr, err := step.Run(ctx, &advisor.CheckSpec{}, item) + assert.NoError(t, err) + if stepErr != nil { + errs = append(errs, *stepErr) + } + } } assert.NoError(t, err) @@ -96,9 +104,13 @@ func TestCheck_Run(t *testing.T) { assert.NoError(t, err) errs := []advisor.CheckReportError{} for _, step := range check.Steps() { - stepErrs, err := step.Run(ctx, &advisor.CheckSpec{}, items) - assert.NoError(t, err) - errs = append(errs, stepErrs...) + for _, item := range items { + stepErr, err := step.Run(ctx, &advisor.CheckSpec{}, item) + assert.NoError(t, err) + if stepErr != nil { + errs = append(errs, *stepErr) + } + } } assert.NoError(t, err) diff --git a/apps/advisor/pkg/app/checks/ifaces.go b/apps/advisor/pkg/app/checks/ifaces.go index b630a84871c..915d986a4a2 100644 --- a/apps/advisor/pkg/app/checks/ifaces.go +++ b/apps/advisor/pkg/app/checks/ifaces.go @@ -24,6 +24,6 @@ type Step interface { Title() string // Description returns the description of the step Description() string - // Run executes the step and returns a list of errors - Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec, items []any) ([]advisorv0alpha1.CheckReportError, error) + // Run executes the step for an item and returns a report + Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec, item any) (*advisorv0alpha1.CheckReportError, error) } diff --git a/apps/advisor/pkg/app/checks/plugincheck/check.go b/apps/advisor/pkg/app/checks/plugincheck/check.go index c024b2608b0..f9dc853d0b8 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check.go @@ -78,35 +78,33 @@ func (s *deprecationStep) ID() string { return "deprecation" } -func (s *deprecationStep) Run(ctx context.Context, _ *advisor.CheckSpec, items []any) ([]advisor.CheckReportError, error) { - errs := []advisor.CheckReportError{} - for _, i := range items { - p, ok := i.(pluginstore.Plugin) - if !ok { - return nil, fmt.Errorf("invalid item type %T", i) - } - - // Skip if it's a core plugin - if p.IsCorePlugin() { - continue - } - - // Check if plugin is deprecated - i, err := s.PluginRepo.PluginInfo(ctx, p.ID) - if err != nil { - continue - } - if i.Status == "deprecated" { - errs = append(errs, checks.NewCheckReportError( - advisor.CheckReportErrorSeverityHigh, - fmt.Sprintf("Plugin deprecated: %s", p.ID), - "Check the documentation for recommended steps.", - s.ID(), - p.ID, - )) - } +func (s *deprecationStep) Run(ctx context.Context, _ *advisor.CheckSpec, it any) (*advisor.CheckReportError, error) { + p, ok := it.(pluginstore.Plugin) + if !ok { + return nil, fmt.Errorf("invalid item type %T", it) } - return errs, nil + + // Skip if it's a core plugin + if p.IsCorePlugin() { + return nil, nil + } + + // Check if plugin is deprecated + i, err := s.PluginRepo.PluginInfo(ctx, p.ID) + if err != nil { + // Unable to check deprecation status + return nil, nil + } + if i.Status == "deprecated" { + return checks.NewCheckReportError( + advisor.CheckReportErrorSeverityHigh, + fmt.Sprintf("Plugin deprecated: %s", p.ID), + "Check the documentation for recommended steps.", + s.ID(), + p.ID, + ), nil + } + return nil, nil } type updateStep struct { @@ -127,44 +125,42 @@ func (s *updateStep) ID() string { return "update" } -func (s *updateStep) Run(ctx context.Context, _ *advisor.CheckSpec, items []any) ([]advisor.CheckReportError, error) { - errs := []advisor.CheckReportError{} - for _, i := range items { - p, ok := i.(pluginstore.Plugin) - if !ok { - return nil, fmt.Errorf("invalid item type %T", i) - } - - // Skip if it's a core plugin - if p.IsCorePlugin() { - continue - } - - // Skip if it's managed or pinned - if s.isManaged(ctx, p.ID) || s.PluginPreinstall.IsPinned(p.ID) { - continue - } - - // Check if plugin has a newer version available - compatOpts := repo.NewCompatOpts(services.GrafanaVersion, sysruntime.GOOS, sysruntime.GOARCH) - info, err := s.PluginRepo.GetPluginArchiveInfo(ctx, p.ID, "", compatOpts) - if err != nil { - continue - } - if hasUpdate(p, info) { - errs = append(errs, checks.NewCheckReportError( - advisor.CheckReportErrorSeverityLow, - fmt.Sprintf("New version available for %s", p.ID), - fmt.Sprintf( - "Go to the plugin admin page"+ - " and upgrade to the latest version.", p.ID), - s.ID(), - p.ID, - )) - } +func (s *updateStep) Run(ctx context.Context, _ *advisor.CheckSpec, i any) (*advisor.CheckReportError, error) { + p, ok := i.(pluginstore.Plugin) + if !ok { + return nil, fmt.Errorf("invalid item type %T", i) } - return errs, nil + // Skip if it's a core plugin + if p.IsCorePlugin() { + return nil, nil + } + + // Skip if it's managed or pinned + if s.isManaged(ctx, p.ID) || s.PluginPreinstall.IsPinned(p.ID) { + return nil, nil + } + + // Check if plugin has a newer version available + compatOpts := repo.NewCompatOpts(services.GrafanaVersion, sysruntime.GOOS, sysruntime.GOARCH) + info, err := s.PluginRepo.GetPluginArchiveInfo(ctx, p.ID, "", compatOpts) + if err != nil { + // Unable to check updates + return nil, nil + } + if hasUpdate(p, info) { + return checks.NewCheckReportError( + advisor.CheckReportErrorSeverityLow, + fmt.Sprintf("New version available for %s", p.ID), + fmt.Sprintf( + "Go to the plugin admin page"+ + " and upgrade to the latest version.", p.ID), + s.ID(), + p.ID, + ), nil + } + + return nil, nil } func hasUpdate(current pluginstore.Plugin, latest *repo.PluginArchiveInfo) bool { diff --git a/apps/advisor/pkg/app/checks/plugincheck/check_test.go b/apps/advisor/pkg/app/checks/plugincheck/check_test.go index 29dc7ccaabd..ff5190278e2 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check_test.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check_test.go @@ -136,9 +136,13 @@ func TestRun(t *testing.T) { assert.NoError(t, err) errs := []advisor.CheckReportError{} for _, step := range check.Steps() { - stepErrs, err := step.Run(context.Background(), &advisor.CheckSpec{}, items) - assert.NoError(t, err) - errs = append(errs, stepErrs...) + for _, item := range items { + stepErr, err := step.Run(context.Background(), &advisor.CheckSpec{}, item) + assert.NoError(t, err) + if stepErr != nil { + errs = append(errs, *stepErr) + } + } } assert.NoError(t, err) assert.Equal(t, len(tt.plugins), len(items)) diff --git a/apps/advisor/pkg/app/checks/utils.go b/apps/advisor/pkg/app/checks/utils.go index a6c4089da44..053dc591389 100644 --- a/apps/advisor/pkg/app/checks/utils.go +++ b/apps/advisor/pkg/app/checks/utils.go @@ -10,8 +10,8 @@ func NewCheckReportError( action string, stepID string, itemID string, -) advisor.CheckReportError { - return advisor.CheckReportError{ +) *advisor.CheckReportError { + return &advisor.CheckReportError{ Severity: severity, Reason: reason, Action: action, diff --git a/apps/advisor/pkg/app/utils.go b/apps/advisor/pkg/app/utils.go index c59dbaca21e..b1809dd2ee3 100644 --- a/apps/advisor/pkg/app/utils.go +++ b/apps/advisor/pkg/app/utils.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sync" claims "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/resource" @@ -83,20 +84,17 @@ func processCheck(ctx context.Context, client resource.Client, obj resource.Obje } // Run the steps steps := check.Steps() - errs := []advisorv0alpha1.CheckReportError{} - for _, step := range steps { - stepErrs, err := step.Run(ctx, &c.Spec, items) - if err != nil { - setErr := setStatusAnnotation(ctx, client, obj, "error") - if setErr != nil { - return setErr - } - return fmt.Errorf("error running step %s: %w", step.Title(), err) + reportErrors, err := runStepsInParallel(ctx, &c.Spec, steps, items) + if err != nil { + setErr := setStatusAnnotation(ctx, client, obj, "error") + if setErr != nil { + return setErr } - errs = append(errs, stepErrs...) + return fmt.Errorf("error running steps: %w", err) } + report := &advisorv0alpha1.CheckV0alpha1StatusReport{ - Errors: errs, + Errors: reportErrors, Count: int64(len(items)), } err = setStatusAnnotation(ctx, client, obj, "processed") @@ -111,3 +109,35 @@ func processCheck(ctx context.Context, client resource.Client, obj resource.Obje }}, }, resource.PatchOptions{}, obj) } + +func runStepsInParallel(ctx context.Context, spec *advisorv0alpha1.CheckSpec, steps []checks.Step, items []any) ([]advisorv0alpha1.CheckReportError, error) { + reportErrs := []advisorv0alpha1.CheckReportError{} + var internalErr error + var wg sync.WaitGroup + var mu sync.Mutex + // Avoid too many concurrent requests + limit := make(chan struct{}, 10) + + for _, step := range steps { + for _, item := range items { + wg.Add(1) + limit <- struct{}{} + go func(step checks.Step, item any) { + defer wg.Done() + defer func() { <-limit }() + stepErr, err := step.Run(ctx, spec, item) + mu.Lock() + defer mu.Unlock() + if err != nil { + internalErr = fmt.Errorf("error running step %s: %w", step.ID(), err) + return + } + if stepErr != nil { + reportErrs = append(reportErrs, *stepErr) + } + }(step, item) + } + } + wg.Wait() + return reportErrs, internalErr +} diff --git a/apps/advisor/pkg/app/utils_test.go b/apps/advisor/pkg/app/utils_test.go index a13880f8209..5905e2a9c44 100644 --- a/apps/advisor/pkg/app/utils_test.go +++ b/apps/advisor/pkg/app/utils_test.go @@ -3,6 +3,7 @@ package app import ( "context" "errors" + "fmt" "testing" "github.com/grafana/grafana-app-sdk/resource" @@ -68,13 +69,45 @@ func TestProcessCheck(t *testing.T) { meta.SetCreatedBy("user:1") client := &mockClient{} ctx := context.TODO() - check := &mockCheck{} + check := &mockCheck{ + items: []any{"item"}, + } err = processCheck(ctx, client, obj, check) assert.NoError(t, err) assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) } +func TestProcessMultipleCheckItems(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{}) + meta, err := utils.MetaAccessor(obj) + if err != nil { + t.Fatal(err) + } + meta.SetCreatedBy("user:1") + client := &mockClient{} + ctx := context.TODO() + items := make([]any, 100) + for i := range items { + if i%2 == 0 { + items[i] = fmt.Sprintf("item-%d", i) + } else { + items[i] = errors.New("error") + } + } + check := &mockCheck{ + items: items, + } + + err = processCheck(ctx, client, obj, check) + assert.NoError(t, err) + assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) + r := client.lastValue.(advisorv0alpha1.CheckV0alpha1StatusReport) + assert.Equal(t, r.Count, int64(100)) + assert.Len(t, r.Errors, 50) +} + func TestProcessCheck_AlreadyProcessed(t *testing.T) { obj := &advisorv0alpha1.Check{} obj.SetAnnotations(map[string]string{statusAnnotation: "processed"}) @@ -98,7 +131,8 @@ func TestProcessCheck_RunError(t *testing.T) { ctx := context.TODO() check := &mockCheck{ - err: errors.New("run error"), + items: []any{"item"}, + err: errors.New("run error"), } err = processCheck(ctx, client, obj, check) @@ -108,14 +142,17 @@ func TestProcessCheck_RunError(t *testing.T) { type mockClient struct { resource.Client + lastValue any } func (m *mockClient) PatchInto(ctx context.Context, id resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions, obj resource.Object) error { + m.lastValue = req.Operations[0].Value return nil } type mockCheck struct { - err error + err error + items []any } func (m *mockCheck) ID() string { @@ -123,7 +160,7 @@ func (m *mockCheck) ID() string { } func (m *mockCheck) Items(ctx context.Context) ([]any, error) { - return []any{}, nil + return m.items, nil } func (m *mockCheck) Steps() []checks.Step { @@ -136,10 +173,13 @@ type mockStep struct { err error } -func (m *mockStep) Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec, items []any) ([]advisorv0alpha1.CheckReportError, error) { +func (m *mockStep) Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec, items any) (*advisorv0alpha1.CheckReportError, error) { if m.err != nil { return nil, m.err } + if _, ok := items.(error); ok { + return &advisorv0alpha1.CheckReportError{}, nil + } return nil, nil } From ccb9cab1318ea76ebb6b1005e7a53e113cba0d3b Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 7 Feb 2025 11:07:08 +0100 Subject: [PATCH 413/894] Plugins: Add synchronous CDN plugin loader (#99096) * WIP * Run plugin validations and validation steps sequentially if feature is off * Remove dependency between sources.Service and pluginscdn.Service * lint * Parallelize validation only if class is CDN * re-generate feature toggles * remove waitgroup usage * PR review: Add loader concurrency limit setting * re-generate feature toggles * pr review feedback * fix const name * Skip module.js validation for cdn plugins * do not run validation steps in parallel * lint * reduce diff * re-generate feature toggles * lint * pr review feedback * remove leftover config.PluginManagementCfg from sources.Service --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/plugins/config/config.go | 7 +-- pkg/plugins/manager/loader/loader.go | 54 ++++++++++++++++--- pkg/plugins/manager/loader/loader_test.go | 10 +++- .../manager/pipeline/validation/steps.go | 5 ++ pkg/services/featuremgmt/registry.go | 6 +++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++ pkg/services/featuremgmt/toggles_gen.json | 12 +++++ .../pluginsintegration/loader/loader.go | 7 ++- .../pluginsintegration/loader/loader_test.go | 4 +- .../pluginsintegration/pluginconfig/config.go | 7 +-- .../pluginsintegration/pluginsintegration.go | 4 +- .../pluginsintegration/renderer/renderer.go | 2 +- .../pluginsintegration/test_helper.go | 2 +- 16 files changed, 105 insertions(+), 22 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 13646ef8948..73143bc543b 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -233,6 +233,7 @@ Experimental features might be changed or removed without prior notice. | `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing | | `datasourceConnectionsTab` | Shows defined connections for a data source in the plugins detail page | | `newLogsPanel` | Enables the new logs panel in Explore | +| `pluginsCDNSyncLoader` | Load plugins from CDN synchronously | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 3e0490dd97b..cbe09d02910 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -257,4 +257,5 @@ export interface FeatureToggles { alertingAlertmanagerExtraDedupStageStopPipeline?: boolean; newLogsPanel?: boolean; grafanaconThemes?: boolean; + pluginsCDNSyncLoader?: boolean; } diff --git a/pkg/plugins/config/config.go b/pkg/plugins/config/config.go index a26c66176a1..67b5b9c15ee 100644 --- a/pkg/plugins/config/config.go +++ b/pkg/plugins/config/config.go @@ -31,9 +31,10 @@ type PluginManagementCfg struct { // Features contains the feature toggles used for the plugin management system. type Features struct { - ExternalCorePluginsEnabled bool - SkipHostEnvVarsEnabled bool - SriChecksEnabled bool + ExternalCorePluginsEnabled bool + SkipHostEnvVarsEnabled bool + SriChecksEnabled bool + PluginsCDNSyncLoaderEnabled bool } // NewPluginManagementCfg returns a new PluginManagementCfg. diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 2abc88ef735..f3c0d47e73b 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -8,6 +8,7 @@ import ( "time" "github.com/grafana/grafana/pkg/plugins" + pluginsCfg "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" @@ -17,7 +18,10 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" ) +const concurrencyLimit = 32 + type Loader struct { + cfg *pluginsCfg.PluginManagementCfg discovery discovery.Discoverer bootstrap bootstrap.Bootstrapper initializer initialization.Initializer @@ -27,9 +31,13 @@ type Loader struct { log log.Logger } -func New(discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, validation validation.Validator, - initializer initialization.Initializer, termination termination.Terminator, errorTracker pluginerrs.ErrorTracker) *Loader { +func New( + cfg *pluginsCfg.PluginManagementCfg, + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, validation validation.Validator, + initializer initialization.Initializer, termination termination.Terminator, errorTracker pluginerrs.ErrorTracker, +) *Loader { return &Loader{ + cfg: cfg, discovery: discovery, bootstrap: bootstrap, validation: validation, @@ -55,11 +63,14 @@ func (l *Loader) recordError(ctx context.Context, p *plugins.Plugin, err error) func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { end := l.instrumentLoad(ctx, src) + st := time.Now() discoveredPlugins, err := l.discovery.Discover(ctx, src) if err != nil { return nil, err } + l.log.Debug("Discovered", "class", src.PluginClass(ctx), "duration", time.Since(st)) + st = time.Now() bootstrappedPlugins := []*plugins.Plugin{} for _, foundBundle := range discoveredPlugins { bootstrappedPlugin, err := l.bootstrap.Bootstrap(ctx, src, foundBundle) @@ -72,17 +83,47 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins } bootstrappedPlugins = append(bootstrappedPlugins, bootstrappedPlugin...) } + l.log.Debug("Bootstrapped", "class", src.PluginClass(ctx), "duration", time.Since(st)) + st = time.Now() validatedPlugins := []*plugins.Plugin{} + type validateResult struct { + bootstrappedPlugin *plugins.Plugin + err error + } + validateResults := make(chan validateResult, len(bootstrappedPlugins)) + + // If the PluginsCDNSyncLoaderEnabled feature is enabled, validate plugins in parallel. + // Otherwise, validate plugins sequentially. + var limitSize int + if l.cfg.Features.PluginsCDNSyncLoaderEnabled && src.PluginClass(ctx) == plugins.ClassCDN { + limitSize = min(len(bootstrappedPlugins), concurrencyLimit) + } else { + limitSize = 1 + } + limit := make(chan struct{}, limitSize) for _, bootstrappedPlugin := range bootstrappedPlugins { - err := l.validation.Validate(ctx, bootstrappedPlugin) - if err != nil { - l.recordError(ctx, bootstrappedPlugin, err) + limit <- struct{}{} + go func(p *plugins.Plugin) { + err := l.validation.Validate(ctx, p) + validateResults <- validateResult{ + bootstrappedPlugin: bootstrappedPlugin, + err: err, + } + <-limit + }(bootstrappedPlugin) + } + for i := 0; i < len(bootstrappedPlugins); i++ { + r := <-validateResults + if r.err != nil { + l.recordError(ctx, r.bootstrappedPlugin, r.err) continue } - validatedPlugins = append(validatedPlugins, bootstrappedPlugin) + validatedPlugins = append(validatedPlugins, r.bootstrappedPlugin) } + l.log.Debug("Validated", "class", src.PluginClass(ctx), "duration", time.Since(st), "total", len(validatedPlugins)) + st = time.Now() initializedPlugins := []*plugins.Plugin{} for _, validatedPlugin := range validatedPlugins { initializedPlugin, err := l.initializer.Initialize(ctx, validatedPlugin) @@ -92,6 +133,7 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins } initializedPlugins = append(initializedPlugins, initializedPlugin) } + l.log.Debug("Initialized", "class", src.PluginClass(ctx), "duration", time.Since(st)) // Clean errors from registry for initialized plugins for _, p := range initializedPlugins { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 3472727f127..d39c9c21303 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -58,6 +58,7 @@ func TestLoader_Load(t *testing.T) { t.Errorf("could not construct absolute path of current dir") return } + zeroCfg := &config.PluginManagementCfg{} tests := []struct { name string class plugins.Class @@ -420,7 +421,7 @@ func TestLoader_Load(t *testing.T) { require.NoError(t, err) et := pluginerrs.ProvideErrorTracker() - l := New(discovery.New(tt.cfg, discovery.Opts{}), bootstrap.New(tt.cfg, bootstrap.Opts{}), + l := New(zeroCfg, discovery.New(tt.cfg, discovery.Opts{}), bootstrap.New(tt.cfg, bootstrap.Opts{}), validation.New(tt.cfg, validation.Opts{}), initialization.New(tt.cfg, initialization.Opts{}), terminationStage, et) @@ -455,6 +456,7 @@ func TestLoader_Load(t *testing.T) { var steps []string l := New( + zeroCfg, &fakes.FakeDiscoverer{ DiscoverFunc: func(ctx context.Context, s plugins.PluginSource) ([]*plugins.FoundBundle, error) { require.Equal(t, src, s) @@ -512,6 +514,7 @@ func TestLoader_Load(t *testing.T) { var steps []string l := New( + zeroCfg, &fakes.FakeDiscoverer{ DiscoverFunc: func(ctx context.Context, s plugins.PluginSource) ([]*plugins.FoundBundle, error) { require.Equal(t, src, s) @@ -574,6 +577,7 @@ func TestLoader_Load(t *testing.T) { var steps []string l := New( + zeroCfg, &fakes.FakeDiscoverer{ DiscoverFunc: func(ctx context.Context, s plugins.PluginSource) ([]*plugins.FoundBundle, error) { require.Equal(t, src, s) @@ -629,7 +633,9 @@ func TestLoader_Unload(t *testing.T) { } for _, tc := range tcs { - l := New(&fakes.FakeDiscoverer{}, + l := New( + &config.PluginManagementCfg{}, + &fakes.FakeDiscoverer{}, &fakes.FakeBootstrapper{}, &fakes.FakeValidator{}, &fakes.FakeInitializer{}, diff --git a/pkg/plugins/manager/pipeline/validation/steps.go b/pkg/plugins/manager/pipeline/validation/steps.go index 10af0cce5e4..1b41c1b5d6d 100644 --- a/pkg/plugins/manager/pipeline/validation/steps.go +++ b/pkg/plugins/manager/pipeline/validation/steps.go @@ -57,6 +57,11 @@ func newModuleJSValidator() *ModuleJSValidator { } func (v *ModuleJSValidator) Validate(_ context.Context, p *plugins.Plugin) error { + // CDN plugins are ignored because the module.js is guaranteed to exist + if p.Class == plugins.ClassCDN { + return nil + } + if !p.IsRenderer() && !p.IsCorePlugin() { f, err := p.FS.Open("module.js") if err != nil { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index ac2eaffc940..81dda206467 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1794,6 +1794,12 @@ var ( HideFromDocs: true, RequiresRestart: true, }, + { + Name: "pluginsCDNSyncLoader", + Description: "Load plugins from CDN synchronously", + Stage: FeatureStageExperimental, + Owner: grafanaPluginsPlatformSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index e88c9c3d176..d21b4c4fded 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -238,3 +238,4 @@ alertingAlertmanagerExtraDedupStage,experimental,@grafana/alerting-squad,false,t alertingAlertmanagerExtraDedupStageStopPipeline,experimental,@grafana/alerting-squad,false,true,false newLogsPanel,experimental,@grafana/observability-logs,false,false,true grafanaconThemes,experimental,@grafana/grafana-frontend-platform,false,true,false +pluginsCDNSyncLoader,experimental,@grafana/plugins-platform-backend,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index e910ed15857..bedb497d4f5 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -962,4 +962,8 @@ const ( // FlagGrafanaconThemes // Enables the temporary themes for GrafanaCon FlagGrafanaconThemes = "grafanaconThemes" + + // FlagPluginsCDNSyncLoader + // Load plugins from CDN synchronously + FlagPluginsCDNSyncLoader = "pluginsCDNSyncLoader" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 607d0581270..db65ff4fdb8 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3034,6 +3034,18 @@ "frontend": true } }, + { + "metadata": { + "name": "pluginsCDNSyncLoader", + "resourceVersion": "1737026684018", + "creationTimestamp": "2025-01-16T11:24:44Z" + }, + "spec": { + "description": "Load plugins from CDN synchronously", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend" + } + }, { "metadata": { "name": "pluginsDetailsRightPanel", diff --git a/pkg/services/pluginsintegration/loader/loader.go b/pkg/services/pluginsintegration/loader/loader.go index 4852e84d1b4..b3a03885752 100644 --- a/pkg/services/pluginsintegration/loader/loader.go +++ b/pkg/services/pluginsintegration/loader/loader.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" pluginsLoader "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" @@ -19,11 +20,13 @@ type Loader struct { loader *pluginsLoader.Loader } -func ProvideService(discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, validation validation.Validator, +func ProvideService( + cfg *config.PluginManagementCfg, + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, validation validation.Validator, initializer initialization.Initializer, termination termination.Terminator, errorTracker pluginerrs.ErrorTracker, ) *Loader { return &Loader{ - loader: pluginsLoader.New(discovery, bootstrap, validation, initializer, termination, errorTracker), + loader: pluginsLoader.New(cfg, discovery, bootstrap, validation, initializer, termination, errorTracker), } } diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index 06466240b9e..ca6fef68b50 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -1575,7 +1575,7 @@ func newLoader(t *testing.T, cfg *config.PluginManagementCfg, reg registry.Servi terminate, err := pipeline.ProvideTerminationStage(cfg, reg, proc) require.NoError(t, err) - return ProvideService(pipeline.ProvideDiscoveryStage(cfg, + return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg), pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector), @@ -1607,7 +1607,7 @@ func newLoaderWithOpts(t *testing.T, cfg *config.PluginManagementCfg, opts loade backendFactoryProvider = fakes.NewFakeBackendProcessProvider() } - return ProvideService(pipeline.ProvideDiscoveryStage(cfg, + return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg), pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector), diff --git a/pkg/services/pluginsintegration/pluginconfig/config.go b/pkg/services/pluginsintegration/pluginconfig/config.go index 71952c64eb8..47751b07711 100644 --- a/pkg/services/pluginsintegration/pluginconfig/config.go +++ b/pkg/services/pluginsintegration/pluginconfig/config.go @@ -30,9 +30,10 @@ func ProvidePluginManagementConfig(cfg *setting.Cfg, settingProvider setting.Pro cfg.PluginsCDNURLTemplate, cfg.AppURL, config.Features{ - ExternalCorePluginsEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalCorePlugins), - SkipHostEnvVarsEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSkipHostEnvVars), - SriChecksEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSriChecks), + ExternalCorePluginsEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalCorePlugins), + SkipHostEnvVarsEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSkipHostEnvVars), + SriChecksEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSriChecks), + PluginsCDNSyncLoaderEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsCDNSyncLoader), }, cfg.AngularSupportEnabled, cfg.GrafanaComAPIURL, diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 22929782cf8..eca6c3be53f 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -107,8 +107,6 @@ var WireSet = wire.NewSet( wire.Bind(new(repo.Service), new(*repo.Manager)), licensing.ProvideLicensing, wire.Bind(new(plugins.Licensing), new(*licensing.Service)), - wire.Bind(new(sources.Registry), new(*sources.Service)), - sources.ProvideService, pluginSettings.ProvideService, wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)), filestore.ProvideService, @@ -146,6 +144,8 @@ var WireExtensionSet = wire.NewSet( wire.Bind(new(plugins.Client), new(*backend.MiddlewareHandler)), managedplugins.NewNoop, wire.Bind(new(managedplugins.Manager), new(*managedplugins.Noop)), + sources.ProvideService, + wire.Bind(new(sources.Registry), new(*sources.Service)), ) func ProvideClientWithMiddlewares( diff --git a/pkg/services/pluginsintegration/renderer/renderer.go b/pkg/services/pluginsintegration/renderer/renderer.go index ff8003b957c..5623b5812f8 100644 --- a/pkg/services/pluginsintegration/renderer/renderer.go +++ b/pkg/services/pluginsintegration/renderer/renderer.go @@ -141,5 +141,5 @@ func createLoader(cfg *config.PluginManagementCfg, pluginEnvProvider envvars.Pro et := pluginerrs.ProvideErrorTracker() - return loader.New(d, b, v, i, t, et), nil + return loader.New(cfg, d, b, v, i, t, et), nil } diff --git a/pkg/services/pluginsintegration/test_helper.go b/pkg/services/pluginsintegration/test_helper.go index d74e18a4bcf..92ce0dab164 100644 --- a/pkg/services/pluginsintegration/test_helper.go +++ b/pkg/services/pluginsintegration/test_helper.go @@ -110,5 +110,5 @@ func CreateTestLoader(t *testing.T, cfg *pluginsCfg.PluginManagementCfg, opts Lo require.NoError(t, err) } - return loader.New(opts.Discoverer, opts.Bootstrapper, opts.Validator, opts.Initializer, opts.Terminator, pluginerrs.ProvideErrorTracker()) + return loader.New(cfg, opts.Discoverer, opts.Bootstrapper, opts.Validator, opts.Initializer, opts.Terminator, pluginerrs.ProvideErrorTracker()) } From 4b9fee61a8eacdcd535ead80a0cdaba6b1a0e5a2 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Fri, 7 Feb 2025 11:09:51 +0100 Subject: [PATCH 414/894] QueryLibrary: Move to enterprise (#100133) --- .betterer.results | 21 -- public/app/AppWrapper.tsx | 8 +- .../PanelDataPane/PanelDataQueriesTab.tsx | 4 +- .../app/features/explore/ExploreToolbar.tsx | 6 +- .../QueriesDrawer/QueriesDrawerDropdown.tsx | 6 +- .../QueryLibrary/AddToQueryLibraryModal.tsx | 36 --- .../explore/QueryLibrary/QueryLibrary.tsx | 36 --- .../QueryLibraryAnalyticsEvents.ts | 56 ----- .../QueryLibrary/QueryLibraryContext.test.tsx | 79 ------- .../QueryLibrary/QueryLibraryContext.tsx | 106 +++------ .../QueryLibrary/QueryLibraryDrawer.tsx | 53 ----- .../QueryLibrary/QueryLibraryExpmInfo.tsx | 23 -- .../QueryLibrary/QueryTemplateForm.tsx | 180 -------------- .../QueryLibrary/QueryTemplatesList.test.tsx | 139 ----------- .../QueryLibrary/QueryTemplatesList.tsx | 222 ------------------ .../QueryTemplatesTable/ActionsCell.tsx | 110 --------- .../QueryTemplatesTable/AddedByCell.tsx | 20 -- .../DatasourceTypeCell.tsx | 13 - .../QueryTemplatesTable/DateAddedCell.tsx | 13 - .../QueryDescriptionCell.tsx | 55 ----- .../QueryTemplatesTable/index.tsx | 81 ------- .../QueryTemplatesTable/styles.tsx | 47 ---- .../QueryLibrary/QueryTemplatesTable/types.ts | 15 -- .../explore/QueryLibrary/SaveQueryButton.tsx | 44 ---- .../QueryLibrary/utils/dataFetching.ts | 106 --------- .../explore/QueryLibrary/utils/search.ts | 24 -- .../QueryLibrary/utils/useDatasource.tsx | 9 - .../RichHistory/RichHistoryAddToLibrary.tsx | 35 +-- .../features/explore/spec/helper/setup.tsx | 17 +- .../explore/spec/queryLibrary.test.tsx | 172 -------------- public/app/features/query-library/index.ts | 6 - .../query/components/QueryEditorRow.tsx | 14 +- public/app/routes/RoutesWrapper.tsx | 14 +- public/locales/en-US/grafana.json | 40 ---- public/locales/pseudo-LOCALE/grafana.json | 40 ---- 35 files changed, 79 insertions(+), 1771 deletions(-) delete mode 100644 public/app/features/explore/QueryLibrary/AddToQueryLibraryModal.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryLibrary.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryLibraryAnalyticsEvents.ts delete mode 100644 public/app/features/explore/QueryLibrary/QueryLibraryContext.test.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryLibraryDrawer.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryLibraryExpmInfo.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryTemplatesList.test.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryTemplatesTable/AddedByCell.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryTemplatesTable/DatasourceTypeCell.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryTemplatesTable/DateAddedCell.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx delete mode 100644 public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts delete mode 100644 public/app/features/explore/QueryLibrary/SaveQueryButton.tsx delete mode 100644 public/app/features/explore/QueryLibrary/utils/dataFetching.ts delete mode 100644 public/app/features/explore/QueryLibrary/utils/search.ts delete mode 100644 public/app/features/explore/QueryLibrary/utils/useDatasource.tsx delete mode 100644 public/app/features/explore/spec/queryLibrary.test.tsx diff --git a/.betterer.results b/.betterer.results index c9ce73693f2..e33c3ac4abd 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4701,27 +4701,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] ], - "public/app/features/explore/QueryLibrary/QueryLibraryExpmInfo.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"] - ], - "public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Input/Input\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], - "public/app/features/explore/QueryLibrary/QueryTemplatesList.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"] - ], - "public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.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"] - ], "public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index 04c8fb80615..227eaeedeb2 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -1,5 +1,5 @@ import { Action, KBarProvider } from 'kbar'; -import { Component, ComponentType, Fragment } from 'react'; +import { Component, ComponentType, Fragment, ReactNode } from 'react'; import CacheProvider from 'react-inlinesvg/provider'; import { Provider } from 'react-redux'; import { Route, Routes } from 'react-router-dom-v5-compat'; @@ -37,6 +37,11 @@ interface AppWrapperState { /** Used by enterprise */ let bodyRenderHooks: ComponentType[] = []; let pageBanners: ComponentType[] = []; +const enterpriseProviders: Array> = []; + +export function addEnterpriseProviders(provider: ComponentType<{ children: ReactNode }>) { + enterpriseProviders.push(provider); +} export function addBodyRenderHook(fn: ComponentType) { bodyRenderHooks.push(fn); @@ -100,6 +105,7 @@ export class AppWrapper extends Component { routes: ready && this.renderRoutes(), pageBanners, bodyRenderHooks, + providers: enterpriseProviders, }; const MaybeTimeRangeProvider = config.featureToggles.timeRangeProvider ? TimeRangeProvider : Fragment; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index cef20ef129f..66f83b97bd5 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -309,7 +309,7 @@ export class PanelDataQueriesTab extends SceneObjectBase) { const { datasource, dsSettings } = model.useState(); const { data, queries } = model.queryRunner.useState(); - const { openDrawer: openQueryLibraryDrawer } = useQueryLibraryContext(); + const { openDrawer: openQueryLibraryDrawer, queryLibraryEnabled } = useQueryLibraryContext(); if (!datasource || !dsSettings || !data) { return null; @@ -355,7 +355,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps Add query - {config.featureToggles.queryLibrary && ( + {queryLibraryEnabled && ( - - - - ); -}; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.test.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.test.tsx deleted file mode 100644 index 15ec78719e5..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesList.test.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { render, waitFor, screen } from '@testing-library/react'; - -import { AnnoKeyCreatedBy } from '../../apiserver/types'; -import { ListQueryTemplateApiResponse } from '../../query-library/api/endpoints.gen'; - -import { QueryTemplatesList } from './QueryTemplatesList'; -import { QueryActionButtonProps } from './types'; - -let data: ListQueryTemplateApiResponse = { - items: [], -}; - -jest.mock('app/features/query-library', () => { - const actual = jest.requireActual('app/features/query-library'); - return { - ...actual, - useDeleteQueryTemplateMutation: () => [() => {}], - useListQueryTemplateQuery: () => { - return { - data: data, - isLoading: false, - error: null, - }; - }, - }; -}); - -jest.mock('./utils/dataFetching', () => { - return { - __esModule: true, - useLoadQueryMetadata: () => { - return { - loading: false, - value: [ - { - index: '0', - uid: '0', - datasourceName: 'prometheus', - datasourceRef: { type: 'prometheus', uid: 'Prometheus0' }, - datasourceType: 'prometheus', - createdAtTimestamp: 0, - query: { refId: 'A' }, - queryText: 'http_requests_total{job="test"}', - description: 'template0', - user: { - uid: 'viewer:JohnDoe', - displayName: 'John Doe', - avatarUrl: '', - }, - error: undefined, - }, - ], - }; - }, - useLoadUsers: () => { - return { - value: { - display: [ - { - avatarUrl: '', - displayName: 'john doe', - identity: { - name: 'JohnDoe', - type: 'viewer', - }, - }, - ], - }, - loading: false, - error: null, - }; - }, - }; -}); - -describe('QueryTemplatesList', () => { - it('renders empty state', async () => { - data = {}; - render(); - await waitFor(() => { - expect(screen.getByText(/You haven't saved any queries to your library yet/)).toBeInTheDocument(); - }); - }); - - it('renders query', async () => { - data.items = testItems; - render(); - await waitFor(() => { - // We don't really show query template title for some reason so creator name - expect(screen.getByText(/John Doe/)).toBeInTheDocument(); - }); - }); - - it('renders actionButton for query', async () => { - data.items = testItems; - let passedProps: QueryActionButtonProps; - - const queryActionButton = (props: QueryActionButtonProps) => { - passedProps = props; - return ; - }; - - render(); - await waitFor(() => { - // We don't really show query template title for some reason so creator name - expect(screen.getByText(/John Doe/)).toBeInTheDocument(); - expect(screen.getByText(/TEST_ACTION_BUTTON/)).toBeInTheDocument(); - // We didn't put much else into the query object but should be enough to check the prop - expect(passedProps.queries).toMatchObject([{ refId: 'A' }]); - }); - }); -}); - -const testItems = [ - { - metadata: { - name: 'TEST_QUERY', - creationTimestamp: '2025-01-01T11:11:11.00Z', - annotations: { - [AnnoKeyCreatedBy]: 'viewer:JohnDoe', - }, - }, - spec: { - title: 'Test Query title', - targets: [ - { - variables: {}, - properties: { - refId: 'A', - datasource: { - uid: 'Prometheus', - type: 'prometheus', - }, - }, - }, - ], - }, - }, -]; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx deleted file mode 100644 index 946e3de9eea..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { css } from '@emotion/css'; -import { uniqBy } from 'lodash'; -import { useEffect, useMemo, useState } from 'react'; - -import { AppEvents, GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { getAppEvents } from '@grafana/runtime'; -import { EmptyState, FilterInput, InlineLabel, MultiSelect, Spinner, useStyles2, Stack, Badge } from '@grafana/ui'; -import { t, Trans } from 'app/core/internationalization'; -import { useListQueryTemplateQuery } from 'app/features/query-library'; -import { QueryTemplate } from 'app/features/query-library/types'; - -import { convertDataQueryResponseToQueryTemplates } from '../../query-library/api/mappers'; - -import { QueryLibraryProps } from './QueryLibrary'; -import { queryLibraryTrackFilterDatasource } from './QueryLibraryAnalyticsEvents'; -import { QueryLibraryExpmInfo } from './QueryLibraryExpmInfo'; -import QueryTemplatesTable from './QueryTemplatesTable'; -import { useLoadQueryMetadata, useLoadUsers } from './utils/dataFetching'; -import { searchQueryLibrary } from './utils/search'; - -interface QueryTemplatesListProps extends QueryLibraryProps {} - -export function QueryTemplatesList(props: QueryTemplatesListProps) { - const { data: rawData, isLoading, error } = useListQueryTemplateQuery({}); - const data = useMemo(() => (rawData ? convertDataQueryResponseToQueryTemplates(rawData) : undefined), [rawData]); - const [isModalOpen, setIsModalOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - const [datasourceFilters, setDatasourceFilters] = useState>>( - props.activeDatasources?.map((ds) => ({ value: ds, label: ds })) || [] - ); - const [userFilters, setUserFilters] = useState>>([]); - const styles = useStyles2(getStyles); - - const loadUsersResult = useLoadUsersWithError(data); - const userNames = loadUsersResult.data ? loadUsersResult.data.display.map((user) => user.displayName) : []; - - const loadQueryMetadataResult = useLoadQueryMetadataWithError(data, loadUsersResult.data); - - // Filtering right now is done just on the frontend until there is better backend support for this. - const filteredRows = useMemo( - () => - searchQueryLibrary( - loadQueryMetadataResult.value || [], - searchQuery, - datasourceFilters.map((f) => f.value || ''), - userFilters.map((f) => f.value || '') - ), - [loadQueryMetadataResult.value, searchQuery, datasourceFilters, userFilters] - ); - - const datasourceNames = useMemo(() => { - return uniqBy(loadQueryMetadataResult.value, 'datasourceName').map((row) => row.datasourceName); - }, [loadQueryMetadataResult.value]); - - if (error instanceof Error) { - return ( - - {error.message} - - ); - } - - if (isLoading || loadUsersResult.isLoading || loadQueryMetadataResult.loading) { - return ; - } - - if (!data || data.length === 0) { - return ( - -

- { - "You haven't saved any queries to your library yet. Start adding them from Explore or your Query History tab." - } -

-
- ); - } - - return ( - <> - setIsModalOpen(false)} /> - - setSearchQuery(query)} - escapeRegex={false} - /> - - Datasource name(s): - - { - setDatasourceFilters(items); - actionMeta.action === 'select-option' && queryLibraryTrackFilterDatasource(); - }} - value={datasourceFilters} - options={datasourceNames.map((r) => { - return { value: r, label: r }; - })} - placeholder={'Filter queries for data sources(s)'} - aria-label={'Filter queries for data sources(s)'} - /> - - User name(s): - - { - setUserFilters(items); - actionMeta.action === 'select-option' && queryLibraryTrackFilterDatasource(); - }} - value={userFilters} - options={userNames.map((r) => { - return { value: r, label: r }; - })} - placeholder={'Filter queries for user name(s)'} - aria-label={'Filter queries for user name(s)'} - /> - setIsModalOpen(true)} - /> - - - - ); -} - -/** - * Wrap useLoadUsers with error handling. - * @param data - */ -function useLoadUsersWithError(data: QueryTemplate[] | undefined) { - const userUIDs = useMemo(() => data?.map((qt) => qt.user?.uid).filter((uid) => uid !== undefined), [data]); - const loadUsersResult = useLoadUsers(userUIDs); - useEffect(() => { - if (loadUsersResult.error) { - getAppEvents().publish({ - type: AppEvents.alertError.name, - payload: [ - t('query-library.user-info-get-error', 'Error attempting to get user info from the library: {{error}}', { - error: JSON.stringify(loadUsersResult.error), - }), - ], - }); - } - }, [loadUsersResult.error]); - return loadUsersResult; -} - -/** - * Wrap useLoadQueryMetadata with error handling. - * @param queryTemplates - * @param userDataList - */ -function useLoadQueryMetadataWithError( - queryTemplates: QueryTemplate[] | undefined, - userDataList: ReturnType['data'] -) { - const result = useLoadQueryMetadata(queryTemplates, userDataList); - - // useLoadQueryMetadata returns errors in the values so we filter and group them and later alert only one time for - // all the errors. This way we show data that is loaded even if some rows errored out. - // TODO: maybe we could show the rows with incomplete data to see exactly which ones errored out. I assume this - // can happen for example when data source for saved query was deleted. Would be nice if user would still be able - // to delete such row or decide what to do. - const [values, errors] = useMemo(() => { - let errors: Error[] = []; - let values = []; - if (!result.loading) { - for (const value of result.value!) { - if (value.error) { - errors.push(value.error); - } else { - values.push(value); - } - } - } - return [values, errors]; - }, [result]); - - useEffect(() => { - if (errors.length) { - getAppEvents().publish({ - type: AppEvents.alertError.name, - payload: [ - t('query-library.query-template-get-error', 'Error attempting to load query template metadata: {{error}}', { - error: JSON.stringify(errors), - }), - ], - }); - } - }, [errors]); - - return { - loading: result.loading, - value: values, - }; -} - -const getStyles = (theme: GrafanaTheme2) => ({ - searchInput: css({ - maxWidth: theme.spacing(55), - }), - multiSelect: css({ - maxWidth: theme.spacing(65), - }), - label: css({ - marginLeft: theme.spacing(1), - border: `1px solid ${theme.colors.secondary.border}`, - }), -}); diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx deleted file mode 100644 index 575f10bd063..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useState } from 'react'; - -import { getAppEvents } from '@grafana/runtime'; -import { IconButton, Modal } from '@grafana/ui'; -import { notifyApp } from 'app/core/actions'; -import { createSuccessNotification } from 'app/core/copy/appNotification'; -import { t } from 'app/core/internationalization'; -import { useDeleteQueryTemplateMutation } from 'app/features/query-library'; -import { dispatch } from 'app/store/store'; -import { ShowConfirmModalEvent } from 'app/types/events'; - -import { - queryLibaryTrackDeleteQuery, - queryLibraryTrackAddOrEditDescription, - queryLibraryTrackRunQuery, -} from '../QueryLibraryAnalyticsEvents'; -import { QueryTemplateForm } from '../QueryTemplateForm'; -import { QueryActionButton } from '../types'; - -import { useQueryLibraryListStyles } from './styles'; -import { QueryTemplateRow } from './types'; - -interface ActionsCellProps { - queryUid?: string; - queryTemplate: QueryTemplateRow; - rootDatasourceUid?: string; - QueryActionButton?: QueryActionButton; -} - -function ActionsCell({ queryTemplate, rootDatasourceUid, queryUid, QueryActionButton }: ActionsCellProps) { - const [deleteQueryTemplate] = useDeleteQueryTemplateMutation(); - const [editFormOpen, setEditFormOpen] = useState(false); - const styles = useQueryLibraryListStyles(); - - const onDeleteQuery = (queryUid: string) => { - const performDelete = (queryUid: string) => { - deleteQueryTemplate({ - name: queryUid, - deleteOptions: {}, - }); - dispatch(notifyApp(createSuccessNotification(t('explore.query-library.query-deleted', 'Query deleted')))); - queryLibaryTrackDeleteQuery(); - }; - - getAppEvents().publish( - new ShowConfirmModalEvent({ - title: t('explore.query-library.delete-query-title', 'Delete query'), - text: t( - 'explore.query-library.delete-query-text', - "You're about to remove this query from the query library. This action cannot be undone. Do you want to continue?" - ), - yesText: t('query-library.delete-query-button', 'Delete query'), - icon: 'trash-alt', - onConfirm: () => performDelete(queryUid), - }) - ); - }; - - return ( -
- { - if (queryUid) { - onDeleteQuery(queryUid); - } - }} - /> - { - setEditFormOpen(true); - queryLibraryTrackAddOrEditDescription(); - }} - /> - {QueryActionButton && ( - { - queryLibraryTrackRunQuery(queryTemplate.datasourceType || ''); - }} - /> - )} - setEditFormOpen(false)} - > - setEditFormOpen(false)} - templateData={queryTemplate} - onSave={() => { - setEditFormOpen(false); - }} - /> - -
- ); -} - -export default ActionsCell; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/AddedByCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/AddedByCell.tsx deleted file mode 100644 index 35793ab7fbd..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/AddedByCell.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Avatar } from '@grafana/ui'; -import { User } from 'app/features/query-library/types'; - -import { useQueryLibraryListStyles } from './styles'; - -type AddedByCellProps = { - user?: User; -}; -export function AddedByCell(props: AddedByCellProps) { - const styles = useQueryLibraryListStyles(); - - return ( -
- - - - {props.user?.displayName || 'Unknown'} -
- ); -} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DatasourceTypeCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DatasourceTypeCell.tsx deleted file mode 100644 index bdc361b7532..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DatasourceTypeCell.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { CellProps } from 'react-table'; - -import { useDatasource } from '../utils/useDatasource'; - -import { useQueryLibraryListStyles } from './styles'; -import { QueryTemplateRow } from './types'; - -export function DatasourceTypeCell(props: CellProps) { - const datasourceApi = useDatasource(props.row.original.datasourceRef); - const styles = useQueryLibraryListStyles(); - - return

{datasourceApi?.meta.name}

; -} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DateAddedCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DateAddedCell.tsx deleted file mode 100644 index 29bf6a96b07..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DateAddedCell.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { CellProps } from 'react-table'; - -import { dateTime } from '@grafana/data'; - -import { useQueryLibraryListStyles } from './styles'; -import { QueryTemplateRow } from './types'; - -export function DateAddedCell(props: CellProps) { - const styles = useQueryLibraryListStyles(); - const formattedTime = dateTime(props.row.original.createdAtTimestamp).format('YYYY-MM-DD HH:mm:ss'); - - return

{formattedTime}

; -} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx deleted file mode 100644 index f19e95f14c8..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { css, cx } from '@emotion/css'; -import { CellProps } from 'react-table'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Spinner, Tooltip, useStyles2 } from '@grafana/ui'; - -import { useDatasource } from '../utils/useDatasource'; - -import { useQueryLibraryListStyles } from './styles'; -import { QueryTemplateRow } from './types'; - -export function QueryDescriptionCell(props: CellProps) { - const datasourceApi = useDatasource(props.row.original.datasourceRef); - const queryLibraryListStyles = useQueryLibraryListStyles(); - const styles = useStyles2(getStyles); - - if (!datasourceApi) { - return ; - } - - if (!props.row.original.query) { - return
No queries
; - } - const queryDisplayText = props.row.original.queryText; - const description = props.row.original.description; - const dsName = props.row.original.datasourceName; - - return ( -
-

- {datasourceApi?.meta.info.description} - {dsName} -

- -

- {queryDisplayText} -

-
-

{description}

-
- ); -} - -const getStyles = (theme: GrafanaTheme2) => ({ - container: css({ - maxWidth: theme.spacing(60), - }), - queryDisplayText: css({ - backgroundColor: theme.colors.background.canvas, - }), -}); diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx deleted file mode 100644 index bd7b94e39ef..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { css } from '@emotion/css'; -import { SortByFn } from 'react-table'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Column, InteractiveTable, useStyles2 } from '@grafana/ui'; - -import { QueryActionButton } from '../types'; - -import ActionsCell from './ActionsCell'; -import { AddedByCell } from './AddedByCell'; -import { DatasourceTypeCell } from './DatasourceTypeCell'; -import { DateAddedCell } from './DateAddedCell'; -import { QueryDescriptionCell } from './QueryDescriptionCell'; -import { QueryTemplateRow } from './types'; - -const timestampSort: SortByFn = (rowA, rowB, _, desc) => { - const timeA = rowA.original.createdAtTimestamp || 0; - const timeB = rowB.original.createdAtTimestamp || 0; - return desc ? timeA - timeB : timeB - timeA; -}; - -function createColumns(queryActionButton?: QueryActionButton): Array> { - return [ - { id: 'description', header: 'Data source and query', cell: QueryDescriptionCell }, - { id: 'addedBy', header: 'Added by', cell: ({ row: { original } }) => }, - { id: 'datasourceType', header: 'Datasource type', cell: DatasourceTypeCell, sortType: 'string' }, - { id: 'createdAtTimestamp', header: 'Date added', cell: DateAddedCell, sortType: timestampSort }, - { - id: 'actions', - header: '', - cell: ({ row: { original } }) => ( - - ), - }, - ]; -} - -type Props = { - queryTemplateRows: QueryTemplateRow[]; - queryActionButton?: QueryActionButton; -}; - -export default function QueryTemplatesTable({ queryTemplateRows, queryActionButton }: Props) { - const styles = useStyles2(getStyles); - const columns = createColumns(queryActionButton); - - return ( - row.index} - pageSize={20} - className={styles.table} - /> - ); -} - -const getStyles = (theme: GrafanaTheme2) => ({ - table: css({ - 'tbody tr': { - position: 'relative', - backgroundColor: theme.colors.background.secondary, - borderCollapse: 'collapse', - borderBottom: 'unset', - overflow: 'hidden', // Ensure the row doesn't overflow and cause additonal scrollbars - }, - /* Adds the pseudo-element for the lines between table rows */ - 'tbody tr::after': { - content: '""', - position: 'absolute', - inset: 'auto 0 0 0', - height: theme.spacing(0.5), - backgroundColor: theme.colors.background.primary, - }, - }), -}); diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx deleted file mode 100644 index 715cd5d235b..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { css } from '@emotion/css'; - -import { GrafanaTheme2 } from '@grafana/data/'; -import { useStyles2 } from '@grafana/ui/'; - -export const useQueryLibraryListStyles = () => { - return useStyles2(getStyles); -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - logo: css({ - marginRight: theme.spacing(2), - width: '16px', - }), - header: css({ - margin: 0, - fontSize: theme.typography.h5.fontSize, - color: theme.colors.text.secondary, - }), - mainText: css({ - margin: 0, - fontSize: theme.typography.body.fontSize, - textOverflow: 'ellipsis', - }), - otherText: css({ - margin: 0, - fontSize: theme.typography.body.fontSize, - color: theme.colors.text.secondary, - textOverflow: 'ellipsis', - }), - singleLine: css({ - display: '-webkit-box', - WebkitBoxOrient: 'vertical', - WebkitLineClamp: 1, - overflow: 'hidden', - }), - cell: css({ - display: 'flex', - alignItems: 'center', - '&:last-child': { - justifyContent: 'end', - }, - }), - actionButton: css({ - padding: theme.spacing(1), - }), -}); diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts deleted file mode 100644 index 51f5e490953..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DataQuery, DataSourceRef } from '@grafana/schema'; -import { User } from 'app/features/query-library/types'; - -export type QueryTemplateRow = { - index: string; - datasourceName?: string; - description?: string; - query?: DataQuery; - queryText?: string; - datasourceRef?: DataSourceRef | null; - datasourceType?: string; - createdAtTimestamp?: number; - user?: User; - uid?: string; -}; diff --git a/public/app/features/explore/QueryLibrary/SaveQueryButton.tsx b/public/app/features/explore/QueryLibrary/SaveQueryButton.tsx deleted file mode 100644 index 07c75179ed6..00000000000 --- a/public/app/features/explore/QueryLibrary/SaveQueryButton.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useLocalStorage } from 'react-use'; - -import { DataQuery } from '@grafana/schema'; -import { Badge } from '@grafana/ui'; - -import { QueryOperationAction } from '../../../core/components/QueryOperationRow/QueryOperationAction'; -import { t } from '../../../core/internationalization'; - -import { QUERY_LIBRARY_LOCAL_STORAGE_KEYS } from './QueryLibrary'; -import { useQueryLibraryContext } from './QueryLibraryContext'; - -interface Props { - query: DataQuery; -} - -export function SaveQueryButton({ query }: Props) { - const { openAddQueryModal } = useQueryLibraryContext(); - - const [showQueryLibraryBadgeButton, setShowQueryLibraryBadgeButton] = useLocalStorage( - QUERY_LIBRARY_LOCAL_STORAGE_KEYS.explore.newButton, - true - ); - - return showQueryLibraryBadgeButton ? ( - { - openAddQueryModal(query); - setShowQueryLibraryBadgeButton(false); - }} - style={{ cursor: 'pointer' }} - /> - ) : ( - { - openAddQueryModal(query); - }} - /> - ); -} diff --git a/public/app/features/explore/QueryLibrary/utils/dataFetching.ts b/public/app/features/explore/QueryLibrary/utils/dataFetching.ts deleted file mode 100644 index e874d6a331e..00000000000 --- a/public/app/features/explore/QueryLibrary/utils/dataFetching.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/query'; -import { compact, uniq } from 'lodash'; -import { useAsync } from 'react-use'; -import { AsyncState } from 'react-use/lib/useAsync'; - -import { getDataSourceSrv } from '@grafana/runtime'; -import { DataQuery, DataSourceRef } from '@grafana/schema'; - -import { createQueryText } from '../../../../core/utils/richHistory'; -import { useGetDisplayMappingQuery } from '../../../iam'; -import { getDatasourceSrv } from '../../../plugins/datasource_srv'; -import { QueryTemplate } from '../../../query-library/types'; - -export function useLoadUsers(userUIDs: string[] | undefined) { - const userQtList = uniq(compact(userUIDs)); - return useGetDisplayMappingQuery( - userUIDs - ? { - key: userQtList, - } - : skipToken - ); -} - -// Explicitly type the result so TS knows to discriminate between the error result and good result by the error prop -// value. -type MetadataValue = - | { - index: string; - uid: string; - datasourceName: string; - datasourceRef: DataSourceRef | undefined | null; - datasourceType: string; - createdAtTimestamp: number; - query: DataQuery; - queryText: string; - description: string; - user: { - uid: string; - displayName: string; - avatarUrl: string; - }; - error: undefined; - } - | { - index: string; - error: Error; - }; - -/** - * Map metadata to query templates we get from the DB. - * @param queryTemplates - * @param userDataList - */ -export function useLoadQueryMetadata( - queryTemplates: QueryTemplate[] | undefined, - userDataList: ReturnType['data'] -): AsyncState { - return useAsync(async () => { - if (!(queryTemplates && userDataList)) { - return []; - } - - const rowsPromises = queryTemplates.map( - async (queryTemplate: QueryTemplate, index: number): Promise => { - try { - const datasourceRef = queryTemplate.targets[0]?.datasource; - const datasourceApi = await getDataSourceSrv().get(datasourceRef); - const datasourceType = getDatasourceSrv().getInstanceSettings(datasourceRef)?.meta.name || ''; - const query = queryTemplate.targets[0]; - const queryText = createQueryText(query, datasourceApi); - const datasourceName = datasourceApi?.name || ''; - const extendedUserData = userDataList.display.find( - (user) => `${user?.identity.type}:${user?.identity.name}` === queryTemplate.user?.uid - ); - - return { - index: index.toString(), - uid: queryTemplate.uid, - datasourceName, - datasourceRef, - datasourceType, - createdAtTimestamp: queryTemplate?.createdAtTimestamp || 0, - query, - queryText, - description: queryTemplate.title, - user: { - uid: queryTemplate.user?.uid || '', - displayName: extendedUserData?.displayName || '', - avatarUrl: extendedUserData?.avatarURL || '', - }, - error: undefined, - }; - } catch (error) { - // Instead of throwing we collect the errors in the result so upstream code can decide what to do. - return { - index: index.toString(), - error: error instanceof Error ? error : new Error('unknown error ' + JSON.stringify(error)), - }; - } - } - ); - - return Promise.all(rowsPromises); - }, [queryTemplates, userDataList]); -} diff --git a/public/app/features/explore/QueryLibrary/utils/search.ts b/public/app/features/explore/QueryLibrary/utils/search.ts deleted file mode 100644 index 2c55be7e9d0..00000000000 --- a/public/app/features/explore/QueryLibrary/utils/search.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { QueryTemplateRow } from '../QueryTemplatesTable/types'; - -export const searchQueryLibrary = ( - queryLibrary: QueryTemplateRow[], - query: string, - dsFilters: string[], - userNameFilters: string[] -) => { - const result = queryLibrary.filter((item) => { - const matchesDsFilter = - dsFilters.length === 0 || dsFilters.some((f) => item.datasourceName?.toLowerCase().includes(f.toLowerCase())); - const matchesUserNameFilter = - userNameFilters.length === 0 || userNameFilters.includes(item.user?.displayName || ''); - return ( - (item.datasourceName?.toLowerCase().includes(query.toLowerCase()) || - item.datasourceType?.toLowerCase().includes(query.toLowerCase()) || - item.description?.toLowerCase().includes(query.toLowerCase()) || - item.queryText?.toLowerCase().includes(query.toLowerCase())) && - matchesDsFilter && - matchesUserNameFilter - ); - }); - return result; -}; diff --git a/public/app/features/explore/QueryLibrary/utils/useDatasource.tsx b/public/app/features/explore/QueryLibrary/utils/useDatasource.tsx deleted file mode 100644 index 1ea4fd29b76..00000000000 --- a/public/app/features/explore/QueryLibrary/utils/useDatasource.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useAsync } from 'react-use'; - -import { getDataSourceSrv } from '@grafana/runtime'; -import { DataSourceRef } from '@grafana/schema'; - -export function useDatasource(dataSourceRef?: DataSourceRef | null) { - const { value } = useAsync(async () => await getDataSourceSrv().get(dataSourceRef), [dataSourceRef]); - return value; -} diff --git a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx index e6aa0e25143..8e7a057a453 100644 --- a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx @@ -1,57 +1,32 @@ import { useState } from 'react'; import { DataQuery } from '@grafana/schema'; -import { Button, Modal } from '@grafana/ui'; +import { Button } from '@grafana/ui'; import { t } from 'app/core/internationalization'; -import { isQueryLibraryEnabled, useListQueryTemplateQuery } from 'app/features/query-library'; -import { - queryLibraryTrackAddFromQueryHistory, - queryLibraryTrackAddFromQueryHistoryAddModalShown, -} from '../QueryLibrary/QueryLibraryAnalyticsEvents'; -import { QueryTemplateForm } from '../QueryLibrary/QueryTemplateForm'; +import { useQueryLibraryContext } from '../QueryLibrary/QueryLibraryContext'; type Props = { query: DataQuery; }; export const RichHistoryAddToLibrary = ({ query }: Props) => { - const { refetch } = useListQueryTemplateQuery({}); - const [isOpen, setIsOpen] = useState(false); const [hasBeenSaved, setHasBeenSaved] = useState(false); + const { openAddQueryModal, queryLibraryEnabled } = useQueryLibraryContext(); const buttonLabel = t('explore.rich-history-card.add-to-library', 'Add to library'); - return isQueryLibraryEnabled() && !hasBeenSaved ? ( + return queryLibraryEnabled && !hasBeenSaved ? ( <> - setIsOpen(false)} - > - setIsOpen(() => false)} - queryToAdd={query} - onSave={(isSuccess) => { - if (isSuccess) { - setIsOpen(false); - setHasBeenSaved(true); - refetch(); - queryLibraryTrackAddFromQueryHistory(query.datasource?.type || ''); - } - }} - /> - ) : undefined; }; diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index c57cf52c435..78aa044cab3 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -4,6 +4,7 @@ import { createMemoryHistory } from 'history'; import { KBarProvider } from 'kbar'; import { fromPairs } from 'lodash'; import { stringify } from 'querystring'; +import { ComponentType, ReactNode } from 'react'; import { Provider } from 'react-redux'; // eslint-disable-next-line no-restricted-imports import { Route, Router } from 'react-router-dom'; @@ -47,7 +48,6 @@ import { ExploreQueryParams } from '../../../../types'; import { initialUserState } from '../../../profile/state/reducers'; import ExplorePage from '../../ExplorePage'; import { QueriesDrawerContextProvider } from '../../QueriesDrawer/QueriesDrawerContext'; -import { QueryLibraryContextProvider } from '../../QueryLibrary/QueryLibraryContext'; type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; @@ -60,6 +60,7 @@ type SetupOptions = { failAddToLibrary?: boolean; // Use AppChrome wrapper around ExplorePage - needed to test query library/history withAppChrome?: boolean; + provider?: ComponentType<{ children: ReactNode }>; }; type TearDownOptions = { @@ -179,12 +180,18 @@ export function setupExplore(options?: SetupOptions): { const contextMock = getGrafanaContextMock({ location }); + const FinalProvider = + options?.provider || + (({ children }) => { + return children; + }); + const { unmount, container } = render( - - + + {options?.withAppChrome ? ( @@ -204,8 +211,8 @@ export function setupExplore(options?: SetupOptions): { render={(props) => } /> )} - - + + diff --git a/public/app/features/explore/spec/queryLibrary.test.tsx b/public/app/features/explore/spec/queryLibrary.test.tsx deleted file mode 100644 index 8de9796fdf0..00000000000 --- a/public/app/features/explore/spec/queryLibrary.test.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { Props } from 'react-virtualized-auto-sizer'; - -import { EventBusSrv } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { DataQuery } from '@grafana/schema/dist/esm/veneer/common.types'; - -import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput'; - -import { - assertAddToQueryLibraryButtonExists, - assertQueryHistory, - assertQueryLibraryTemplateExists, -} from './helper/assert'; -import { - addQueryHistoryToQueryLibrary, - openQueryHistory, - openQueryLibrary, - submitAddToQueryLibrary, -} from './helper/interactions'; -import { setupExplore, waitForExplore } from './helper/setup'; - -const reportInteractionMock = jest.fn(); -const testEventBus = new EventBusSrv(); -testEventBus.publish = jest.fn(); - -interface MockQuery extends DataQuery { - expr: string; -} - -jest.mock('../QueryLibrary/utils/dataFetching', () => { - return { - __esModule: true, - ...jest.requireActual('../QueryLibrary/utils/dataFetching'), - useLoadUsers: () => { - return { - data: { - display: [ - { - avatarUrl: '', - displayName: 'john doe', - identity: { - name: 'JohnDoe', - type: 'viewer', - }, - }, - ], - }, - isLoading: false, - error: null, - }; - }, - }; -}); - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - reportInteraction: (...args: object[]) => { - reportInteractionMock(...args); - }, - getAppEvents: () => testEventBus, - usePluginLinks: jest.fn().mockReturnValue({ links: [] }), -})); - -jest.mock('app/core/core', () => ({ - contextSrv: { - hasPermission: () => true, - isSignedIn: true, - getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, - user: { - isSignedIn: true, - }, - }, -})); - -jest.mock('app/core/services/PreferencesService', () => ({ - PreferencesService: function () { - return { - patch: jest.fn(), - load: jest.fn().mockResolvedValue({ - queryHistory: { - homeTab: 'query', - }, - }), - }; - }, -})); - -jest.mock('../hooks/useExplorePageTitle', () => ({ - useExplorePageTitle: jest.fn(), -})); - -jest.mock('react-virtualized-auto-sizer', () => { - return { - __esModule: true, - default(props: Props) { - return
{props.children({ height: 1, scaledHeight: 1, scaledWidth: 1000, width: 1000 })}
; - }, - }; -}); - -function setupQueryLibrary() { - const mockQuery: MockQuery = { refId: 'TEST', expr: 'TEST' }; - setupExplore({ - queryHistory: { - queryHistory: [{ datasourceUid: 'loki', queries: [mockQuery] }], - totalCount: 1, - }, - withAppChrome: true, - }); -} - -let previousQueryLibraryEnabled: boolean | undefined; -let previousQueryHistoryEnabled: boolean; - -describe('QueryLibrary', () => { - silenceConsoleOutput(); - - beforeAll(() => { - previousQueryLibraryEnabled = config.featureToggles.queryLibrary; - previousQueryHistoryEnabled = config.queryHistoryEnabled; - - config.featureToggles.queryLibrary = true; - config.queryHistoryEnabled = true; - }); - - afterAll(() => { - config.featureToggles.queryLibrary = previousQueryLibraryEnabled; - config.queryHistoryEnabled = previousQueryHistoryEnabled; - jest.restoreAllMocks(); - }); - - it('Load query templates', async () => { - setupQueryLibrary(); - await waitForExplore(); - await openQueryLibrary(); - await assertQueryLibraryTemplateExists('loki', 'Loki Query Template'); - }); - - it('Shows add to query library button only when the toggle is enabled', async () => { - setupQueryLibrary(); - await waitForExplore(); - await openQueryHistory(); - await assertQueryHistory(['{"expr":"TEST"}']); - await assertAddToQueryLibraryButtonExists(true); - }); - - it('Does not show the query library button when the toggle is disabled', async () => { - config.featureToggles.queryLibrary = false; - setupQueryLibrary(); - await waitForExplore(); - await openQueryHistory(); - await assertQueryHistory(['{"expr":"TEST"}']); - await assertAddToQueryLibraryButtonExists(false); - config.featureToggles.queryLibrary = true; - }); - - it('Shows a notification when a template is added and hides the add button', async () => { - setupQueryLibrary(); - await waitForExplore(); - await openQueryHistory(); - await assertQueryHistory(['{"expr":"TEST"}']); - await addQueryHistoryToQueryLibrary(); - await submitAddToQueryLibrary({ description: 'Test' }); - expect(testEventBus.publish).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'alert-success', - payload: ['Query successfully saved to the library'], - }) - ); - await assertAddToQueryLibraryButtonExists(false); - }); -}); diff --git a/public/app/features/query-library/index.ts b/public/app/features/query-library/index.ts index 246ca735681..7f0c6f1c325 100644 --- a/public/app/features/query-library/index.ts +++ b/public/app/features/query-library/index.ts @@ -7,8 +7,6 @@ * @alpha */ -import { config } from '@grafana/runtime'; - import { QUERY_LIBRARY_GET_LIMIT } from './api/api'; import { generatedQueryLibraryApi } from './api/endpoints.gen'; import { mockData } from './api/mocks'; @@ -46,10 +44,6 @@ export const { }, }); -export function isQueryLibraryEnabled() { - return config.featureToggles.queryLibrary; -} - export const QueryLibraryMocks = { data: mockData.all, }; diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index a3dc05930d7..36a3d5818ee 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -8,7 +8,6 @@ import { PureComponent, ReactNode } from 'react'; // Utils & Services import { CoreApp, - DataQuery, DataSourceApi, DataSourceInstanceSettings, DataSourcePluginContextProvider, @@ -24,7 +23,8 @@ import { toLegacyResponseData, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { AngularComponent, config, getAngularLoader, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { AngularComponent, getAngularLoader, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { Badge, ErrorBoundaryAlert } from '@grafana/ui'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; import { @@ -40,7 +40,7 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { SaveQueryButton as SaveQueryToQueryLibraryButton } from '../../explore/QueryLibrary/SaveQueryButton'; +import { useQueryLibraryContext } from '../../explore/QueryLibrary/QueryLibraryContext'; import { QueryActionComponent, RowActionComponents } from './QueryActionComponent'; import { QueryEditorRowHeader } from './QueryEditorRowHeader'; @@ -489,7 +489,7 @@ export class QueryEditorRow extends PureComponent )} {this.renderExtraActions()} - {config.featureToggles.queryLibrary && } + > }) { + return props.providers.reduce((tree, Provider): ReactNode => { + return {tree}; + }, props.children); +} type RouterWrapperProps = { routes?: JSX.Element | false; bodyRenderHooks: ComponentType[]; pageBanners: ComponentType[]; + providers: Array>; }; export function RouterWrapper(props: RouterWrapperProps) { return ( @@ -31,7 +37,7 @@ export function RouterWrapper(props: RouterWrapperProps) { - + @@ -48,7 +54,7 @@ export function RouterWrapper(props: RouterWrapperProps) { - + diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 66492eac6b1..f4bbcfbcf7c 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1346,36 +1346,6 @@ "scan-for-older-logs": "Scan for older logs", "stop-scan": "Stop scan" }, - "query-library": { - "add-edit-description": "Add/edit description", - "cancel": "Cancel", - "default-description": "Public", - "delete-query": "Delete query", - "delete-query-text": "You're about to remove this query from the query library. This action cannot be undone. Do you want to continue?", - "delete-query-title": "Delete query", - "private": "Private", - "public": "Public", - "query-deleted": "Query deleted", - "query-template-add-error": "Error attempting to save this query to the library", - "query-template-added": "Query successfully saved to the library", - "query-template-edit-error": "Error attempting to edit this query", - "query-template-edited": "Query template successfully edited", - "save": "Save" - }, - "query-template-modal": { - "add-info": "You're about to save this query. Once saved, you can easily access it in the Query Library tab for future use and reference.", - "add-title": "Add query to Query Library", - "auto-star": "Auto-star this query to add it to your starred list in the Query Library.", - "data-source-name": "Data source name", - "description": "Description", - "edit-info": "You're about to edit this query. Once saved, you can easily access it in the Query Library tab for future use and reference.", - "edit-title": "Edit query", - "query": "Query", - "visibility": "Visibility" - }, - "query-template-modall": { - "data-source-type": "Data source type" - }, "rich-history": { "close-tooltip": "Close query history", "datasource-a-z": "Data source A-Z", @@ -2993,14 +2963,6 @@ "role-label": "Role" } }, - "query-library": { - "datasource-names": "Datasource name(s):", - "delete-query-button": "Delete query", - "query-template-get-error": "Error attempting to load query template metadata: {{error}}", - "search": "Search by data source, query content or description", - "user-info-get-error": "Error attempting to get user info from the library: {{error}}", - "user-names": "User name(s):" - }, "query-operation": { "header": { "collapse-row": "Collapse query row", @@ -3010,8 +2972,6 @@ "expand-row": "Expand query row", "hide-response": "Hide response", "remove-query": "Remove query", - "save-to-query-library": "Save to query library", - "save-to-query-library-new": "New: Save to query library", "show-response": "Show response", "toggle-edit-mode": "Toggle text edit mode" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 193643a5111..af213f5ae27 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1346,36 +1346,6 @@ "scan-for-older-logs": "Ŝčäʼn ƒőř őľđęř ľőģş", "stop-scan": "Ŝŧőp şčäʼn" }, - "query-library": { - "add-edit-description": "Åđđ/ęđįŧ đęşčřįpŧįőʼn", - "cancel": "Cäʼnčęľ", - "default-description": "Pūþľįč", - "delete-query": "Đęľęŧę qūęřy", - "delete-query-text": "Ÿőū'řę äþőūŧ ŧő řęmővę ŧĥįş qūęřy ƒřőm ŧĥę qūęřy ľįþřäřy. Ŧĥįş äčŧįőʼn čäʼnʼnőŧ þę ūʼnđőʼnę. Đő yőū ŵäʼnŧ ŧő čőʼnŧįʼnūę?", - "delete-query-title": "Đęľęŧę qūęřy", - "private": "Přįväŧę", - "public": "Pūþľįč", - "query-deleted": "Qūęřy đęľęŧęđ", - "query-template-add-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő şävę ŧĥįş qūęřy ŧő ŧĥę ľįþřäřy", - "query-template-added": "Qūęřy şūččęşşƒūľľy şävęđ ŧő ŧĥę ľįþřäřy", - "query-template-edit-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ęđįŧ ŧĥįş qūęřy", - "query-template-edited": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy ęđįŧęđ", - "save": "Ŝävę" - }, - "query-template-modal": { - "add-info": "Ÿőū'řę äþőūŧ ŧő şävę ŧĥįş qūęřy. Øʼnčę şävęđ, yőū čäʼn ęäşįľy äččęşş įŧ įʼn ŧĥę Qūęřy Ŀįþřäřy ŧäþ ƒőř ƒūŧūřę ūşę äʼnđ řęƒęřęʼnčę.", - "add-title": "Åđđ qūęřy ŧő Qūęřy Ŀįþřäřy", - "auto-star": "Åūŧő-şŧäř ŧĥįş qūęřy ŧő äđđ įŧ ŧő yőūř şŧäřřęđ ľįşŧ įʼn ŧĥę Qūęřy Ŀįþřäřy.", - "data-source-name": "Đäŧä şőūřčę ʼnämę", - "description": "Đęşčřįpŧįőʼn", - "edit-info": "Ÿőū'řę äþőūŧ ŧő ęđįŧ ŧĥįş qūęřy. Øʼnčę şävęđ, yőū čäʼn ęäşįľy äččęşş įŧ įʼn ŧĥę Qūęřy Ŀįþřäřy ŧäþ ƒőř ƒūŧūřę ūşę äʼnđ řęƒęřęʼnčę.", - "edit-title": "Ēđįŧ qūęřy", - "query": "Qūęřy", - "visibility": "Vįşįþįľįŧy" - }, - "query-template-modall": { - "data-source-type": "Đäŧä şőūřčę ŧypę" - }, "rich-history": { "close-tooltip": "Cľőşę qūęřy ĥįşŧőřy", "datasource-a-z": "Đäŧä şőūřčę Å-Ż", @@ -2993,14 +2963,6 @@ "role-label": "Ŗőľę" } }, - "query-library": { - "datasource-names": "Đäŧäşőūřčę ʼnämę(ş):", - "delete-query-button": "Đęľęŧę qūęřy", - "query-template-get-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ľőäđ qūęřy ŧęmpľäŧę męŧäđäŧä: {{error}}", - "search": "Ŝęäřčĥ þy đäŧä şőūřčę, qūęřy čőʼnŧęʼnŧ őř đęşčřįpŧįőʼn", - "user-info-get-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ģęŧ ūşęř įʼnƒő ƒřőm ŧĥę ľįþřäřy: {{error}}", - "user-names": "Ůşęř ʼnämę(ş):" - }, "query-operation": { "header": { "collapse-row": "Cőľľäpşę qūęřy řőŵ", @@ -3010,8 +2972,6 @@ "expand-row": "Ēχpäʼnđ qūęřy řőŵ", "hide-response": "Ħįđę řęşpőʼnşę", "remove-query": "Ŗęmővę qūęřy", - "save-to-query-library": "Ŝävę ŧő qūęřy ľįþřäřy", - "save-to-query-library-new": "Ńęŵ: Ŝävę ŧő qūęřy ľįþřäřy", "show-response": "Ŝĥőŵ řęşpőʼnşę", "toggle-edit-mode": "Ŧőģģľę ŧęχŧ ęđįŧ mőđę" }, From a51e785bc17e7f1683ea41187be05a6dbd5d2124 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Fri, 7 Feb 2025 12:57:54 +0200 Subject: [PATCH 415/894] Dashboards: Finalize refactoring for dynamic dashboards (#100198) --- .../edit-pane/ElementSelection.ts | 2 +- .../MultiSelectedObjectsEditableElement.tsx | 30 +- .../MultiSelectedVizPanelsEditableElement.tsx | 45 +-- .../edit-pane/VizPanelEditableElement.tsx | 2 +- .../dashboard-scene/scene/DashboardScene.tsx | 4 +- .../layout-default/DashboardGridItem.tsx | 220 +++-------- .../DashboardGridItemEditor.tsx | 11 +- .../DashboardGridItemRenderer.tsx | 82 ++++ ...hboardGridItemVariableDependencyHandler.ts | 31 ++ .../DefaultGridLayoutManager.tsx | 234 +++++------ .../RowRepeaterBehavior.test.tsx | 12 +- .../layout-default/RowRepeaterBehavior.ts | 4 - .../layout-default/row-actions/RowActions.tsx | 140 +------ .../row-actions/RowActionsRenderer.tsx | 92 +++++ .../row-actions/RowOptionsButton.tsx | 18 +- .../row-actions/RowOptionsForm.test.tsx | 12 +- .../row-actions/RowOptionsForm.tsx | 33 +- .../row-actions/RowOptionsModal.tsx | 14 +- .../ResponsiveGridItem.tsx | 77 +--- .../ResponsiveGridItemEditor.tsx | 29 ++ .../ResponsiveGridItemRenderer.tsx | 27 ++ .../ResponsiveGridLayoutManager.tsx | 134 ++----- .../ResponsiveGridLayoutManagerEditor.tsx | 83 ++++ .../MultiSelectedRowItemsElement.tsx | 89 ----- .../scene/layout-rows/RowItem.tsx | 370 +++--------------- .../scene/layout-rows/RowItemEditor.tsx | 144 +++++++ .../scene/layout-rows/RowItemRenderer.tsx | 122 ++++++ .../RowItemRepeaterBehavior.test.tsx | 6 +- .../layout-rows/RowItemRepeaterBehavior.ts | 4 - .../scene/layout-rows/RowItems.tsx | 39 ++ .../scene/layout-rows/RowItemsEditor.tsx | 64 +++ .../scene/layout-rows/RowsLayoutManager.tsx | 131 ++----- .../layout-rows/RowsLayoutManagerRenderer.tsx | 32 ++ .../scene/types/BulkActionElement.ts | 7 + .../scene/types/DashboardLayoutManager.ts | 4 +- .../scene/types/LayoutParent.ts | 9 + .../MultiSelectedEditableDashboardElement.ts | 5 + .../utils/dashboardSceneGraph.ts | 11 +- public/locales/en-US/grafana.json | 25 +- public/locales/pseudo-LOCALE/grafana.json | 25 +- 40 files changed, 1209 insertions(+), 1214 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemRenderer.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemVariableDependencyHandler.ts create mode 100644 public/app/features/dashboard-scene/scene/layout-default/row-actions/RowActionsRenderer.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItemEditor.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItemRenderer.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManagerEditor.tsx delete mode 100644 public/app/features/dashboard-scene/scene/layout-rows/MultiSelectedRowItemsElement.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-rows/RowItems.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx diff --git a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts index 7a5a9ee61af..cecd28020cf 100644 --- a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts +++ b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts @@ -152,7 +152,7 @@ export class ElementSelection { const firstObj = this.selectedObjects?.values().next().value?.resolve(); if (firstObj instanceof VizPanel) { - return new MultiSelectedVizPanelsEditableElement(sceneObjects); + return new MultiSelectedVizPanelsEditableElement(sceneObjects.filter((obj) => obj instanceof VizPanel)); } if (isEditableDashboardElement(firstObj!)) { diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx index 79724e4474e..3278deb3a17 100644 --- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx @@ -1,4 +1,5 @@ import { ReactNode } from 'react'; +import { v4 as uuidv4 } from 'uuid'; import { Stack, Text, Button } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; @@ -9,31 +10,32 @@ import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelec export class MultiSelectedObjectsEditableElement implements MultiSelectedEditableDashboardElement { public readonly isMultiSelectedEditableDashboardElement = true; public readonly typeName = 'Objects'; + public readonly key: string; - private items?: BulkActionElement[]; - - constructor(items: BulkActionElement[]) { - this.items = items; + constructor(private _elements: BulkActionElement[]) { + this.key = uuidv4(); } - public onDelete = () => { - for (const item of this.items || []) { - item.onDelete(); - } - }; - - renderActions(): ReactNode { + public renderActions(): ReactNode { return ( - No. of objects selected: - {this.items?.length} + + No. of objects selected: {{ length }} + - {!isClone && isEditing && ( -
- )} - {!isCollapsed && } -
- ); - }; -} + public createMultiSelectedElement(items: SceneObject[]): RowItems { + return new RowItems(items.filter((item) => item instanceof RowItem)); + } -function getStyles(theme: GrafanaTheme2) { - return { - rowHeader: css({ - width: '100%', - display: 'flex', - gap: theme.spacing(1), - padding: theme.spacing(0, 0, 0.5, 0), - margin: theme.spacing(0, 0, 1, 0), - alignItems: 'center', + public getRepeatVariable(): string | undefined { + return this._getRepeatBehavior()?.state.variableName; + } - '&:hover, &:focus-within': { - '& > div': { - opacity: 1, - }, - }, + public onChangeTitle(title: string) { + this.setState({ title }); + } - '& > div': { - marginBottom: 0, - marginRight: theme.spacing(1), - }, - }), - rowTitleButton: css({ - display: 'flex', - alignItems: 'center', - cursor: 'pointer', - background: 'transparent', - border: 'none', - minWidth: 0, - gap: theme.spacing(1), - }), - rowTitle: css({ - fontSize: theme.typography.h5.fontSize, - fontWeight: theme.typography.fontWeightMedium, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - maxWidth: '100%', - flexGrow: 1, - minWidth: 0, - }), - wrapper: css({ - display: 'flex', - flexDirection: 'column', - width: '100%', - minHeight: '100px', - }), - wrapperGrow: css({ - flexGrow: 1, - }), - wrapperCollapsed: css({ - flexGrow: 0, - borderBottom: `1px solid ${theme.colors.border.weak}`, - minHeight: 'unset', - }), - rowActions: css({ - display: 'flex', - opacity: 0, - }), - }; -} + public onHeaderHiddenToggle(isHeaderHidden = !this.state.isHeaderHidden) { + this.setState({ isHeaderHidden }); + } -export function RowTitleInput({ row }: { row: RowItem }) { - const { title } = row.useState(); + public onChangeHeight(height: 'expand' | 'min') { + this.setState({ height }); + } - return row.setState({ title: e.currentTarget.value })} />; -} + public onChangeRepeat(repeat: string | undefined) { + let repeatBehavior = this._getRepeatBehavior(); -export function RowHeaderSwitch({ row }: { row: RowItem }) { - const { isHeaderHidden = false } = row.useState(); - - return ( - { - row.setState({ - isHeaderHidden: !row.state.isHeaderHidden, - }); - }} - /> - ); -} - -export function RowHeightSelect({ row }: { row: RowItem }) { - const { height = 'expand' } = row.useState(); - - const options: Array> = [ - { label: t('dashboard.rows-layout.row-options.height.expand', 'Expand'), value: 'expand' }, - { label: t('dashboard.rows-layout.row-options.height.min', 'Min'), value: 'min' }, - ]; - - return ( - - row.setState({ - height: option, - }) + if (repeat) { + // Remove repeat behavior if it exists to trigger repeat when adding new one + if (repeatBehavior) { + repeatBehavior.removeBehavior(); } - /> - ); -} -export function RowRepeatSelect({ row, dashboard }: { row: RowItem; dashboard: DashboardScene }) { - const { layout, $behaviors } = row.useState(); + repeatBehavior = new RowItemRepeaterBehavior({ variableName: repeat }); + this.setState({ $behaviors: [...(this.state.$behaviors ?? []), repeatBehavior] }); + repeatBehavior.activate(); + } else { + repeatBehavior?.removeBehavior(); + } + } - let repeatBehavior: RowItemRepeaterBehavior | undefined = $behaviors?.find( - (b) => b instanceof RowItemRepeaterBehavior - ); - const { variableName } = repeatBehavior?.state ?? {}; + public onCollapseToggle() { + this.setState({ isCollapsed: !this.state.isCollapsed }); + } - const isAnyPanelUsingDashboardDS = layout.getVizPanels().some((vizPanel) => { - const runner = getQueryRunnerFor(vizPanel); - return ( - runner?.state.datasource?.uid === SHARED_DASHBOARD_QUERY || - (runner?.state.datasource?.uid === MIXED_DATASOURCE_NAME && - runner?.state.queries.some((query) => query.datasource?.uid === SHARED_DASHBOARD_QUERY)) - ); - }); - - return ( - <> - { - if (repeat) { - // Remove repeat behavior if it exists to trigger repeat when adding new one - if (repeatBehavior) { - repeatBehavior.removeBehavior(); - } - - repeatBehavior = new RowItemRepeaterBehavior({ variableName: repeat }); - row.setState({ $behaviors: [...(row.state.$behaviors ?? []), repeatBehavior] }); - repeatBehavior.activate(); - } else { - repeatBehavior?.removeBehavior(); - } - }} - /> - {isAnyPanelUsingDashboardDS ? ( - -

- - Panels in this row use the {{ SHARED_DASHBOARD_QUERY }} data source. These panels will reference the panel - in the original row, not the ones in the repeated rows. - -

- - Learn more - -
- ) : undefined} - - ); + private _getRepeatBehavior(): RowItemRepeaterBehavior | undefined { + return this.state.$behaviors?.find((b) => b instanceof RowItemRepeaterBehavior); + } } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx new file mode 100644 index 00000000000..e45f9fa99ba --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx @@ -0,0 +1,144 @@ +import { useMemo } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { Alert, Button, Input, RadioButtonGroup, Switch, TextLink } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect'; +import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; + +import { getDashboardSceneFor, getQueryRunnerFor } from '../../utils/utils'; +import { DashboardScene } from '../DashboardScene'; +import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector'; + +import { RowItem } from './RowItem'; + +export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] { + const rowOptions = useMemo(() => { + return new OptionsPaneCategoryDescriptor({ + title: t('dashboard.rows-layout.row-options.title', 'Row options'), + id: 'row-options', + isOpenDefault: true, + }) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.rows-layout.row-options.title-option', 'Title'), + render: () => , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.rows-layout.row-options.height.title', 'Height'), + render: () => , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.rows-layout.row-options.height.hide-row-header', 'Hide row header'), + render: () => , + }) + ); + }, [model]); + + const rowRepeatOptions = useMemo(() => { + const dashboard = getDashboardSceneFor(model); + + return new OptionsPaneCategoryDescriptor({ + title: t('dashboard.rows-layout.row-options.repeat.title', 'Repeat options'), + id: 'row-repeat-options', + isOpenDefault: true, + }).addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Variable'), + render: () => , + }) + ); + }, [model]); + + const { layout } = model.useState(); + const layoutOptions = useLayoutCategory(layout); + + return [rowOptions, rowRepeatOptions, layoutOptions]; +} + +export function renderActions(model: RowItem) { + return ( + <> + + {!isClone && isEditing && ( +
+ )} + {!isCollapsed && } +
+ ); +} + +function getStyles(theme: GrafanaTheme2) { + return { + rowHeader: css({ + width: '100%', + display: 'flex', + gap: theme.spacing(1), + padding: theme.spacing(0, 0, 0.5, 0), + margin: theme.spacing(0, 0, 1, 0), + alignItems: 'center', + + '&:hover, &:focus-within': { + '& > div': { + opacity: 1, + }, + }, + + '& > div': { + marginBottom: 0, + marginRight: theme.spacing(1), + }, + }), + rowTitleButton: css({ + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + background: 'transparent', + border: 'none', + minWidth: 0, + gap: theme.spacing(1), + }), + rowTitle: css({ + fontSize: theme.typography.h5.fontSize, + fontWeight: theme.typography.fontWeightMedium, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + maxWidth: '100%', + flexGrow: 1, + minWidth: 0, + }), + wrapper: css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + minHeight: '100px', + }), + wrapperGrow: css({ + flexGrow: 1, + }), + wrapperCollapsed: css({ + flexGrow: 0, + borderBottom: `1px solid ${theme.colors.border.weak}`, + minHeight: 'unset', + }), + rowActions: css({ + display: 'flex', + opacity: 0, + }), + }; +} diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.test.tsx index 753987c15ae..d152fed3a64 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.test.tsx @@ -141,7 +141,7 @@ function buildScene( $behaviors: [repeatBehavior], layout: DefaultGridLayoutManager.fromGridItems([ new DashboardGridItem({ - key: 'griditem-1', + key: 'grid-item-1', x: 0, y: 11, width: 24, @@ -155,13 +155,13 @@ function buildScene( title: 'Row at the bottom', layout: DefaultGridLayoutManager.fromGridItems([ new DashboardGridItem({ - key: 'griditem-2', + key: 'grid-item-2', x: 0, y: 17, body: buildTextPanel('text-2', 'Panel inside row, server = $server'), }), new DashboardGridItem({ - key: 'griditem-3', + key: 'grid-item-3', x: 0, y: 25, body: buildTextPanel('text-3', 'Panel inside row, server = $server'), diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.ts index 175351744a6..49b4e9c0a25 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.ts +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.ts @@ -22,10 +22,6 @@ interface RowItemRepeaterBehaviorState extends SceneObjectState { variableName: string; } -/** - * This behavior will run an effect function when specified variables change - */ - export class RowItemRepeaterBehavior extends SceneObjectBase { protected _variableDependency = new VariableDependencyConfig(this, { variableNames: [this.state.variableName], diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItems.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItems.tsx new file mode 100644 index 00000000000..e2523935b56 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItems.tsx @@ -0,0 +1,39 @@ +import { ReactNode } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; + +import { MultiSelectedEditableDashboardElement } from '../types/MultiSelectedEditableDashboardElement'; + +import { RowItem } from './RowItem'; +import { getEditOptions, renderActions } from './RowItemsEditor'; + +export class RowItems implements MultiSelectedEditableDashboardElement { + public readonly isMultiSelectedEditableDashboardElement = true; + public readonly typeName = 'Rows'; + public readonly key: string; + + public constructor(private _rows: RowItem[]) { + this.key = uuidv4(); + } + + public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { + return getEditOptions(this); + } + + public renderActions(): ReactNode { + return renderActions(this); + } + + public getRows(): RowItem[] { + return this._rows; + } + + public onDelete() { + this._rows.forEach((row) => row.onDelete()); + } + + public onHeaderHiddenToggle(value: boolean, indeterminate: boolean) { + this._rows.forEach((row) => row.onHeaderHiddenToggle(indeterminate ? true : !value)); + } +} diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx new file mode 100644 index 00000000000..bd4837f3d59 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemsEditor.tsx @@ -0,0 +1,64 @@ +import { Button, Checkbox, Stack, Text } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; + +import { RowItems } from './RowItems'; + +export function getEditOptions(model: RowItems): OptionsPaneCategoryDescriptor[] { + const options = new OptionsPaneCategoryDescriptor({ + title: t('dashboard.edit-pane.row.multi-select.options-header', 'Multi-selected Row options'), + id: `ms-row-options-${model.key}`, + isOpenDefault: true, + }).addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.edit-pane.row.header.title', 'Row header'), + render: () => , + }) + ); + + return [options]; +} + +export function renderActions(model: RowItems) { + const rows = model.getRows(); + + return ( + + + + No. of rows selected: {{ length }} + + + + ), }); + leftActions.push({ + group: 'add-panel', + condition: isEditingAndShowingDashboard, + render: () => ( + + ), + }); leftActions.push({ group: 'add-panel', condition: isEditingAndShowingDashboard, diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index 1ee8080fd64..567df0709bf 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -26,6 +26,7 @@ import { getGridItemKeyForPanelId, getDashboardSceneFor, } from '../../utils/utils'; +import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { DashboardGridItem } from './DashboardGridItem'; @@ -177,6 +178,22 @@ export class DefaultGridLayoutManager return panels; } + public hasVizPanels(): boolean { + for (const child of this.state.grid.state.children) { + if (child instanceof DashboardGridItem) { + return true; + } else if (child instanceof SceneGridRow) { + for (const rowChild of child.state.children) { + if (rowChild instanceof DashboardGridItem) { + return true; + } + } + } + } + + return false; + } + public addNewRow(): SceneGridRow { const id = dashboardSceneGraph.getNextPanelId(this); @@ -205,6 +222,17 @@ export class DefaultGridLayoutManager return row; } + public addNewTab() { + const shouldAddTab = this.hasVizPanels(); + const tabsLayout = TabsLayoutManager.createFromLayout(this); + + if (shouldAddTab) { + tabsLayout.addNewTab(); + } + + getDashboardSceneFor(this).switchLayout(tabsLayout); + } + public editModeChanged(isEditing: boolean) { const updateResizeAndDragging = () => { this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing }); diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index 04061ea503d..928ac786861 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -5,6 +5,7 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelId } from '../../utils/utils'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; +import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { ResponsiveGridItem } from './ResponsiveGridItem'; @@ -101,12 +102,38 @@ export class ResponsiveGridLayoutManager return panels; } + public hasVizPanels(): boolean { + for (const child of this.state.layout.state.children) { + if (child instanceof ResponsiveGridItem) { + return true; + } + } + + return false; + } + public addNewRow() { + const shouldAddRow = this.hasVizPanels(); const rowsLayout = RowsLayoutManager.createFromLayout(this); - rowsLayout.addNewRow(); + + if (shouldAddRow) { + rowsLayout.addNewRow(); + } + getDashboardSceneFor(this).switchLayout(rowsLayout); } + public addNewTab() { + const shouldAddTab = this.hasVizPanels(); + const tabsLayout = TabsLayoutManager.createFromLayout(this); + + if (shouldAddTab) { + tabsLayout.addNewTab(); + } + + getDashboardSceneFor(this).switchLayout(tabsLayout); + } + public getOptions(): OptionsPaneItemDescriptor[] { return getEditOptions(this); } 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 f9c5b5e43ba..0c770a333b1 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -3,9 +3,11 @@ 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 { RowItem } from './RowItem'; @@ -62,10 +64,31 @@ export class RowsLayoutManager extends SceneObjectBase i return panels; } + public hasVizPanels(): boolean { + for (const row of this.state.rows) { + if (row.getLayout().hasVizPanels()) { + return true; + } + } + + return false; + } + public addNewRow() { 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)); } @@ -87,9 +110,8 @@ export class RowsLayoutManager extends SceneObjectBase i } public removeRow(row: RowItem) { - this.setState({ - rows: this.state.rows.filter((r) => r !== row), - }); + const rows = this.state.rows.filter((r) => r !== row); + this.setState({ rows: rows.length === 0 ? [new RowItem()] : rows }); } public static createEmpty(): RowsLayoutManager { diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx new file mode 100644 index 00000000000..73ce7af6b19 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx @@ -0,0 +1,80 @@ +import { ReactNode } from 'react'; + +import { SceneObjectState, SceneObjectBase, sceneGraph, VariableDependencyConfig, SceneObject } from '@grafana/scenes'; +import { t } from 'app/core/internationalization'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; + +import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager'; +import { BulkActionElement } from '../types/BulkActionElement'; +import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; +import { EditableDashboardElement } from '../types/EditableDashboardElement'; +import { LayoutParent } from '../types/LayoutParent'; + +import { getEditOptions, renderActions } from './TabItemEditor'; +import { TabItemRenderer } from './TabItemRenderer'; +import { TabItems } from './TabItems'; +import { TabsLayoutManager } from './TabsLayoutManager'; + +export interface TabItemState extends SceneObjectState { + layout: DashboardLayoutManager; + title?: string; +} + +export class TabItem + extends SceneObjectBase + implements LayoutParent, BulkActionElement, EditableDashboardElement +{ + public static Component = TabItemRenderer; + + protected _variableDependency = new VariableDependencyConfig(this, { + statePaths: ['title'], + }); + + public readonly isEditableDashboardElement = true; + public readonly typeName = 'Tab'; + + constructor(state?: Partial) { + super({ + ...state, + title: state?.title ?? t('dashboard.tabs-layout.tab.new', 'New tab'), + layout: state?.layout ?? ResponsiveGridLayoutManager.createEmpty(), + }); + } + + public getLayout(): DashboardLayoutManager { + return this.state.layout; + } + + public switchLayout(layout: DashboardLayoutManager) { + this.setState({ layout }); + } + + public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { + return getEditOptions(this); + } + + public renderActions(): ReactNode { + return renderActions(this); + } + + public getParentLayout(): TabsLayoutManager { + return sceneGraph.getAncestor(this, TabsLayoutManager); + } + + public onDelete() { + const layout = sceneGraph.getAncestor(this, TabsLayoutManager); + layout.removeTab(this); + } + + public createMultiSelectedElement(items: SceneObject[]): TabItems { + return new TabItems(items.filter((item) => item instanceof TabItem)); + } + + public onChangeTab() { + this.getParentLayout().changeTab(this); + } + + public onChangeTitle(title: string) { + this.setState({ title }); + } +} diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx new file mode 100644 index 00000000000..7d0e8c70b8c --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx @@ -0,0 +1,45 @@ +import { ReactNode, useMemo } from 'react'; + +import { Button, Input } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; + +import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector'; + +import { TabItem } from './TabItem'; + +export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[] { + const tabOptions = useMemo(() => { + return new OptionsPaneCategoryDescriptor({ + title: t('dashboard.tabs-layout.tab-options.title', 'Tab options'), + id: 'tab-options', + isOpenDefault: true, + }).addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.tabs-layout.tab-options.title-option', 'Title'), + render: () => , + }) + ); + }, [model]); + + const { layout } = model.useState(); + const layoutOptions = useLayoutCategory(layout); + + return [tabOptions, layoutOptions]; +} + +export function renderActions(tab: TabItem): ReactNode { + return ( + <> + + <> + + {showConfirm && ( + { + setShowConfirm(false); + action.onClick(new MouseEvent('click')); + }} + onDismiss={() => { + setShowConfirm(false); + }} + /> + )} + ); } diff --git a/public/app/features/actions/ActionEditor.tsx b/public/app/features/actions/ActionEditor.tsx index fc7b006a8fc..bc8edcddd1d 100644 --- a/public/app/features/actions/ActionEditor.tsx +++ b/public/app/features/actions/ActionEditor.tsx @@ -8,6 +8,7 @@ import { InlineFieldRow } from '@grafana/ui/src/components/Forms/InlineFieldRow' import { RadioButtonGroup } from '@grafana/ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup'; import { JSONFormatter } from '@grafana/ui/src/components/JSONFormatter/JSONFormatter'; import { useStyles2 } from '@grafana/ui/src/themes'; +import { t } from '@grafana/ui/src/utils/i18n'; import { HTMLElementType, SuggestionsInput } from '../transformers/suggestionsInput/SuggestionsInput'; @@ -29,6 +30,10 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions }: Actio onChange(index, { ...value, title }); }; + const onConfirmationChange = (confirmation: string) => { + onChange(index, { ...value, confirmation }); + }; + const onUrlChange = (url: string) => { onChange(index, { ...value, @@ -98,18 +103,42 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions }: Actio return (
- + + + + + - + value={value?.fetch.method} options={httpMethodOptions} @@ -130,7 +159,10 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions }: Actio - + @@ -144,7 +176,7 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions }: Actio {value?.fetch.method !== HttpRequestMethod.GET && ( - + ({ fieldGap: css({ marginTop: theme.spacing(2), }), + inputField: css({ + marginRight: 4, + }), }); ActionEditor.displayName = 'ActionEditor'; diff --git a/public/app/features/actions/utils.ts b/public/app/features/actions/utils.ts index f33fa82eb9a..29751c4f4e3 100644 --- a/public/app/features/actions/utils.ts +++ b/public/app/features/actions/utils.ts @@ -50,10 +50,15 @@ export const getActions = ( dataContext.value.calculatedValue = config.calculatedValue; } - let actionModel: ActionModel = { title: '', onClick: (e) => {} }; + const title = replaceVariables(action.title, actionScopedVars); + const confirmation = replaceVariables( + action.confirmation || `Are you sure you want to ${action.title}?`, + actionScopedVars + ); - actionModel = { - title: replaceVariables(action.title || '', actionScopedVars), + const actionModel: ActionModel = { + title, + confirmation, onClick: (evt: MouseEvent, origin: Field) => { buildActionOnClick(action, boundReplaceVariables); }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a313559321e..c40a820b16d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1569,6 +1569,19 @@ "send-custom-feedback": "Send" }, "grafana-ui": { + "action-editor": { + "button": { + "confirm": "Confirm", + "confirm-action": "Confirm action" + }, + "modal": { + "action-body": "Body", + "action-method": "Method", + "action-query-params": "Query parameters", + "action-title": "Title", + "action-title-placeholder": "Action title" + } + }, "auto-save-field": { "saved": "Saved!", "saving": "Saving <1>" @@ -1676,6 +1689,9 @@ "right-axis-indicator": "(right y-axis)" }, "viz-tooltip": { + "actions-confirmation-input-placeholder": "Are you sure you want to {{ actionTitle }}?", + "actions-confirmation-label": "Confirmation message", + "actions-confirmation-message": "Provide a descriptive prompt to confirm or cancel the action.", "footer-add-annotation": "Add annotation", "footer-click-to-navigate": "Click to open {{linkTitle}}" } diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 69a34972f6b..f67fd6afb55 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1569,6 +1569,19 @@ "send-custom-feedback": "Ŝęʼnđ" }, "grafana-ui": { + "action-editor": { + "button": { + "confirm": "Cőʼnƒįřm", + "confirm-action": "Cőʼnƒįřm äčŧįőʼn" + }, + "modal": { + "action-body": "ßőđy", + "action-method": "Męŧĥőđ", + "action-query-params": "Qūęřy päřämęŧęřş", + "action-title": "Ŧįŧľę", + "action-title-placeholder": "Åčŧįőʼn ŧįŧľę" + } + }, "auto-save-field": { "saved": "Ŝävęđ!", "saving": "Ŝävįʼnģ <1>" @@ -1676,6 +1689,9 @@ "right-axis-indicator": "(řįģĥŧ y-äχįş)" }, "viz-tooltip": { + "actions-confirmation-input-placeholder": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő {{ actionTitle }}?", + "actions-confirmation-label": "Cőʼnƒįřmäŧįőʼn męşşäģę", + "actions-confirmation-message": "Přővįđę ä đęşčřįpŧįvę přőmpŧ ŧő čőʼnƒįřm őř čäʼnčęľ ŧĥę äčŧįőʼn.", "footer-add-annotation": "Åđđ äʼnʼnőŧäŧįőʼn", "footer-click-to-navigate": "Cľįčĸ ŧő őpęʼn {{linkTitle}}" } From f5c049012bd59b22e36a84b813b543b2f7f3ce49 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:03:05 -0600 Subject: [PATCH 444/894] Chore: Data links and Actions components refactor (#100097) Co-authored-by: Leon Sorokin --- .betterer.results | 7 - packages/grafana-data/src/types/action.ts | 2 + .../components/DataLinks/DataLinkEditor.tsx | 94 ++++---- .../DataLinkEditorModalContent.tsx | 8 +- .../DataLinksInlineEditor.tsx | 210 ++---------------- .../DataLinksInlineEditorBase.tsx | 191 ++++++++++++++++ .../DataLinksListItem.test.tsx | 58 +---- .../DataLinksListItem.tsx | 121 +--------- .../DataLinksListItemBase.tsx | 82 +++++-- packages/grafana-ui/src/components/index.ts | 4 + public/app/features/actions/ActionEditor.tsx | 24 +- .../actions/ActionEditorModalContent.tsx | 3 + .../features/actions/ActionsInlineEditor.tsx | 207 ++--------------- .../canvas/editor/element/ActionsEditor.tsx | 18 +- .../canvas/editor/element/DataLinksEditor.tsx | 15 +- public/locales/en-US/grafana.json | 19 +- public/locales/pseudo-LOCALE/grafana.json | 19 +- 17 files changed, 430 insertions(+), 652 deletions(-) create mode 100644 packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditorBase.tsx rename public/app/features/actions/ActionsListItem.tsx => packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItemBase.tsx (52%) diff --git a/.betterer.results b/.betterer.results index e33c3ac4abd..21ed4ac7d28 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1367,13 +1367,6 @@ exports[`better eslint`] = { [0, 0, 0, "\'@grafana/ui/src/themes\' import is restricted from being used by a pattern. Import from the public export instead.", "2"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"] ], - "public/app/features/actions/ActionsListItem.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Icon/Icon\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], - [0, 0, 0, "\'@grafana/ui/src/components/IconButton/IconButton\' import is restricted from being used by a pattern. Import from the public export instead.", "1"], - [0, 0, 0, "\'@grafana/ui/src/themes\' import is restricted from being used by a pattern. Import from the public export instead.", "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"] - ], "public/app/features/actions/ParamsEditor.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/IconButton/IconButton\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], [0, 0, 0, "\'@grafana/ui/src/components/Input/Input\' import is restricted from being used by a pattern. Import from the public export instead.", "1"], diff --git a/packages/grafana-data/src/types/action.ts b/packages/grafana-data/src/types/action.ts index bbdfd91b4f5..fc17aa25046 100644 --- a/packages/grafana-data/src/types/action.ts +++ b/packages/grafana-data/src/types/action.ts @@ -16,6 +16,7 @@ export interface Action { // once multiple types are valid, usage of this will need to be optional [ActionType.Fetch]: FetchOptions; confirmation?: string; + oneClick?: boolean; } /** @@ -25,6 +26,7 @@ export interface ActionModel { title: string; onClick: (event: any, origin?: any) => void; confirmation?: string; + oneClick?: boolean; } interface FetchOptions { diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx index e87d01fbea2..8ff7535790c 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx @@ -17,6 +17,7 @@ interface DataLinkEditorProps { value: DataLink; suggestions: VariableSuggestion[]; onChange: (index: number, link: DataLink, callback?: () => void) => void; + showOneClick?: boolean; } const getStyles = (theme: GrafanaTheme2) => ({ @@ -30,58 +31,63 @@ const getStyles = (theme: GrafanaTheme2) => ({ }), }); -export const DataLinkEditor = memo(({ index, value, onChange, suggestions, isLast }: DataLinkEditorProps) => { - const styles = useStyles2(getStyles); +export const DataLinkEditor = memo( + ({ index, value, onChange, suggestions, isLast, showOneClick = false }: DataLinkEditorProps) => { + const styles = useStyles2(getStyles); - const onUrlChange = (url: string, callback?: () => void) => { - onChange(index, { ...value, url }, callback); - }; - const onTitleChange = (event: ChangeEvent) => { - onChange(index, { ...value, title: event.target.value }); - }; + const onUrlChange = (url: string, callback?: () => void) => { + onChange(index, { ...value, url }, callback); + }; - const onOpenInNewTabChanged = () => { - onChange(index, { ...value, targetBlank: !value.targetBlank }); - }; + const onTitleChange = (event: ChangeEvent) => { + onChange(index, { ...value, title: event.target.value }); + }; - const onOneClickChanged = () => { - onChange(index, { ...value, oneClick: !value.oneClick }); - }; + const onOpenInNewTabChanged = () => { + onChange(index, { ...value, targetBlank: !value.targetBlank }); + }; - return ( -
- - - + const onOneClickChanged = () => { + onChange(index, { ...value, oneClick: !value.oneClick }); + }; - - - + return ( +
+ + + - - - + + + - + + + + {showOneClick && ( + + + )} - > - - - {isLast && ( -
- - With data links you can reference data variables like series name, labels and values. Type CMD+Space, - CTRL+Space, or $ to open variable suggestions. - -
- )} -
- ); -}); + {isLast && ( +
+ + With data links you can reference data variables like series name, labels and values. Type CMD+Space, + CTRL+Space, or $ to open variable suggestions. + +
+ )} +
+ ); + } +); DataLinkEditor.displayName = 'DataLinkEditor'; diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx index 5348251f1f2..02287da7215 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx @@ -14,14 +14,16 @@ interface DataLinkEditorModalContentProps { getSuggestions: () => VariableSuggestion[]; onSave: (index: number, ink: DataLink) => void; onCancel: (index: number) => void; + showOneClick?: boolean; } export const DataLinkEditorModalContent = ({ link, index, - getSuggestions, onSave, onCancel, + getSuggestions, + showOneClick, }: DataLinkEditorModalContentProps) => { const [dirtyLink, setDirtyLink] = useState(link); return ( @@ -30,10 +32,11 @@ export const DataLinkEditorModalContent = ({ value={dirtyLink} index={index} isLast={false} - suggestions={getSuggestions()} onChange={(index, link) => { setDirtyLink(link); }} + suggestions={getSuggestions()} + showOneClick={showOneClick} /> diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx index fa4e61589f3..99b15ea1697 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx @@ -1,198 +1,26 @@ -import { css } from '@emotion/css'; -import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd'; -import { cloneDeep } from 'lodash'; -import { useEffect, useState } from 'react'; - -import { DataFrame, DataLink, GrafanaTheme2, VariableSuggestion } from '@grafana/data'; - -import { useStyles2 } from '../../../themes'; -import { Trans } from '../../../utils/i18n'; -import { Button } from '../../Button'; -import { Modal } from '../../Modal/Modal'; +import { DataLink, VariableSuggestion } from '@grafana/data'; import { DataLinkEditorModalContent } from './DataLinkEditorModalContent'; -import { DataLinksListItem } from './DataLinksListItem'; +import { DataLinksInlineEditorBase, DataLinksInlineEditorBaseProps } from './DataLinksInlineEditorBase'; -interface DataLinksInlineEditorProps { +type DataLinksInlineEditorProps = Omit, 'children' | 'type' | 'items'> & { links?: DataLink[]; - onChange: (links: DataLink[]) => void; - getSuggestions: () => VariableSuggestion[]; - data: DataFrame[]; showOneClick?: boolean; -} - -export const DataLinksInlineEditor = ({ - links, - onChange, - getSuggestions, - data, - showOneClick = false, -}: DataLinksInlineEditorProps) => { - const [editIndex, setEditIndex] = useState(null); - const [isNew, setIsNew] = useState(false); - - const [linksSafe, setLinksSafe] = useState([]); - - useEffect(() => { - setLinksSafe(links ?? []); - }, [links]); - - const styles = useStyles2(getDataLinksInlineEditorStyles); - const isEditing = editIndex !== null; - - const onDataLinkChange = (index: number, link: DataLink) => { - if (isNew) { - if (link.title.trim() === '' && link.url.trim() === '') { - setIsNew(false); - setEditIndex(null); - return; - } else { - setEditIndex(null); - setIsNew(false); - } - } - - if (link.oneClick === true) { - linksSafe.forEach((link) => { - if (link.oneClick) { - link.oneClick = false; - } - }); - } - - const update = cloneDeep(linksSafe); - update[index] = link; - onChange(update); - setEditIndex(null); - }; - - const onDataLinkAdd = () => { - let update = cloneDeep(linksSafe); - setEditIndex(update.length); - setIsNew(true); - }; - - const onDataLinkCancel = (index: number) => { - if (isNew) { - setIsNew(false); - } - setEditIndex(null); - }; - - const onDataLinkRemove = (index: number) => { - const update = cloneDeep(linksSafe); - update.splice(index, 1); - onChange(update); - }; - - const onDragEnd = (result: DropResult) => { - if (!links || !result.destination) { - return; - } - - const update = cloneDeep(linksSafe); - const link = update[result.source.index]; - - update.splice(result.source.index, 1); - update.splice(result.destination.index, 0, link); - - setLinksSafe(update); - onChange(update); - }; - - return ( -
- {/* one-link placeholder */} - {showOneClick && linksSafe.length > 0 && ( -
- - One-click link - -
- )} - - - - {(provided) => ( -
0 ? '28px' : '0px' }} - > - {linksSafe.map((link, idx) => { - const key = `${link.title}/${idx}`; - return ( - setEditIndex(idx)} - onRemove={() => onDataLinkRemove(idx)} - data={data} - itemKey={key} - /> - ); - })} - {provided.placeholder} -
- )} -
-
- - {isEditing && editIndex !== null && ( - { - onDataLinkCancel(editIndex); - }} - > - - - )} - - -
- ); + getSuggestions: () => VariableSuggestion[]; }; -const getDataLinksInlineEditorStyles = (theme: GrafanaTheme2) => ({ - container: css({ - position: 'relative', - }), - wrapper: css({ - marginBottom: theme.spacing(2), - display: 'flex', - flexDirection: 'column', - }), - oneClickOverlay: css({ - border: `2px dashed ${theme.colors.text.link}`, - fontSize: 10, - color: theme.colors.text.primary, - marginBottom: theme.spacing(1), - position: 'absolute', - width: '100%', - height: '92px', - }), - oneClickSpan: css({ - padding: 10, - // Negates the padding on the span from moving the underlying link - marginBottom: -10, - display: 'inline-block', - }), - button: css({ - marginLeft: theme.spacing(1), - }), -}); +export const DataLinksInlineEditor = ({ links, getSuggestions, showOneClick, ...rest }: DataLinksInlineEditorProps) => ( + type="link" items={links} {...rest}> + {(item, index, onSave, onCancel) => ( + + )} + +); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditorBase.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditorBase.tsx new file mode 100644 index 00000000000..a5f66c6d0c3 --- /dev/null +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditorBase.tsx @@ -0,0 +1,191 @@ +import { css } from '@emotion/css'; +import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd'; +import { cloneDeep } from 'lodash'; +import { useEffect, useState } from 'react'; + +import { Action, DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../../themes'; +import { t } from '../../../utils/i18n'; +import { Button } from '../../Button'; +import { Modal } from '../../Modal/Modal'; + +import { DataLinksListItemBase } from './DataLinksListItemBase'; + +export interface DataLinksInlineEditorBaseProps { + type: 'link' | 'action'; + items?: T[]; + onChange: (items: T[]) => void; + data: DataFrame[]; + children: ( + item: T, + index: number, + onSave: (index: number, item: T) => void, + onCancel: (index: number) => void + ) => React.ReactNode; +} + +/** @internal */ +export function DataLinksInlineEditorBase({ + type, + items, + onChange, + data, + children, +}: DataLinksInlineEditorBaseProps) { + const [editIndex, setEditIndex] = useState(null); + const [isNew, setIsNew] = useState(false); + + const [itemsSafe, setItemsSafe] = useState([]); + + useEffect(() => { + setItemsSafe(items ?? []); + }, [items]); + + const styles = useStyles2(getDataLinksInlineEditorStyles); + const isEditing = editIndex !== null; + + const _onChange = (index: number, item: T) => { + if (isNew) { + const title = item.title; + // @ts-ignore - https://github.com/microsoft/TypeScript/issues/27808 + const url = item.url ?? item.fetch?.url ?? ''; + + if (title.trim() === '' && url.trim() === '') { + setIsNew(false); + setEditIndex(null); + return; + } else { + setEditIndex(null); + setIsNew(false); + } + } + + if (item.oneClick === true) { + itemsSafe.forEach((item) => { + if (item.oneClick) { + item.oneClick = false; + } + }); + } + + const update = cloneDeep(itemsSafe); + update[index] = item; + onChange(update); + setEditIndex(null); + }; + + const _onCancel = (index: number) => { + if (isNew) { + setIsNew(false); + } + setEditIndex(null); + }; + + const onDataLinkAdd = () => { + let update = cloneDeep(itemsSafe); + setEditIndex(update.length); + setIsNew(true); + }; + + const onDataLinkRemove = (index: number) => { + const update = cloneDeep(itemsSafe); + update.splice(index, 1); + onChange(update); + }; + + const onDragEnd = (result: DropResult) => { + if (items == null || result.destination == null) { + return; + } + + const update = cloneDeep(itemsSafe); + const link = update[result.source.index]; + + update.splice(result.source.index, 1); + update.splice(result.destination.index, 0, link); + + setItemsSafe(update); + onChange(update); + }; + + const getItemText = (action: 'edit' | 'add') => { + let text = ''; + switch (type) { + case 'link': + text = + action === 'edit' + ? t('grafana-ui.data-links-inline-editor.edit-link', 'Edit link') + : t('grafana-ui.data-links-inline-editor.add-link', 'Add link'); + break; + case 'action': + text = + action === 'edit' + ? t('grafana-ui.action-editor.inline.edit-action', 'Edit action') + : t('grafana-ui.action-editor.inline.add-action', 'Add action'); + break; + } + + return text; + }; + + return ( +
+ + + {(provided) => ( +
+ {itemsSafe.map((item, idx) => { + const key = `${item.title}/${idx}`; + return ( + + key={key} + index={idx} + item={item} + onChange={_onChange} + onEdit={() => setEditIndex(idx)} + onRemove={() => onDataLinkRemove(idx)} + data={data} + itemKey={key} + /> + ); + })} + {provided.placeholder} +
+ )} +
+
+ + {isEditing && editIndex !== null && ( + { + _onCancel(editIndex); + }} + > + {children(itemsSafe[editIndex], editIndex, _onChange, _onCancel)} + + )} + + +
+ ); +} + +const getDataLinksInlineEditorStyles = (theme: GrafanaTheme2) => ({ + container: css({ + position: 'relative', + }), + wrapper: css({ + marginBottom: theme.spacing(2), + display: 'flex', + flexDirection: 'column', + }), + button: css({ + marginLeft: theme.spacing(1), + }), +}); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx index b44576d53f7..d74f11e09f8 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx @@ -13,7 +13,7 @@ const baseLink = { function setupTestContext(options: Partial) { const defaults: DataLinksListItemProps = { index: 0, - link: baseLink, + item: baseLink, data: [], onChange: jest.fn(), onEdit: jest.fn(), @@ -42,11 +42,11 @@ function setupTestContext(options: Partial) { describe('DataLinksListItem', () => { describe('when link has title', () => { it('then the link title should be visible', () => { - const link = { + const item = { ...baseLink, title: 'Some Data Link Title', }; - setupTestContext({ link }); + setupTestContext({ item }); expect(screen.getByText(/some data link title/i)).toBeInTheDocument(); }); @@ -54,62 +54,14 @@ describe('DataLinksListItem', () => { describe('when link has url', () => { it('then the link url should be visible', () => { - const link = { + const item = { ...baseLink, url: 'http://localhost:3000', }; - setupTestContext({ link }); + setupTestContext({ item }); expect(screen.getByText(/http:\/\/localhost\:3000/i)).toBeInTheDocument(); expect(screen.getByTitle(/http:\/\/localhost\:3000/i)).toBeInTheDocument(); }); }); - - describe('when link is missing title', () => { - it('then the link title should be replaced by [Data link title not provided]', () => { - const link = { - ...baseLink, - title: undefined as unknown as string, - }; - setupTestContext({ link }); - - expect(screen.getByText(/data link title not provided/i)).toBeInTheDocument(); - }); - }); - - describe('when link is missing url', () => { - it('then the link url should be replaced by [Data link url not provided]', () => { - const link = { - ...baseLink, - url: undefined as unknown as string, - }; - setupTestContext({ link }); - - expect(screen.getByText(/data link url not provided/i)).toBeInTheDocument(); - }); - }); - - describe('when link title is empty', () => { - it('then the link title should be replaced by [Data link title not provided]', () => { - const link = { - ...baseLink, - title: ' ', - }; - setupTestContext({ link }); - - expect(screen.getByText(/data link title not provided/i)).toBeInTheDocument(); - }); - }); - - describe('when link url is empty', () => { - it('then the link url should be replaced by [Data link url not provided]', () => { - const link = { - ...baseLink, - url: ' ', - }; - setupTestContext({ link }); - - expect(screen.getByText(/data link url not provided/i)).toBeInTheDocument(); - }); - }); }); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx index 9bddc35da71..ce8fbe3d95d 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx @@ -1,119 +1,6 @@ -import { css, cx } from '@emotion/css'; -import { Draggable } from '@hello-pangea/dnd'; +import { DataLink } from '@grafana/data'; -import { DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data'; +import { DataLinksListItemBase, DataLinksListItemBaseProps } from './DataLinksListItemBase'; -import { useStyles2 } from '../../../themes'; -import { t } from '../../../utils/i18n'; -import { Badge } from '../../Badge/Badge'; -import { Icon } from '../../Icon/Icon'; -import { IconButton } from '../../IconButton/IconButton'; - -export interface DataLinksListItemProps { - index: number; - link: DataLink; - data: DataFrame[]; - onChange: (index: number, link: DataLink) => void; - onEdit: () => void; - onRemove: () => void; - isEditing?: boolean; - itemKey: string; -} - -export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: DataLinksListItemProps) => { - const styles = useStyles2(getDataLinkListItemStyles); - const { title = '', url = '', oneClick = false } = link; - - const hasTitle = title.trim() !== ''; - const hasUrl = url.trim() !== ''; - - return ( - - {(provided) => ( -
-
-
- {hasTitle ? title : 'Data link title not provided'} -
-
- {hasUrl ? url : 'Data link url not provided'} -
-
-
- {oneClick && ( - - )} - - -
- -
-
-
- )} -
- ); -}; - -const getDataLinkListItemStyles = (theme: GrafanaTheme2) => { - return { - wrapper: css({ - display: 'flex', - flexGrow: 1, - alignItems: 'center', - justifyContent: 'space-between', - padding: '5px 0 5px 10px', - borderRadius: theme.shape.radius.default, - background: theme.colors.background.secondary, - gap: 8, - }), - linkDetails: css({ - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - maxWidth: `calc(100% - 100px)`, - }), - notConfigured: css({ - fontStyle: 'italic', - }), - title: css({ - color: theme.colors.text.primary, - fontSize: theme.typography.size.sm, - fontWeight: theme.typography.fontWeightMedium, - }), - url: css({ - color: theme.colors.text.secondary, - fontSize: theme.typography.size.sm, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }), - dragRow: css({ - position: 'relative', - margin: '8px', - }), - icons: css({ - display: 'flex', - padding: 6, - alignItems: 'center', - gap: 8, - }), - dragIcon: css({ - cursor: 'grab', - color: theme.colors.text.secondary, - margin: theme.spacing(0, 0.5), - }), - icon: css({ - color: theme.colors.text.secondary, - }), - }; -}; +export const DataLinksListItem = DataLinksListItemBase; +export type DataLinksListItemProps = DataLinksListItemBaseProps; diff --git a/public/app/features/actions/ActionsListItem.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItemBase.tsx similarity index 52% rename from public/app/features/actions/ActionsListItem.tsx rename to packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItemBase.tsx index 6dd9cebf58f..6ddbc97e994 100644 --- a/public/app/features/actions/ActionsListItem.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItemBase.tsx @@ -1,27 +1,41 @@ import { css, cx } from '@emotion/css'; import { Draggable } from '@hello-pangea/dnd'; -import { Action, DataFrame, GrafanaTheme2 } from '@grafana/data'; -import { Icon } from '@grafana/ui/src/components/Icon/Icon'; -import { IconButton } from '@grafana/ui/src/components/IconButton/IconButton'; -import { useStyles2 } from '@grafana/ui/src/themes'; +import { Action, DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data'; -export interface ActionsListItemProps { +import { useStyles2 } from '../../../themes'; +import { t } from '../../../utils/i18n'; +import { Badge } from '../../Badge/Badge'; +import { Icon } from '../../Icon/Icon'; +import { IconButton } from '../../IconButton/IconButton'; + +export interface DataLinksListItemBaseProps { index: number; - action: Action; + item: T; data: DataFrame[]; - onChange: (index: number, action: Action) => void; + onChange: (index: number, item: T) => void; onEdit: () => void; onRemove: () => void; isEditing?: boolean; itemKey: string; } -export const ActionListItem = ({ action, onEdit, onRemove, index, itemKey }: ActionsListItemProps) => { - const styles = useStyles2(getActionListItemStyles); - const { title = '' } = action; +/** @internal */ +export function DataLinksListItemBase({ + item, + onEdit, + onRemove, + index, + itemKey, +}: DataLinksListItemBaseProps) { + const styles = useStyles2(getDataLinkListItemStyles); + const { title = '', oneClick = false } = item; + + // @ts-ignore - https://github.com/microsoft/TypeScript/issues/27808 + const url = item.url ?? item.fetch?.url ?? ''; const hasTitle = title.trim() !== ''; + const hasUrl = url.trim() !== ''; return ( @@ -34,12 +48,32 @@ export const ActionListItem = ({ action, onEdit, onRemove, index, itemKey }: Act >
- {hasTitle ? title : 'Action title not provided'} + {hasTitle ? title : t('grafana-ui.data-links-inline-editor.title-not-provided', 'Title not provided')} +
+
+ {hasUrl ? url : t('grafana-ui.data-links-inline-editor.url-not-provided', 'Data link url not provided')}
- - + {oneClick && ( + + )} + +
@@ -48,9 +82,9 @@ export const ActionListItem = ({ action, onEdit, onRemove, index, itemKey }: Act )} ); -}; +} -const getActionListItemStyles = (theme: GrafanaTheme2) => { +const getDataLinkListItemStyles = (theme: GrafanaTheme2) => { return { wrapper: css({ display: 'flex', @@ -66,6 +100,7 @@ const getActionListItemStyles = (theme: GrafanaTheme2) => { display: 'flex', flexDirection: 'column', flexGrow: 1, + maxWidth: `calc(100% - 100px)`, }), errored: css({ color: theme.colors.error.text, @@ -85,15 +120,6 @@ const getActionListItemStyles = (theme: GrafanaTheme2) => { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', - maxWidth: `calc(100% - 100px)`, - }), - dragIcon: css({ - cursor: 'grab', - color: theme.colors.text.secondary, - margin: theme.spacing(0, 0.5), - }), - icon: css({ - color: theme.colors.text.secondary, }), dragRow: css({ position: 'relative', @@ -105,5 +131,13 @@ const getActionListItemStyles = (theme: GrafanaTheme2) => { alignItems: 'center', gap: 8, }), + dragIcon: css({ + cursor: 'grab', + color: theme.colors.text.secondary, + margin: theme.spacing(0, 0.5), + }), + icon: css({ + color: theme.colors.text.secondary, + }), }; }; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 900190b9acf..8296a00cb70 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -169,6 +169,10 @@ export { MenuGroup, type MenuItemsGroup, type MenuGroupProps } from './Menu/Menu export { MenuItem, type MenuItemProps } from './Menu/MenuItem'; export { WithContextMenu } from './ContextMenu/WithContextMenu'; export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor'; +export { + DataLinksInlineEditorBase, + type DataLinksInlineEditorBaseProps, +} from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditorBase'; export { DataLinkInput } from './DataLinks/DataLinkInput'; export { DataLinksContextMenu, diff --git a/public/app/features/actions/ActionEditor.tsx b/public/app/features/actions/ActionEditor.tsx index bc8edcddd1d..4194ab2680c 100644 --- a/public/app/features/actions/ActionEditor.tsx +++ b/public/app/features/actions/ActionEditor.tsx @@ -2,6 +2,8 @@ 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'; import { InlineFieldRow } from '@grafana/ui/src/components/Forms/InlineFieldRow'; @@ -19,11 +21,12 @@ interface ActionEditorProps { value: Action; onChange: (index: number, action: Action) => void; suggestions: VariableSuggestion[]; + showOneClick?: boolean; } const LABEL_WIDTH = 13; -export const ActionEditor = memo(({ index, value, onChange, suggestions }: ActionEditorProps) => { +export const ActionEditor = memo(({ index, value, onChange, suggestions, showOneClick }: ActionEditorProps) => { const styles = useStyles2(getStyles); const onTitleChange = (title: string) => { @@ -34,6 +37,10 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions }: Actio onChange(index, { ...value, confirmation }); }; + const onOneClickChanged = () => { + onChange(index, { ...value, oneClick: !value.oneClick }); + }; + const onUrlChange = (url: string) => { onChange(index, { ...value, @@ -101,6 +108,8 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions }: Actio 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 (
@@ -133,6 +142,19 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions }: Actio /> + {showOneClick && ( + + + + )} + void; onCancel: (index: number) => void; getSuggestions: () => VariableSuggestion[]; + showOneClick: boolean; } export const ActionEditorModalContent = ({ @@ -22,6 +23,7 @@ export const ActionEditorModalContent = ({ onSave, onCancel, getSuggestions, + showOneClick, }: ActionEditorModalContentProps) => { const [dirtyAction, setDirtyAction] = useState(action); @@ -34,6 +36,7 @@ export const ActionEditorModalContent = ({ setDirtyAction(action); }} suggestions={getSuggestions()} + showOneClick={showOneClick} /> -
- ); + getSuggestions: () => VariableSuggestion[]; }; -const getActionsInlineEditorStyle = (theme: GrafanaTheme2) => ({ - container: css({ - position: 'relative', - }), - wrapper: css({ - marginBottom: theme.spacing(2), - display: 'flex', - flexDirection: 'column', - }), - oneClickOverlay: css({ - border: `2px dashed ${theme.colors.text.link}`, - fontSize: 10, - color: theme.colors.text.primary, - marginBottom: theme.spacing(1), - position: 'absolute', - width: '100%', - height: '89px', - }), - oneClickSpan: css({ - padding: 10, - // Negates the padding on the span from moving the underlying link - marginBottom: -10, - display: 'inline-block', - }), - itemWrapper: css({ - padding: '4px 8px 8px 8px', - }), - button: css({ - marginLeft: theme.spacing(1), - }), -}); +export const ActionsInlineEditor = ({ actions, getSuggestions, showOneClick, ...rest }: DataLinksInlineEditorProps) => ( + type="action" items={actions} {...rest}> + {(item, index, onSave, onCancel) => ( + + )} + +); diff --git a/public/app/plugins/panel/canvas/editor/element/ActionsEditor.tsx b/public/app/plugins/panel/canvas/editor/element/ActionsEditor.tsx index 06613ffe843..4928c7c3368 100644 --- a/public/app/plugins/panel/canvas/editor/element/ActionsEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/element/ActionsEditor.tsx @@ -1,20 +1,26 @@ -import { StandardEditorProps, OneClickMode, Action, VariableSuggestionsScope } from '@grafana/data'; +import { StandardEditorProps, Action, VariableSuggestionsScope } from '@grafana/data'; +import { ActionsInlineEditor } from 'app/features/actions/ActionsInlineEditor'; import { CanvasElementOptions } from 'app/features/canvas/element'; -import { ActionsInlineEditor } from '../../../../../features/actions/ActionsInlineEditor'; - type Props = StandardEditorProps; export function ActionsEditor({ value, onChange, item, context }: Props) { - const oneClickMode = item.settings?.oneClickMode; + const dataLinks = item.settings?.links || []; return ( { + if (actions.some(({ oneClick }) => oneClick === true)) { + dataLinks.forEach((link) => { + link.oneClick = false; + }); + } + onChange(actions); + }} getSuggestions={() => (context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : [])} data={[]} - showOneClick={oneClickMode === OneClickMode.Action} + showOneClick={true} /> ); } diff --git a/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx b/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx index 95779a4b7a1..9849b35d368 100644 --- a/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx @@ -1,19 +1,26 @@ -import { StandardEditorProps, DataLink, VariableSuggestionsScope, OneClickMode } from '@grafana/data'; +import { StandardEditorProps, DataLink, VariableSuggestionsScope } from '@grafana/data'; import { DataLinksInlineEditor } from '@grafana/ui'; import { CanvasElementOptions } from 'app/features/canvas/element'; type Props = StandardEditorProps; export function DataLinksEditor({ value, onChange, item, context }: Props) { - const oneClickMode = item.settings?.oneClickMode; + const actions = item.settings?.actions || []; return ( { + if (links.some(({ oneClick }) => oneClick === true)) { + actions.forEach((action) => { + action.oneClick = false; + }); + } + onChange(links); + }} getSuggestions={() => (context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : [])} data={[]} - showOneClick={oneClickMode === OneClickMode.Link} + showOneClick={false} /> ); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c40a820b16d..c739515378a 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -34,12 +34,6 @@ "save-button": "Save" } }, - "actions-editor": { - "inline": { - "add-button": "Add action", - "one-click-action": "One-click action" - } - }, "admin": { "anon-users": { "not-found": "No anonymous users found." @@ -1574,12 +1568,17 @@ "confirm": "Confirm", "confirm-action": "Confirm action" }, + "inline": { + "add-action": "Add action", + "edit-action": "Edit action" + }, "modal": { "action-body": "Body", "action-method": "Method", "action-query-params": "Query parameters", "action-title": "Title", - "action-title-placeholder": "Action title" + "action-title-placeholder": "Action title", + "one-click-description": "Only one link {{ action }} can have one click enabled at a time" } }, "auto-save-field": { @@ -1606,9 +1605,13 @@ }, "data-links-inline-editor": { "add-link": "Add link", + "edit-link": "Edit link", "one-click": "One click", "one-click-enabled": "One click enabled", - "one-click-link": "One-click link" + "title-not-provided": "Title not provided", + "tooltip-edit": "Edit", + "tooltip-remove": "Remove", + "url-not-provided": "Data link url not provided" }, "data-source-http-settings": { "access-help": "Help <1>", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index f67fd6afb55..4c73778c894 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -34,12 +34,6 @@ "save-button": "Ŝävę" } }, - "actions-editor": { - "inline": { - "add-button": "Åđđ äčŧįőʼn", - "one-click-action": "Øʼnę-čľįčĸ äčŧįőʼn" - } - }, "admin": { "anon-users": { "not-found": "Ńő äʼnőʼnymőūş ūşęřş ƒőūʼnđ." @@ -1574,12 +1568,17 @@ "confirm": "Cőʼnƒįřm", "confirm-action": "Cőʼnƒįřm äčŧįőʼn" }, + "inline": { + "add-action": "Åđđ äčŧįőʼn", + "edit-action": "Ēđįŧ äčŧįőʼn" + }, "modal": { "action-body": "ßőđy", "action-method": "Męŧĥőđ", "action-query-params": "Qūęřy päřämęŧęřş", "action-title": "Ŧįŧľę", - "action-title-placeholder": "Åčŧįőʼn ŧįŧľę" + "action-title-placeholder": "Åčŧįőʼn ŧįŧľę", + "one-click-description": "Øʼnľy őʼnę ľįʼnĸ {{ action }} čäʼn ĥävę őʼnę čľįčĸ ęʼnäþľęđ äŧ ä ŧįmę" } }, "auto-save-field": { @@ -1606,9 +1605,13 @@ }, "data-links-inline-editor": { "add-link": "Åđđ ľįʼnĸ", + "edit-link": "Ēđįŧ ľįʼnĸ", "one-click": "Øʼnę čľįčĸ", "one-click-enabled": "Øʼnę čľįčĸ ęʼnäþľęđ", - "one-click-link": "Øʼnę-čľįčĸ ľįʼnĸ" + "title-not-provided": "Ŧįŧľę ʼnőŧ přővįđęđ", + "tooltip-edit": "Ēđįŧ", + "tooltip-remove": "Ŗęmővę", + "url-not-provided": "Đäŧä ľįʼnĸ ūřľ ʼnőŧ přővįđęđ" }, "data-source-http-settings": { "access-help": "Ħęľp <1>", From 45775dd6ad7ad3d895f4181e45ac8631fc1604f4 Mon Sep 17 00:00:00 2001 From: Sam Jewell <2903904+samjewell@users.noreply.github.com> Date: Mon, 10 Feb 2025 07:40:32 +0000 Subject: [PATCH 445/894] Skip flakey test (#100251) --- pkg/tests/api/correlations/correlations_update_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/tests/api/correlations/correlations_update_test.go b/pkg/tests/api/correlations/correlations_update_test.go index 2c42d37585e..457fbe985e1 100644 --- a/pkg/tests/api/correlations/correlations_update_test.go +++ b/pkg/tests/api/correlations/correlations_update_test.go @@ -246,6 +246,7 @@ func TestIntegrationUpdateCorrelation(t *testing.T) { }) t.Run("should correctly update correlations", func(t *testing.T) { + t.Skip("flaky test: See failure at https://drone.grafana.net/grafana/grafana/222544/1/9") correlation := ctx.createCorrelation(correlations.CreateCorrelationCommand{ SourceUID: writableDs, TargetUID: &writableDs, From 95e61f63f76841fcce46410487c8c5d032019670 Mon Sep 17 00:00:00 2001 From: Elliot Kirk Date: Mon, 10 Feb 2025 00:32:47 -0800 Subject: [PATCH 446/894] Faro: Upgrading faro deps to 1.13.1, enabling error serializer (#100145) * upping faro deps, enabling error serializer * linting * fix lint errors * more linting stuff * yarn lock --- package.json | 6 +- packages/grafana-runtime/src/utils/logging.ts | 13 +- .../GrafanaJavascriptAgentBackend.ts | 8 + yarn.lock | 294 +++++++----------- 4 files changed, 135 insertions(+), 186 deletions(-) diff --git a/package.json b/package.json index 2d7c19a553f..8457c7db5d2 100644 --- a/package.json +++ b/package.json @@ -264,9 +264,9 @@ "@grafana/azure-sdk": "0.0.5", "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", - "@grafana/faro-core": "^1.3.6", - "@grafana/faro-web-sdk": "^1.3.6", - "@grafana/faro-web-tracing": "^1.8.2", + "@grafana/faro-core": "^1.13.1", + "@grafana/faro-web-sdk": "^1.13.1", + "@grafana/faro-web-tracing": "^1.13.1", "@grafana/flamegraph": "workspace:*", "@grafana/google-sdk": "0.1.2", "@grafana/lezer-logql": "0.2.7", diff --git a/packages/grafana-runtime/src/utils/logging.ts b/packages/grafana-runtime/src/utils/logging.ts index 98eb7241361..6a08ced5f69 100644 --- a/packages/grafana-runtime/src/utils/logging.ts +++ b/packages/grafana-runtime/src/utils/logging.ts @@ -76,14 +76,21 @@ export function logMeasurement(type: string, values: MeasurementValues, context? } } +export interface MonitoringLogger { + logDebug: (message: string, contexts?: LogContext) => void; + logInfo: (message: string, contexts?: LogContext) => void; + logWarning: (message: string, contexts?: LogContext) => void; + logError: (error: Error, contexts?: LogContext) => void; + logMeasurement: (type: string, measurement: MeasurementValues, contexts?: LogContext) => void; +} /** - * Creates a monitoring logger with four levels of logging methods: `logDebug`, `logInfo`, `logWarning`, and `logError`. + * Creates a monitoring logger with five levels of logging methods: `logDebug`, `logInfo`, `logWarning`, `logError`, and `logMeasurement`. * These methods use `faro.api.pushX` web SDK methods to report these logs or errors to the Faro collector. * * @param {string} source - Identifier for the source of the log messages. * @param {LogContext} [defaultContext] - Context to be included in every log message. * - * @returns {Object} Logger object with four methods: + * @returns {MonitoringLogger} Logger object with five methods: * - `logDebug(message: string, contexts?: LogContext)`: Logs a debug message. * - `logInfo(message: string, contexts?: LogContext)`: Logs an informational message. * - `logWarning(message: string, contexts?: LogContext)`: Logs a warning message. @@ -91,7 +98,7 @@ export function logMeasurement(type: string, values: MeasurementValues, context? * - `logMeasurement(measurement: Omit, contexts?: LogContext)`: Logs a measurement. * Each method combines the `defaultContext` (if provided), the `source`, and an optional `LogContext` parameter into a full context that is included with the log message. */ -export function createMonitoringLogger(source: string, defaultContext?: LogContext) { +export function createMonitoringLogger(source: string, defaultContext?: LogContext): MonitoringLogger { const createFullContext = (contexts?: LogContext) => ({ source: source, ...defaultContext, diff --git a/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.ts b/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.ts index 2aa81fb11fc..fb181d2030e 100644 --- a/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.ts +++ b/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.ts @@ -5,6 +5,7 @@ import { BrowserConfig, ErrorsInstrumentation, ConsoleInstrumentation, + ConsoleInstrumentationOptions, WebVitalsInstrumentation, SessionInstrumentation, FetchTransport, @@ -60,6 +61,12 @@ export class GrafanaJavascriptAgentBackend ]; const transports: BaseTransport[] = [new EchoSrvTransport({ ignoreUrls })]; + const consoleInstrumentationOptions: ConsoleInstrumentationOptions = + options.allInstrumentationsEnabled || options.consoleInstrumentalizationEnabled + ? { + serializeErrors: true, + } + : {}; // If in cross origin iframe, default to writing to instance logging endpoint if (options.customEndpoint && !isCrossOriginIframe()) { @@ -94,6 +101,7 @@ export class GrafanaJavascriptAgentBackend instrumentations: options.allInstrumentationsEnabled ? instrumentations : [...getWebInstrumentations(), new TracingInstrumentation()], + consoleInstrumentation: consoleInstrumentationOptions, transports, ignoreErrors: [ 'ResizeObserver loop limit exceeded', diff --git a/yarn.lock b/yarn.lock index a00aba7fb24..077f50edb1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3311,7 +3311,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/faro-core@npm:^1.12.3, @grafana/faro-core@npm:^1.3.6": +"@grafana/faro-core@npm:^1.12.3": version: 1.12.3 resolution: "@grafana/faro-core@npm:1.12.3" dependencies: @@ -3321,7 +3321,17 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:1.12.3, @grafana/faro-web-sdk@npm:^1.12.1, @grafana/faro-web-sdk@npm:^1.3.6": +"@grafana/faro-core@npm:^1.13.1": + version: 1.13.1 + resolution: "@grafana/faro-core@npm:1.13.1" + dependencies: + "@opentelemetry/api": "npm:^1.9.0" + "@opentelemetry/otlp-transformer": "npm:^0.57.1" + checksum: 10/ce3747c476bd2b0f2b07c808c584953de75bf970fc17fbeb27258decf86919a753472cc464a4dccd4e0091003232feec6d3ba07f2e1b7bc38b487d9199abd18f + languageName: node + linkType: hard + +"@grafana/faro-web-sdk@npm:1.12.3, @grafana/faro-web-sdk@npm:^1.3.6": version: 1.12.3 resolution: "@grafana/faro-web-sdk@npm:1.12.3" dependencies: @@ -3332,23 +3342,34 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-tracing@npm:^1.8.2": - version: 1.12.1 - resolution: "@grafana/faro-web-tracing@npm:1.12.1" +"@grafana/faro-web-sdk@npm:^1.13.1": + version: 1.13.1 + resolution: "@grafana/faro-web-sdk@npm:1.13.1" dependencies: - "@grafana/faro-web-sdk": "npm:^1.12.1" + "@grafana/faro-core": "npm:^1.13.1" + ua-parser-js: "npm:^1.0.32" + web-vitals: "npm:^4.0.1" + checksum: 10/c06e0b5eb179ab3e5d29ec856a45f1c0e1e8980f1fc5b03500e3ca3ed725b74aae709abb0749f2053b532c20c28b937fa916101f3466f5fd30e979bbb826f151 + languageName: node + linkType: hard + +"@grafana/faro-web-tracing@npm:^1.13.1": + version: 1.13.1 + resolution: "@grafana/faro-web-tracing@npm:1.13.1" + dependencies: + "@grafana/faro-web-sdk": "npm:^1.13.1" "@opentelemetry/api": "npm:^1.9.0" - "@opentelemetry/context-zone": "npm:1.26.0" - "@opentelemetry/core": "npm:^1.26.0" - "@opentelemetry/exporter-trace-otlp-http": "npm:^0.53.0" - "@opentelemetry/instrumentation": "npm:^0.53.0" - "@opentelemetry/instrumentation-fetch": "npm:^0.53.0" - "@opentelemetry/instrumentation-xml-http-request": "npm:^0.53.0" - "@opentelemetry/otlp-transformer": "npm:^0.53.0" - "@opentelemetry/resources": "npm:^1.26.0" - "@opentelemetry/sdk-trace-web": "npm:^1.26.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - checksum: 10/3635d15bf22bf210d9758dfc0ac0562101ff50cccb18d006bbff282e219486e02847a6d2475e2bf76ca509a4862c533fce84c528134dfceef92a4d7985e0e746 + "@opentelemetry/context-zone": "npm:1.30.1" + "@opentelemetry/core": "npm:^1.30.0" + "@opentelemetry/exporter-trace-otlp-http": "npm:^0.57.0" + "@opentelemetry/instrumentation": "npm:^0.57.0" + "@opentelemetry/instrumentation-fetch": "npm:^0.57.0" + "@opentelemetry/instrumentation-xml-http-request": "npm:^0.57.0" + "@opentelemetry/otlp-transformer": "npm:^0.57.1" + "@opentelemetry/resources": "npm:^1.30.0" + "@opentelemetry/sdk-trace-web": "npm:^1.30.0" + "@opentelemetry/semantic-conventions": "npm:^1.28.0" + checksum: 10/378f235b384d4b53c32d5f7779a57114e48f7044cd6205fede8e368fc26f5d872d0146803159e82ed6b7a19b43b4317e867af7f0b8e9f90f63aa0dcb42db156d languageName: node linkType: hard @@ -5970,15 +5991,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/api-logs@npm:0.53.0" - dependencies: - "@opentelemetry/api": "npm:^1.0.0" - checksum: 10/347b4554d6ee01afb29bd39e8f9cbbccd80abb0883fe6a84e3bcce8ab4dbfe357a2729246d2f66de0de6272846fd1bb2d71e286e18ad2690d9e7f46f02f00f73 - languageName: node - linkType: hard - "@opentelemetry/api-logs@npm:0.57.1": version: 0.57.1 resolution: "@opentelemetry/api-logs@npm:0.57.1" @@ -5997,30 +6009,30 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api@npm:1.9.0, @opentelemetry/api@npm:^1.0.0, @opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.0": +"@opentelemetry/api@npm:1.9.0, @opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.0": version: 1.9.0 resolution: "@opentelemetry/api@npm:1.9.0" checksum: 10/a607f0eef971893c4f2ee2a4c2069aade6ec3e84e2a1f5c2aac19f65c5d9eeea41aa72db917c1029faafdd71789a1a040bdc18f40d63690e22ccae5d7070f194 languageName: node linkType: hard -"@opentelemetry/context-zone-peer-dep@npm:1.26.0": - version: 1.26.0 - resolution: "@opentelemetry/context-zone-peer-dep@npm:1.26.0" +"@opentelemetry/context-zone-peer-dep@npm:1.30.1": + version: 1.30.1 + resolution: "@opentelemetry/context-zone-peer-dep@npm:1.30.1" peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - zone.js: ^0.10.2 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0 - checksum: 10/aed06016c380001418656810f80d24b68d05ce8d8138ed278fd27ed7990961b02dc4f3e1e75511662eb97afcfb4b7f96be2dfd28894eba3edff375be8908749a + zone.js: ^0.10.2 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0 || ^0.15.0 + checksum: 10/07fcc721f718f356902cb1028cb1bf79a029dc81113e386a7a5e2110331fddef20e5f1e04e905c8ce22b2b8879ba5a86509794b285357e128fcde95cf31d7fdf languageName: node linkType: hard -"@opentelemetry/context-zone@npm:1.26.0": - version: 1.26.0 - resolution: "@opentelemetry/context-zone@npm:1.26.0" +"@opentelemetry/context-zone@npm:1.30.1": + version: 1.30.1 + resolution: "@opentelemetry/context-zone@npm:1.30.1" dependencies: - "@opentelemetry/context-zone-peer-dep": "npm:1.26.0" - zone.js: "npm:^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0" - checksum: 10/16ddaa1129e818a6950db15f5f2c254e7d3216f2e7deb93ecb5b11496d4a68ea043cc469ac0f426840b797a21ad9162daabe74b5311ac3d0661fc6b64c51fff6 + "@opentelemetry/context-zone-peer-dep": "npm:1.30.1" + zone.js: "npm:^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0 || ^0.15.0" + checksum: 10/54517b4dbd9b32c1fc5c268a85d3778e0c3daadc6e2963d491e4e3b889175c02ae1e0e128ae5d180a0c6b6ac80655551693a301391408d78c8dd96cd08217aa2 languageName: node linkType: hard @@ -6036,18 +6048,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/core@npm:1.26.0": - version: 1.26.0 - resolution: "@opentelemetry/core@npm:1.26.0" - dependencies: - "@opentelemetry/semantic-conventions": "npm:1.27.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10/474b6bcf42cd2825d56f915eb0d6e6cdcb37777a11fc2618fc2fa50754f4b9b5df23944f3aab186cb3ab930db5c3a81efa3183362802314a966930110346e6a4 - languageName: node - linkType: hard - -"@opentelemetry/core@npm:1.30.1, @opentelemetry/core@npm:^1.26.0": +"@opentelemetry/core@npm:1.30.1, @opentelemetry/core@npm:^1.30.0": version: 1.30.1 resolution: "@opentelemetry/core@npm:1.30.1" dependencies: @@ -6073,54 +6074,54 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-http@npm:^0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.53.0" +"@opentelemetry/exporter-trace-otlp-http@npm:^0.57.0": + version: 0.57.1 + resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.57.1" dependencies: - "@opentelemetry/core": "npm:1.26.0" - "@opentelemetry/otlp-exporter-base": "npm:0.53.0" - "@opentelemetry/otlp-transformer": "npm:0.53.0" - "@opentelemetry/resources": "npm:1.26.0" - "@opentelemetry/sdk-trace-base": "npm:1.26.0" + "@opentelemetry/core": "npm:1.30.1" + "@opentelemetry/otlp-exporter-base": "npm:0.57.1" + "@opentelemetry/otlp-transformer": "npm:0.57.1" + "@opentelemetry/resources": "npm:1.30.1" + "@opentelemetry/sdk-trace-base": "npm:1.30.1" peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 10/28c75e25564833bc448b5733415730483c9f28714577acb679087d5ccfc46d74b3f24996c41f2c93bf6a6406edb1cad7e8cf2a76b61096e3f417f90044e1d795 + "@opentelemetry/api": ^1.3.0 + checksum: 10/94c1a0f70b1272c338a3cace0e2ec2d3958fe407ef8d6245d9f497a19cec95430c1226750a3148cd1f069c5e1b9871fa9889844c88094b3e1b81c7a3a2e25012 languageName: node linkType: hard -"@opentelemetry/instrumentation-fetch@npm:^0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/instrumentation-fetch@npm:0.53.0" +"@opentelemetry/instrumentation-fetch@npm:^0.57.0": + version: 0.57.1 + resolution: "@opentelemetry/instrumentation-fetch@npm:0.57.1" dependencies: - "@opentelemetry/core": "npm:1.26.0" - "@opentelemetry/instrumentation": "npm:0.53.0" - "@opentelemetry/sdk-trace-web": "npm:1.26.0" - "@opentelemetry/semantic-conventions": "npm:1.27.0" + "@opentelemetry/core": "npm:1.30.1" + "@opentelemetry/instrumentation": "npm:0.57.1" + "@opentelemetry/sdk-trace-web": "npm:1.30.1" + "@opentelemetry/semantic-conventions": "npm:1.28.0" peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 10/422e9c749523a2be4cd6b82304f20b3208ced12a14990b1cfee273c7485252c94618336c94342a0b0ad72863ec5519d3b1c31f0d09395c29926ecbc0986c9b15 + "@opentelemetry/api": ^1.3.0 + checksum: 10/e736a62a5952aff0a0a7bfa1d04f67912f57945e151b798e6d7e7fddd2c3a293b44f36ae1b0a612495da8d3b7b235bb46a9fd18e64c5a6b5319f1b7296d6170f languageName: node linkType: hard -"@opentelemetry/instrumentation-xml-http-request@npm:^0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/instrumentation-xml-http-request@npm:0.53.0" +"@opentelemetry/instrumentation-xml-http-request@npm:^0.57.0": + version: 0.57.1 + resolution: "@opentelemetry/instrumentation-xml-http-request@npm:0.57.1" dependencies: - "@opentelemetry/core": "npm:1.26.0" - "@opentelemetry/instrumentation": "npm:0.53.0" - "@opentelemetry/sdk-trace-web": "npm:1.26.0" - "@opentelemetry/semantic-conventions": "npm:1.27.0" + "@opentelemetry/core": "npm:1.30.1" + "@opentelemetry/instrumentation": "npm:0.57.1" + "@opentelemetry/sdk-trace-web": "npm:1.30.1" + "@opentelemetry/semantic-conventions": "npm:1.28.0" peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 10/82c7e3d9f54fafccfabfe02033b11ca39b052134ec6e8902ac3c54993745fc02503f60e7d223b74319e855091c5f961e78483857a690633958fb61852c041749 + "@opentelemetry/api": ^1.3.0 + checksum: 10/dad99f2d37c550177575a621c4538677a173c607bcc43133095c58fe8c36101d38e3e0cc84158bd717e4ad925dcec4788f30ae5592fdf27cc71e411990544473 languageName: node linkType: hard -"@opentelemetry/instrumentation@npm:0.53.0, @opentelemetry/instrumentation@npm:^0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/instrumentation@npm:0.53.0" +"@opentelemetry/instrumentation@npm:0.57.1, @opentelemetry/instrumentation@npm:^0.57.0": + version: 0.57.1 + resolution: "@opentelemetry/instrumentation@npm:0.57.1" dependencies: - "@opentelemetry/api-logs": "npm:0.53.0" + "@opentelemetry/api-logs": "npm:0.57.1" "@types/shimmer": "npm:^1.2.0" import-in-the-middle: "npm:^1.8.1" require-in-the-middle: "npm:^7.1.1" @@ -6128,40 +6129,23 @@ __metadata: shimmer: "npm:^1.2.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/4b994c8568a503a15655cba249b1dbdef3f67dfda37938abba6267ba75b6d72a9aa276be4b0c8874e86f98ab89d92877e1874e0565a7e67f062c43dfcbbb16a5 + checksum: 10/8f21a1b69aab5b48f8d85da2dd944d12f498757b890d4da062f7736a2254b19fb2c678db1807889e0526d3bbb653455c24c0d89523662d358fdb4e615f099fcf languageName: node linkType: hard -"@opentelemetry/otlp-exporter-base@npm:0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/otlp-exporter-base@npm:0.53.0" +"@opentelemetry/otlp-exporter-base@npm:0.57.1": + version: 0.57.1 + resolution: "@opentelemetry/otlp-exporter-base@npm:0.57.1" dependencies: - "@opentelemetry/core": "npm:1.26.0" - "@opentelemetry/otlp-transformer": "npm:0.53.0" - peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 10/ca59d73ae8f83946062b060a9a382fc7db6154c892ed56b6ab7f545530ba4850b4d0a748daaa30d1177ef6a8c2a0fddd34a199080f4474ec445944cece86f1ef - languageName: node - linkType: hard - -"@opentelemetry/otlp-transformer@npm:0.53.0, @opentelemetry/otlp-transformer@npm:^0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/otlp-transformer@npm:0.53.0" - dependencies: - "@opentelemetry/api-logs": "npm:0.53.0" - "@opentelemetry/core": "npm:1.26.0" - "@opentelemetry/resources": "npm:1.26.0" - "@opentelemetry/sdk-logs": "npm:0.53.0" - "@opentelemetry/sdk-metrics": "npm:1.26.0" - "@opentelemetry/sdk-trace-base": "npm:1.26.0" - protobufjs: "npm:^7.3.0" + "@opentelemetry/core": "npm:1.30.1" + "@opentelemetry/otlp-transformer": "npm:0.57.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/578cf13d7984a0b1ba1db3d86d1e358bf70e8b534166f8327a10fccca0afd3900896a80e5e73caae61837b0cbc99d81b44784edee68a3517d73f5330a3624ccd + checksum: 10/973d92d99f85926f9f19d9a7ef5d549aa72d91707299608ae6494c38cb4dba44baabc8c0f35ced116a5c851fb5cb354c650b292cb7a0efcdc39aa34a0917564b languageName: node linkType: hard -"@opentelemetry/otlp-transformer@npm:^0.57.1": +"@opentelemetry/otlp-transformer@npm:0.57.1, @opentelemetry/otlp-transformer@npm:^0.57.1": version: 0.57.1 resolution: "@opentelemetry/otlp-transformer@npm:0.57.1" dependencies: @@ -6190,19 +6174,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/resources@npm:1.26.0": - version: 1.26.0 - resolution: "@opentelemetry/resources@npm:1.26.0" - dependencies: - "@opentelemetry/core": "npm:1.26.0" - "@opentelemetry/semantic-conventions": "npm:1.27.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10/ce60dbf2bd424b01824b72f533724eaf64418e01c43bef952b87dbff6d2a0f28cdcbea0d3d95c5e324f609e58721bf52ea91b5518b0e30d6bb03fb95af85cc33 - languageName: node - linkType: hard - -"@opentelemetry/resources@npm:1.30.1, @opentelemetry/resources@npm:^1.26.0": +"@opentelemetry/resources@npm:1.30.1, @opentelemetry/resources@npm:^1.30.0": version: 1.30.1 resolution: "@opentelemetry/resources@npm:1.30.1" dependencies: @@ -6214,19 +6186,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-logs@npm:0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/sdk-logs@npm:0.53.0" - dependencies: - "@opentelemetry/api-logs": "npm:0.53.0" - "@opentelemetry/core": "npm:1.26.0" - "@opentelemetry/resources": "npm:1.26.0" - peerDependencies: - "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10/b11b512820f3d55288f7478831587ebe2e7077980f060a779a13848c62cab30023734857c68ef110eebe961884cb8892d7c77841a5f1d22c2426cbb18d762975 - languageName: node - linkType: hard - "@opentelemetry/sdk-logs@npm:0.57.1": version: 0.57.1 resolution: "@opentelemetry/sdk-logs@npm:0.57.1" @@ -6254,18 +6213,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-metrics@npm:1.26.0": - version: 1.26.0 - resolution: "@opentelemetry/sdk-metrics@npm:1.26.0" - dependencies: - "@opentelemetry/core": "npm:1.26.0" - "@opentelemetry/resources": "npm:1.26.0" - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10/e48e4dd1fed1e501750460e1320f89507c19287c5059cfaccc8268ad8cc3e1de40feeee6584b23626e01f9cde0f10301d08edf6a65bbd1346ef94f70ae8844f5 - languageName: node - linkType: hard - "@opentelemetry/sdk-metrics@npm:1.30.1": version: 1.30.1 resolution: "@opentelemetry/sdk-metrics@npm:1.30.1" @@ -6292,19 +6239,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-trace-base@npm:1.26.0": - version: 1.26.0 - resolution: "@opentelemetry/sdk-trace-base@npm:1.26.0" - dependencies: - "@opentelemetry/core": "npm:1.26.0" - "@opentelemetry/resources": "npm:1.26.0" - "@opentelemetry/semantic-conventions": "npm:1.27.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10/e4a3d296ad908b9f58d7aefdcc1f7383fb0eb64fc85b0b5d18c4a7d829ce3d0efa5e53f5fe1a23185d9b5d97b782431384efe01aba8ba788922260a9dbbdb662 - languageName: node - linkType: hard - "@opentelemetry/sdk-trace-base@npm:1.30.1": version: 1.30.1 resolution: "@opentelemetry/sdk-trace-base@npm:1.30.1" @@ -6318,16 +6252,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-trace-web@npm:1.26.0, @opentelemetry/sdk-trace-web@npm:^1.26.0": - version: 1.26.0 - resolution: "@opentelemetry/sdk-trace-web@npm:1.26.0" +"@opentelemetry/sdk-trace-web@npm:1.30.1, @opentelemetry/sdk-trace-web@npm:^1.30.0": + version: 1.30.1 + resolution: "@opentelemetry/sdk-trace-web@npm:1.30.1" dependencies: - "@opentelemetry/core": "npm:1.26.0" - "@opentelemetry/sdk-trace-base": "npm:1.26.0" - "@opentelemetry/semantic-conventions": "npm:1.27.0" + "@opentelemetry/core": "npm:1.30.1" + "@opentelemetry/sdk-trace-base": "npm:1.30.1" + "@opentelemetry/semantic-conventions": "npm:1.28.0" peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10/2882863d02510151460850575269129c603f2b3e44f5b2c34bede4128d7f724b7bb8bb8253469848110725e2b2af9340d13a81d565ea9e0c80cf57804b02d65f + checksum: 10/43e73a70201d936dbc69934107a92dcdbeaf4c1043935e1c8803462226ff6bf6dfc4be83c110efbaf041209af8365530cbe5b9734867091e565ef11d7a534b7a languageName: node linkType: hard @@ -6338,20 +6272,20 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/semantic-conventions@npm:1.27.0": - version: 1.27.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.27.0" - checksum: 10/98166522f299e2fe3d43376adbdeb92679b75ebb172e2a3c4c71f2942bd91585e9537618efbbae6dc08177699e5719368edf66d7e69e8636f360b85217bbdbe1 - languageName: node - linkType: hard - -"@opentelemetry/semantic-conventions@npm:1.28.0, @opentelemetry/semantic-conventions@npm:^1.27.0": +"@opentelemetry/semantic-conventions@npm:1.28.0": version: 1.28.0 resolution: "@opentelemetry/semantic-conventions@npm:1.28.0" checksum: 10/c182a3206769b5d5a8ab89a5c674d046fd789421cef27ea55af179990e314732433c98e5017aa23e99f15fd2b0e13cb129bb6c2282da6860ce9419adf32b2e87 languageName: node linkType: hard +"@opentelemetry/semantic-conventions@npm:^1.28.0": + version: 1.29.0 + resolution: "@opentelemetry/semantic-conventions@npm:1.29.0" + checksum: 10/94008a2e374f18dd71a30fe8b91cb58dccbbe3e13e1de2200907e42fd1441459acff65c5eadcbfff30f7e9575895df5dc8ced01228f434e555a9a3d72b595752 + languageName: node + linkType: hard + "@parcel/watcher-android-arm64@npm:2.4.1": version: 2.4.1 resolution: "@parcel/watcher-android-arm64@npm:2.4.1" @@ -18234,9 +18168,9 @@ __metadata: "@grafana/e2e-selectors": "workspace:*" "@grafana/eslint-config": "npm:8.0.0" "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules" - "@grafana/faro-core": "npm:^1.3.6" - "@grafana/faro-web-sdk": "npm:^1.3.6" - "@grafana/faro-web-tracing": "npm:^1.8.2" + "@grafana/faro-core": "npm:^1.13.1" + "@grafana/faro-web-sdk": "npm:^1.13.1" + "@grafana/faro-web-tracing": "npm:^1.13.1" "@grafana/flamegraph": "workspace:*" "@grafana/google-sdk": "npm:0.1.2" "@grafana/lezer-logql": "npm:0.2.7" @@ -32653,9 +32587,9 @@ __metadata: languageName: node linkType: hard -"zone.js@npm:^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0": - version: 0.14.10 - resolution: "zone.js@npm:0.14.10" - checksum: 10/a7bed2f9a7ce67ba4e70b03b7e59dc955e4d2d738570950c4a0e16fafb5c23c47d0c6ece84f6e871a0f77d81c26a051da566d09738ebfdab297f54b862ae0b5d +"zone.js@npm:^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0 || ^0.15.0": + version: 0.15.0 + resolution: "zone.js@npm:0.15.0" + checksum: 10/99b9381edcf1ca3da147375a9776f8ad5e6570b9e2cbd33095284a67904d94b5083448440ffcb8ec1e418a505020de0e37837db04d6a0303e111b054a8b752a2 languageName: node linkType: hard From acc15219296206fb1a971b3742110e6a9d972cef Mon Sep 17 00:00:00 2001 From: Misi Date: Mon, 10 Feb 2025 10:48:35 +0100 Subject: [PATCH 447/894] Auth: Fix redirect with JWT auth URL login (#100295) fix --- public/app/app.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/app/app.ts b/public/app/app.ts index d49f27a8732..f4c4fd5d77c 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -403,6 +403,11 @@ function handleRedirectTo(): void { const queryParams = locationService.getSearch(); const redirectToParamKey = 'redirectTo'; + if (queryParams.has('auth_token')) { + // URL Login should not be redirected + window.sessionStorage.removeItem(RedirectToUrlKey); + } + if (queryParams.has(redirectToParamKey) && window.location.pathname !== '/') { const rawRedirectTo = queryParams.get(redirectToParamKey)!; window.sessionStorage.setItem(RedirectToUrlKey, encodeURIComponent(rawRedirectTo)); From 00155abf1b2f54c76dd136c2f33eef723ce2cdc3 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 10 Feb 2025 09:50:49 +0000 Subject: [PATCH 448/894] Codeowners: Make Grafana Frontend Platform only own Grafana UI documentation (#100314) * Codeowners: Make Grafana Frontend Platform exclusive owners of MDX documentation * more --- .github/CODEOWNERS | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3848ffb1f07..cbb2fa1a54f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -405,7 +405,7 @@ /packages/grafana-schema/src/**/*canvas* @grafana/dataviz-squad /packages/grafana-schema/src/**/*tempo* @grafana/observability-traces-and-profiling /packages/grafana-sql/ @grafana/partner-datasources @grafana/oss-big-tent -/packages/grafana-ui/.storybook/ @grafana/plugins-platform-frontend +/packages/grafana-ui/.storybook/ @grafana/grafana-frontend-platform /packages/grafana-ui/src/components/ @grafana/grafana-frontend-platform /packages/grafana-ui/src/components/BarGauge/ @grafana/dataviz-squad /packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad @@ -424,7 +424,7 @@ /packages/grafana-ui/src/graveyard/Graph/ @grafana/dataviz-squad /packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad /packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad -/packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend +/packages/grafana-ui/src/utils/storybook/ @grafana/grafana-frontend-platform # root files, mostly frontend /.browserslistrc @grafana/frontend-ops @@ -637,9 +637,6 @@ playwright.config.ts @grafana/plugins-platform-frontend .betterer.results @grafanabot .betterer.ts @grafana/grafana-frontend-platform -# @grafana/ui component documentation -*.mdx @grafana/plugins-platform-frontend - # Design system /public/img/icons/unicons/ @grafana/design-system From 6723159b12333efab889898fe831f19a7e78c4db Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Mon, 10 Feb 2025 10:51:54 +0100 Subject: [PATCH 449/894] Alerting: Fix useAlertingQueryRunner re-rendering loop (#100206) * Fix AlertingQueryRunner infinite re-rendering loop * Update tests --- .../rule-editor/RecordingRuleEditor.tsx | 65 ++++++++++--------- .../components/rule-viewer/tabs/Query.tsx | 65 ++++++++----------- .../unified/state/AlertingQueryRunner.test.ts | 16 ++--- .../unified/state/AlertingQueryRunner.ts | 3 +- 4 files changed, 65 insertions(+), 84 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/RecordingRuleEditor.tsx b/public/app/features/alerting/unified/components/rule-editor/RecordingRuleEditor.tsx index 7e29b573b54..73431a3f64b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/RecordingRuleEditor.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/RecordingRuleEditor.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { FC, useEffect, useState } from 'react'; +import { FC, useCallback, useEffect, useState } from 'react'; import { useAsync } from 'react-use'; import { CoreApp, GrafanaTheme2, LoadingState, PanelData } from '@grafana/data'; @@ -51,39 +51,42 @@ export const RecordingRuleEditor: FC = ({ return getDataSourceSrv().get(dataSourceName); }, [dataSourceName]); - const handleChangedQuery = (changedQuery: DataQuery) => { - if (!isPromOrLokiQuery(changedQuery) || !dataSource) { - return; - } + const handleChangedQuery = useCallback( + (changedQuery: DataQuery) => { + if (!isPromOrLokiQuery(changedQuery) || !dataSource) { + return; + } - const [query] = queries; - const { uid: dataSourceId, type } = dataSource; - const isLoki = type === DataSourceType.Loki; - const expr = changedQuery.expr; + const [query] = queries; + const { uid: dataSourceId, type } = dataSource; + const isLoki = type === DataSourceType.Loki; + const expr = changedQuery.expr; - const merged = { - ...query, - ...changedQuery, - datasourceUid: dataSourceId, - expr, - model: { + const merged = { + ...query, + ...changedQuery, + datasourceUid: dataSourceId, expr, - datasource: changedQuery.datasource, - refId: changedQuery.refId, - editorMode: changedQuery.editorMode, - // Instant and range are used by Prometheus queries - instant: changedQuery.instant, - range: changedQuery.range, - // Query type is used by Loki queries - // On first render/when creating a recording rule, the query type is not set - // unless the user has changed it betwee range/instant. The cleanest way to handle this - // is to default to instant, or whatever the changed type is - queryType: isLoki ? changedQuery.queryType || LokiQueryType.Instant : changedQuery.queryType, - legendFormat: changedQuery.legendFormat, - }, - }; - onChangeQuery([merged]); - }; + model: { + expr, + datasource: changedQuery.datasource, + refId: changedQuery.refId, + editorMode: changedQuery.editorMode, + // Instant and range are used by Prometheus queries + instant: changedQuery.instant, + range: changedQuery.range, + // Query type is used by Loki queries + // On first render/when creating a recording rule, the query type is not set + // unless the user has changed it betwee range/instant. The cleanest way to handle this + // is to default to instant, or whatever the changed type is + queryType: isLoki ? changedQuery.queryType || LokiQueryType.Instant : changedQuery.queryType, + legendFormat: changedQuery.legendFormat, + }, + }; + onChangeQuery([merged]); + }, + [dataSource, queries, onChangeQuery] + ); if (loading || dataSource?.name !== dataSourceName) { return null; diff --git a/public/app/features/alerting/unified/components/rule-viewer/tabs/Query.tsx b/public/app/features/alerting/unified/components/rule-viewer/tabs/Query.tsx index 967b3f76a75..c7550d5bd21 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/tabs/Query.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/tabs/Query.tsx @@ -1,25 +1,21 @@ import { useCallback, useEffect, useMemo } from 'react'; -import { useObservable } from 'react-use'; -import { LoadingState, PanelData } from '@grafana/data'; import { config } from '@grafana/runtime'; import { Alert, Stack } from '@grafana/ui'; import { CombinedRule } from 'app/types/unified-alerting'; import { GrafanaRuleQueryViewer, QueryPreview } from '../../../GrafanaRuleQueryViewer'; import { useAlertQueriesStatus } from '../../../hooks/useAlertQueriesStatus'; -import { AlertingQueryRunner } from '../../../state/AlertingQueryRunner'; import { alertRuleToQueries } from '../../../utils/query'; import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../../utils/rules'; +import { useAlertQueryRunner } from '../../rule-editor/query-and-alert-condition/useAlertQueryRunner'; interface Props { rule: CombinedRule; } const QueryResults = ({ rule }: Props) => { - const runner = useMemo(() => new AlertingQueryRunner(), []); - const data = useObservable(runner.get()); - const loadingData = isLoading(data); + const { queryPreviewData, runQueries, isPreviewLoading } = useAlertQueryRunner(); const queries = useMemo(() => alertRuleToQueries(rule), [rule]); @@ -31,9 +27,9 @@ const QueryResults = ({ rule }: Props) => { if (rule && isGrafanaRulerRule(rule.rulerRule)) { condition = rule.rulerRule.grafana_alert.condition; } - runner.run(queries, condition ?? 'A'); + runQueries(queries, condition ?? 'A'); } - }, [queries, allDataSourcesAvailable, rule, runner]); + }, [queries, allDataSourcesAvailable, rule, runQueries]); useEffect(() => { if (allDataSourcesAvailable) { @@ -41,15 +37,11 @@ const QueryResults = ({ rule }: Props) => { } }, [allDataSourcesAvailable, onRunQueries]); - useEffect(() => { - return () => runner.destroy(); - }, [runner]); - const isFederatedRule = isFederatedRuleGroup(rule.group); return ( <> - {loadingData ? ( + {isPreviewLoading ? ( 'Loading...' ) : ( <> @@ -58,27 +50,30 @@ const QueryResults = ({ rule }: Props) => { rule={rule} condition={rule.rulerRule.grafana_alert.condition} queries={queries} - evalDataByQuery={data} + evalDataByQuery={queryPreviewData} /> )} - {!isGrafanaRulerRule(rule.rulerRule) && !isFederatedRule && data && Object.keys(data).length > 0 && ( - - {queries.map((query) => { - return ( - ds.uid === query.datasourceUid)} - queryData={data[query.refId]} - relativeTimeRange={query.relativeTimeRange} - /> - ); - })} - - )} + {!isGrafanaRulerRule(rule.rulerRule) && + !isFederatedRule && + queryPreviewData && + Object.keys(queryPreviewData).length > 0 && ( + + {queries.map((query) => { + return ( + ds.uid === query.datasourceUid)} + queryData={queryPreviewData[query.refId]} + relativeTimeRange={query.relativeTimeRange} + /> + ); + })} + + )} {!isFederatedRule && !allDataSourcesAvailable && ( Cannot display the query preview. Some of the data sources used in the queries are not available. @@ -90,12 +85,4 @@ const QueryResults = ({ rule }: Props) => { ); }; -function isLoading(data?: Record): boolean { - if (!data) { - return true; - } - - return !!Object.values(data).find((d) => d.state === LoadingState.Loading); -} - export { QueryResults }; diff --git a/public/app/features/alerting/unified/state/AlertingQueryRunner.test.ts b/public/app/features/alerting/unified/state/AlertingQueryRunner.test.ts index a926172c907..8985847981a 100644 --- a/public/app/features/alerting/unified/state/AlertingQueryRunner.test.ts +++ b/public/app/features/alerting/unified/state/AlertingQueryRunner.test.ts @@ -1,6 +1,6 @@ import { defaultsDeep } from 'lodash'; -import { Observable, of, throwError } from 'rxjs'; -import { delay, take } from 'rxjs/operators'; +import { Observable, TimeoutError, lastValueFrom, of, throwError } from 'rxjs'; +import { delay, take, timeout } from 'rxjs/operators'; import { createFetchResponse } from 'test/helpers/createFetchResponse'; import { @@ -200,7 +200,7 @@ describe('AlertingQueryRunner', () => { }); }); - it('should not execute if all queries fail filterQuery check', async () => { + it('should not push any values if all queries fail filterQuery check', async () => { const runner = new AlertingQueryRunner( mockBackendSrv({ fetch: () => throwError(new Error("shouldn't happen")), @@ -211,15 +211,7 @@ describe('AlertingQueryRunner', () => { const data = runner.get(); runner.run([createQuery('A'), createQuery('B')], 'B'); - await expect(data.pipe(take(1))).toEmitValuesWith((values) => { - const [data] = values; - - expect(data.A.state).toEqual(LoadingState.Done); - expect(data.A.series).toHaveLength(0); - - expect(data.B.state).toEqual(LoadingState.Done); - expect(data.B.series).toHaveLength(0); - }); + await expect(lastValueFrom(data.pipe(timeout(200)))).rejects.toThrow(TimeoutError); }); it('should skip hidden queries and descendant nodes', async () => { diff --git a/public/app/features/alerting/unified/state/AlertingQueryRunner.ts b/public/app/features/alerting/unified/state/AlertingQueryRunner.ts index fcba1587acc..3e8fc2b8cf7 100644 --- a/public/app/features/alerting/unified/state/AlertingQueryRunner.ts +++ b/public/app/features/alerting/unified/state/AlertingQueryRunner.ts @@ -51,11 +51,10 @@ export class AlertingQueryRunner { } async run(queries: AlertQuery[], condition: string) { - const empty = initialState(queries, LoadingState.Done); const queriesToRun = await this.prepareQueries(queries); if (queriesToRun.length === 0) { - return this.subject.next(empty); + return; } this.subscription = runRequest(this.backendSrv, queriesToRun, condition).subscribe({ From afab71e28c35c4baf3f59037ffa9ba21729dc33f Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Mon, 10 Feb 2025 11:13:28 +0100 Subject: [PATCH 450/894] Alerting: Remove rule group edit from single rule editor (#100191) remove rule group edit from single rule editor --- .../rule-editor/GrafanaEvaluationBehavior.tsx | 40 ++----------------- 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx index 5178df5a685..988649f6b3e 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -10,7 +10,6 @@ import { Button, Field, Icon, - IconButton, Input, Label, Modal, @@ -28,7 +27,6 @@ import { alertRuleApi } from '../../api/alertRuleApi'; import { GRAFANA_RULER_CONFIG } from '../../api/featureDiscoveryApi'; import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../rule-editor/formDefaults'; import { RuleFormValues } from '../../types/rule-form'; -import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { isGrafanaAlertingRuleByType, isGrafanaManagedRuleByType, @@ -38,7 +36,7 @@ import { import { parsePrometheusDuration } from '../../utils/time'; import { CollapseToggle } from '../CollapseToggle'; import { ProvisioningBadge } from '../Provisioning'; -import { EditRuleGroupModal, evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal'; +import { evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal'; import { EvaluationGroupQuickPick } from './EvaluationGroupQuickPick'; import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker'; @@ -160,7 +158,6 @@ export function GrafanaEvaluationBehaviorStep({ const isGrafanaAlertingRule = isGrafanaAlertingRuleByType(type); const isGrafanaRecordingRule = isGrafanaRecordingRuleByType(type); const { currentData: rulerNamespace, isLoading: loadingGroups } = useFetchGroupsForFolder(folder?.uid ?? ''); - const [isEditingGroup, setIsEditingGroup] = useState(false); const groupOptions = useMemo(() => { return rulerNamespace ? namespaceToGroupOptions(rulerNamespace, enableProvisionedGroups) : []; @@ -169,7 +166,6 @@ export function GrafanaEvaluationBehaviorStep({ const existingGroup = Object.values(rulerNamespace ?? {}) .flat() .find((ruleGroup) => ruleGroup.name === group); - const isNewGroup = !existingGroup && !loadingGroups; // synchronize the evaluation interval with the group name when it's an existing group useEffect(() => { @@ -178,11 +174,6 @@ export function GrafanaEvaluationBehaviorStep({ } }, [existingGroup, setValue]); - const closeEditGroupModal = () => setIsEditingGroup(false); - const onOpenEditGroupModal = () => setIsEditingGroup(true); - - const editGroupDisabled = loadingGroups || isNewGroup || !folder?.uid || !group; - const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false); const handleEvalGroupCreation = (groupName: string, evaluationInterval: string) => { @@ -287,38 +278,15 @@ export function GrafanaEvaluationBehaviorStep({ )} - {folder?.uid && isEditingGroup && ( - closeEditGroupModal()} - intervalEditOnly - hideFolder={true} - /> - )} {folder?.title && group && (
{getValues('group') && getValues('evaluateEvery') && ( - - - All rules in the selected group are evaluated every {{ evaluateEvery }}. - - {!isNewGroup && ( - - )} - + + All rules in the selected group are evaluated every {{ evaluateEvery }}. + )}
From 390b5eb6d475bca511e609ef27258069b864a024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Mon, 10 Feb 2025 11:38:30 +0100 Subject: [PATCH 451/894] go.mod: updated grafana-plugin-sdk-go (#100342) * go.mod: update grafana-plugin-sdk-go * make update-workspace --- apps/alerting/notifications/go.mod | 8 ++--- apps/alerting/notifications/go.sum | 16 ++++----- apps/investigation/go.mod | 11 +++--- apps/investigation/go.sum | 22 ++++++------ apps/playlist/go.mod | 11 +++--- apps/playlist/go.sum | 22 ++++++------ go.mod | 30 +++++++++------- go.sum | 54 +++++++++++++++-------------- go.work.sum | 1 + pkg/aggregator/go.mod | 31 +++++++++-------- pkg/aggregator/go.sum | 54 +++++++++++++++-------------- pkg/apimachinery/go.mod | 4 +-- pkg/apimachinery/go.sum | 8 ++--- pkg/apiserver/go.mod | 10 +++--- pkg/apiserver/go.sum | 20 +++++------ pkg/build/go.mod | 8 ++--- pkg/build/go.sum | 16 ++++----- pkg/build/wire/go.mod | 2 +- pkg/build/wire/go.sum | 4 +-- pkg/codegen/go.mod | 7 ++-- pkg/codegen/go.sum | 14 ++++---- pkg/plugins/codegen/go.mod | 7 ++-- pkg/plugins/codegen/go.sum | 14 ++++---- pkg/promlib/go.mod | 30 ++++++++-------- pkg/promlib/go.sum | 54 +++++++++++++++-------------- pkg/storage/unified/apistore/go.mod | 27 ++++++++------- pkg/storage/unified/apistore/go.sum | 54 +++++++++++++++-------------- pkg/storage/unified/resource/go.mod | 27 ++++++++------- pkg/storage/unified/resource/go.sum | 54 +++++++++++++++-------------- 29 files changed, 327 insertions(+), 293 deletions(-) diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index fa03852fea1..348d3e997ca 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -52,7 +52,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -61,8 +61,8 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect go.etcd.io/etcd/client/v3 v3.5.16 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.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.3 // indirect + google.golang.org/protobuf v1.36.4 // 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 df3c4bdd06f..88f0f30100a 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -130,8 +130,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= @@ -178,10 +178,10 @@ go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +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/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= @@ -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.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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/investigation/go.mod b/apps/investigation/go.mod index 531dfcc4346..c931cbb36a2 100644 --- a/apps/investigation/go.mod +++ b/apps/investigation/go.mod @@ -18,7 +18,7 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/getkin/kin-openapi v0.128.0 // indirect + github.com/getkin/kin-openapi v0.129.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -35,7 +35,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect @@ -44,10 +43,12 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -63,7 +64,7 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect @@ -72,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.3 // indirect + google.golang.org/protobuf v1.36.4 // 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/investigation/go.sum b/apps/investigation/go.sum index bdb711181f5..eb9c8887d1d 100644 --- a/apps/investigation/go.sum +++ b/apps/investigation/go.sum @@ -16,8 +16,8 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= -github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30= +github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -60,8 +60,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= 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= @@ -87,6 +85,10 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= @@ -102,8 +104,8 @@ github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+ github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= @@ -160,8 +162,8 @@ golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -193,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.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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 3be13946b41..214e97e3054 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -19,7 +19,7 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/getkin/kin-openapi v0.128.0 // indirect + github.com/getkin/kin-openapi v0.129.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -36,7 +36,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect @@ -45,10 +44,12 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -64,7 +65,7 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect @@ -73,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.3 // indirect + google.golang.org/protobuf v1.36.4 // 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 bdb711181f5..eb9c8887d1d 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -16,8 +16,8 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= -github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30= +github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -60,8 +60,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= 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= @@ -87,6 +85,10 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= @@ -102,8 +104,8 @@ github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+ github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= @@ -160,8 +162,8 @@ golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -193,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.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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 a4495e1314a..e8ac930a4ca 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( github.com/fatih/color v1.17.0 // @grafana/grafana-backend-group github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage - github.com/getkin/kin-openapi v0.128.0 // @grafana/grafana-app-platform-squad + github.com/getkin/kin-openapi v0.129.0 // @grafana/grafana-app-platform-squad github.com/go-jose/go-jose/v3 v3.0.3 // @grafana/identity-access-team github.com/go-kit/log v0.2.1 // @grafana/grafana-backend-group github.com/go-ldap/ldap/v3 v3.4.4 // @grafana/identity-access-team @@ -87,7 +87,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.263.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.265.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 @@ -98,7 +98,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // @grafana/identity-access-team github.com/hashicorp/go-hclog v1.6.3 // @grafana/plugins-platform-backend github.com/hashicorp/go-multierror v1.1.1 // @grafana/alerting-squad - github.com/hashicorp/go-plugin v1.6.2 // @grafana/plugins-platform-backend + github.com/hashicorp/go-plugin v1.6.3 // @grafana/plugins-platform-backend github.com/hashicorp/go-version v1.7.0 // @grafana/grafana-backend-group github.com/hashicorp/golang-lru/v2 v2.0.7 // @grafana/alerting-backend github.com/hashicorp/hcl/v2 v2.17.0 // @grafana/alerting-backend @@ -134,7 +134,7 @@ require ( github.com/prometheus/alertmanager v0.27.0 // @grafana/alerting-backend github.com/prometheus/client_golang v1.20.5 // @grafana/alerting-backend github.com/prometheus/client_model v0.6.1 // @grafana/grafana-backend-group - github.com/prometheus/common v0.61.0 // @grafana/alerting-backend + github.com/prometheus/common v0.62.0 // @grafana/alerting-backend github.com/prometheus/prometheus v0.301.0 // @grafana/alerting-backend github.com/redis/go-redis/v9 v9.7.0 // @grafana/alerting-backend github.com/robfig/cron/v3 v3.0.1 // @grafana/grafana-backend-group @@ -154,10 +154,10 @@ require ( github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // @grafana/grafana-operator-experience-squad github.com/yudai/gojsondiff v1.0.0 // @grafana/grafana-backend-group go.opentelemetry.io/collector/pdata v1.22.0 // @grafana/grafana-backend-group - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // @grafana/plugins-platform-backend - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 // @grafana/grafana-operator-experience-squad - go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 // @grafana/grafana-backend-group - go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 // @grafana/grafana-backend-group + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // @grafana/plugins-platform-backend + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0 // @grafana/grafana-operator-experience-squad + go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // @grafana/grafana-backend-group + go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel v1.34.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // @grafana/grafana-backend-group @@ -173,14 +173,14 @@ require ( golang.org/x/mod v0.22.0 // indirect; @grafana/grafana-backend-group golang.org/x/net v0.34.0 // @grafana/oss-big-tent @grafana/partner-datasources golang.org/x/oauth2 v0.25.0 // @grafana/identity-access-team - golang.org/x/sync v0.10.0 // @grafana/alerting-backend + golang.org/x/sync v0.11.0 // @grafana/alerting-backend golang.org/x/text v0.21.0 // @grafana/grafana-backend-group golang.org/x/time v0.9.0 // @grafana/grafana-backend-group golang.org/x/tools v0.29.0 // indirect; @grafana/grafana-as-code 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.3 // @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.4 // @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 @@ -322,7 +322,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.3.0 // indirect + github.com/elazarl/goproxy v1.7.0 // 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 @@ -380,7 +380,6 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect - github.com/invopop/yaml v0.3.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.2 // indirect @@ -505,7 +504,7 @@ 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/instrumentation/net/http/otelhttp v0.58.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/proto/otlp v1.5.0 // indirect @@ -540,6 +539,11 @@ require ( sigs.k8s.io/yaml v1.4.0 // indirect ) +require ( + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 // indirect +) + // Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56 diff --git a/go.sum b/go.sum index 766b690104b..b4820083bad 100644 --- a/go.sum +++ b/go.sum @@ -1082,8 +1082,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.3.0 h1:hpDH1r1qJgM3eusz7lP+BiMPnLiWPa6hDjIFF5WVCjE= -github.com/elazarl/goproxy v1.3.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +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/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= @@ -1143,8 +1143,8 @@ github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0 github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= github.com/gchaincl/sqlhooks v1.3.0 h1:yKPXxW9a5CjXaVf2HkQn6wn7TZARvbAOAelr3H8vK2Y= github.com/gchaincl/sqlhooks v1.3.0/go.mod h1:9BypXnereMT0+Ys8WGWHqzgkkOfHIhyeUCqXC24ra34= -github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= -github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30= +github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -1540,8 +1540,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.263.0 h1:y8vo7hUm50Ei7rdeNNivgehHNsOmCjc8wRmBat5yA3w= -github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= +github.com/grafana/grafana-plugin-sdk-go v0.265.0 h1:XshoH8R23Jm9jRreW9R3aOrIVr9vxhCWFyrMe7BFSks= +github.com/grafana/grafana-plugin-sdk-go v0.265.0/go.mod h1:nkN6xI08YcX6CGsgvRA2+19nhXA/ZPuneLMUUElOD80= github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173 h1:uOM89HiWVVOTls0LrD4coHTckb2lA4U0sIJwCYdbhbw= github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d h1:NRVOtiG1aUwOazBj9KM7X2o2shsM6TchqisezzoH1gw= @@ -1640,8 +1640,8 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= -github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= @@ -1715,8 +1715,6 @@ github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmne github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/ionos-cloud/sdk-go/v6 v6.3.0 h1:/lTieTH9Mo/CWm3cTlFLnK10jgxjUGkAqRffGqvPteY= github.com/ionos-cloud/sdk-go/v6 v6.3.0/go.mod h1:SXrO9OGyWjd2rZhAhEpdYN6VUAODzzqRdqA9BCviQtI= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= @@ -1999,6 +1997,10 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= @@ -2135,8 +2137,8 @@ github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= @@ -2443,18 +2445,18 @@ 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/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= +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= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 h1:xwH3QJv6zL4u+gkPUu59NeT1Gyw9nScWT8FQpKLUJJI= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0/go.mod h1:uosvgpqTcTXtcPQORTbEkZNDQTCDOgTz1fe6aLSyqrQ= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0 h1:iQZYNQ7WwIcYXzOPR46FQv9O0dS1PW16RjvR0TjDOe8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0/go.mod h1:54CaSNqYEXvpzDh8KPjiMVoWm60t5R0dZRt0leEPgAs= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 h1:Jok/dG8kfp+yod29XKYV/blWgYPlMuRUoRHljrXMF5E= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0/go.mod h1:ku/EpGk44S5lyVMbtJRK2KFOnXEehxf6SDnhu1eZmjA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 h1:Co7WDtZosbvNcG4Nqbs3AEVuHNsN6EMc1/1uGKAvyJk= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0/go.mod h1:IohbtCIY5Erb6wKnDddXOMNlG7GwyZnkrgcqjPmhpaA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 h1:Xx1N6cDr8iWy1Cz6OcY7oS0ACdt/6HDYTdu4KskuC7s= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8= go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= @@ -2754,8 +2756,8 @@ golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -3334,8 +3336,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.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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 ff30236f289..d49e9e2b217 100644 --- a/go.work.sum +++ b/go.work.sum @@ -2693,6 +2693,7 @@ google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWn google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index 463fc7f4d61..e60db59ff74 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -1,10 +1,12 @@ module github.com/grafana/grafana/pkg/aggregator -go 1.23.1 +go 1.23.5 + +toolchain go1.23.6 require ( github.com/emicklei/go-restful/v3 v3.11.0 - github.com/grafana/grafana-plugin-sdk-go v0.263.0 + github.com/grafana/grafana-plugin-sdk-go v0.265.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 @@ -37,13 +39,13 @@ 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.3.0 // indirect + github.com/elazarl/goproxy v1.7.0 // 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 github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/getkin/kin-openapi v0.128.0 // indirect + github.com/getkin/kin-openapi v0.129.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -71,10 +73,9 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect @@ -91,6 +92,8 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 // indirect github.com/oklog/run v1.1.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect @@ -99,7 +102,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -118,11 +121,11 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect go.etcd.io/etcd/client/v3 v3.5.16 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect @@ -136,7 +139,7 @@ require ( golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect @@ -147,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.3 // indirect + google.golang.org/protobuf v1.36.4 // 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 85def8afafb..05728e114af 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.3.0 h1:hpDH1r1qJgM3eusz7lP+BiMPnLiWPa6hDjIFF5WVCjE= -github.com/elazarl/goproxy v1.3.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +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/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= @@ -69,8 +69,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= -github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30= +github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -134,8 +134,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.263.0 h1:y8vo7hUm50Ei7rdeNNivgehHNsOmCjc8wRmBat5yA3w= -github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= +github.com/grafana/grafana-plugin-sdk-go v0.265.0 h1:XshoH8R23Jm9jRreW9R3aOrIVr9vxhCWFyrMe7BFSks= +github.com/grafana/grafana-plugin-sdk-go v0.265.0/go.mod h1:nkN6xI08YcX6CGsgvRA2+19nhXA/ZPuneLMUUElOD80= 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= @@ -158,14 +158,12 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3Ar github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= -github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= @@ -233,6 +231,10 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -261,8 +263,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= @@ -351,16 +353,16 @@ go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 h1:xwH3QJv6zL4u+gkPUu59NeT1Gyw9nScWT8FQpKLUJJI= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0/go.mod h1:uosvgpqTcTXtcPQORTbEkZNDQTCDOgTz1fe6aLSyqrQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 h1:Jok/dG8kfp+yod29XKYV/blWgYPlMuRUoRHljrXMF5E= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0/go.mod h1:ku/EpGk44S5lyVMbtJRK2KFOnXEehxf6SDnhu1eZmjA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 h1:Co7WDtZosbvNcG4Nqbs3AEVuHNsN6EMc1/1uGKAvyJk= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0/go.mod h1:IohbtCIY5Erb6wKnDddXOMNlG7GwyZnkrgcqjPmhpaA= +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.59.0 h1:iQZYNQ7WwIcYXzOPR46FQv9O0dS1PW16RjvR0TjDOe8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0/go.mod h1:54CaSNqYEXvpzDh8KPjiMVoWm60t5R0dZRt0leEPgAs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 h1:Xx1N6cDr8iWy1Cz6OcY7oS0ACdt/6HDYTdu4KskuC7s= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= @@ -426,8 +428,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -493,8 +495,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.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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 000e0ebefc4..f74e3ba70a4 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -38,12 +38,12 @@ require ( go.opentelemetry.io/otel/trace v1.34.0 // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.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.3 // indirect + google.golang.org/protobuf v1.36.4 // 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 2af8fd224ce..20fc926c23f 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -110,8 +110,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -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.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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 ef79d050e07..dfa02fe3c7c 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -8,7 +8,7 @@ require ( github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 github.com/prometheus/client_golang v1.20.5 github.com/stretchr/testify v1.10.0 - go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 + go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 go.opentelemetry.io/otel v1.34.0 go.opentelemetry.io/otel/trace v1.34.0 k8s.io/apimachinery v0.32.1 @@ -59,7 +59,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -69,8 +69,8 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect go.etcd.io/etcd/client/v3 v3.5.16 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect @@ -89,7 +89,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.3 // indirect + google.golang.org/protobuf v1.36.4 // 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 141f6dc717f..599ae15a527 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -143,8 +143,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= @@ -196,12 +196,12 @@ go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 h1:Jok/dG8kfp+yod29XKYV/blWgYPlMuRUoRHljrXMF5E= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0/go.mod h1:ku/EpGk44S5lyVMbtJRK2KFOnXEehxf6SDnhu1eZmjA= +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/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= @@ -310,8 +310,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.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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 00a85c4814e..a7192113337 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -23,7 +23,7 @@ require ( github.com/stretchr/testify v1.10.0 // @grafana/grafana-backend-group github.com/urfave/cli v1.22.16 // @grafana/grafana-backend-group github.com/urfave/cli/v2 v2.27.1 // @grafana/grafana-backend-group - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect; @grafana/plugins-platform-backend + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect; @grafana/plugins-platform-backend go.opentelemetry.io/otel v1.34.0 // indirect; @grafana/grafana-backend-group go.opentelemetry.io/otel/sdk v1.34.0 // indirect; @grafana/grafana-backend-group go.opentelemetry.io/otel/trace v1.34.0 // indirect; @grafana/grafana-backend-group @@ -31,12 +31,12 @@ require ( golang.org/x/mod v0.22.0 // @grafana/grafana-backend-group golang.org/x/net v0.34.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources golang.org/x/oauth2 v0.25.0 // @grafana/identity-access-team - golang.org/x/sync v0.10.0 // indirect; @grafana/alerting-backend + golang.org/x/sync v0.11.0 // indirect; @grafana/alerting-backend golang.org/x/text v0.21.0 // indirect; @grafana/grafana-backend-group 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.3 // indirect; @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.4 // indirect; @grafana/plugins-platform-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend ) @@ -72,7 +72,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect golang.org/x/sys v0.29.0 // indirect diff --git a/pkg/build/go.sum b/pkg/build/go.sum index ac9820f6625..3aafd25e523 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -231,10 +231,10 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +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/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 h1:oM0GTNKGlc5qHctWeIGTVyda4iFFalOzMZ3Ehj5rwB4= @@ -300,8 +300,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -364,8 +364,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.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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 689df1e2b23..ee43dc13181 100644 --- a/pkg/build/wire/go.mod +++ b/pkg/build/wire/go.mod @@ -11,5 +11,5 @@ require ( require ( golang.org/x/mod v0.22.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.11.0 // indirect ) diff --git a/pkg/build/wire/go.sum b/pkg/build/wire/go.sum index 08b1c35c90e..56cfeb71f60 100644 --- a/pkg/build/wire/go.sum +++ b/pkg/build/wire/go.sum @@ -6,7 +6,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index 46391ed0872..abb8a0fcbf1 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -17,7 +17,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/proto v1.13.2 // indirect github.com/expr-lang/expr v1.16.9 // indirect - github.com/getkin/kin-openapi v0.128.0 // indirect + 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/golang/glog v1.2.3 // indirect @@ -26,7 +26,6 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect - github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.9 // indirect @@ -34,6 +33,8 @@ require ( github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d // indirect @@ -44,7 +45,7 @@ require ( github.com/yalue/merged_fs v1.3.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.29.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/codegen/go.sum b/pkg/codegen/go.sum index 1a4c60c5a84..ef8c7f75b3d 100644 --- a/pkg/codegen/go.sum +++ b/pkg/codegen/go.sum @@ -13,8 +13,8 @@ github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= -github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= -github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30= +github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= @@ -44,8 +44,6 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= 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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -69,6 +67,10 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto= github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -100,8 +102,8 @@ golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index 8286b7e04a0..1aa5a648860 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -18,7 +18,7 @@ require ( github.com/dave/dst v0.27.3 // indirect github.com/emicklei/proto v1.13.2 // indirect github.com/expr-lang/expr v1.16.9 // indirect - github.com/getkin/kin-openapi v0.128.0 // indirect + 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 @@ -26,12 +26,13 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect - github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect @@ -44,7 +45,7 @@ require ( golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.29.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/plugins/codegen/go.sum b/pkg/plugins/codegen/go.sum index 40d59e1bad8..d8efc875fc9 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -14,8 +14,8 @@ github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= -github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= -github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30= +github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= @@ -41,8 +41,6 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -61,6 +59,10 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -96,8 +98,8 @@ golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 5f4803c615e..287d0d33ca6 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -1,13 +1,15 @@ module github.com/grafana/grafana/pkg/promlib -go 1.23.1 +go 1.23.5 + +toolchain go1.23.6 require ( github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 - github.com/grafana/grafana-plugin-sdk-go v0.263.0 + github.com/grafana/grafana-plugin-sdk-go v0.265.0 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.20.5 - github.com/prometheus/common v0.61.0 + github.com/prometheus/common v0.62.0 github.com/prometheus/prometheus v0.301.0 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.34.0 @@ -31,11 +33,11 @@ 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.3.0 // indirect + github.com/elazarl/goproxy v1.7.0 // 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 - github.com/getkin/kin-openapi v0.128.0 // indirect + github.com/getkin/kin-openapi v0.129.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -57,10 +59,9 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/invopop/jsonschema v0.13.0 // indirect - github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect github.com/klauspost/compress v1.17.11 // indirect @@ -75,6 +76,8 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 // indirect github.com/oklog/run v1.1.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect @@ -94,10 +97,10 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect @@ -107,8 +110,7 @@ require ( golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.29.0 // indirect @@ -117,7 +119,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.3 // indirect + google.golang.org/protobuf v1.36.4 // 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 9b6453c1080..315ec4e7f57 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.3.0 h1:hpDH1r1qJgM3eusz7lP+BiMPnLiWPa6hDjIFF5WVCjE= -github.com/elazarl/goproxy v1.3.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +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/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= @@ -69,8 +69,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= -github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30= +github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= @@ -120,8 +120,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.263.0 h1:y8vo7hUm50Ei7rdeNNivgehHNsOmCjc8wRmBat5yA3w= -github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= +github.com/grafana/grafana-plugin-sdk-go v0.265.0 h1:XshoH8R23Jm9jRreW9R3aOrIVr9vxhCWFyrMe7BFSks= +github.com/grafana/grafana-plugin-sdk-go v0.265.0/go.mod h1:nkN6xI08YcX6CGsgvRA2+19nhXA/ZPuneLMUUElOD80= 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= @@ -136,14 +136,12 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3Ar github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= -github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -208,6 +206,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -229,8 +231,8 @@ github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+ github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/prometheus v0.301.0 h1:0z8dgegmILivNomCd79RKvVkIols8vBGPKmcIBc7OyY= @@ -290,16 +292,16 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 h1:xwH3QJv6zL4u+gkPUu59NeT1Gyw9nScWT8FQpKLUJJI= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0/go.mod h1:uosvgpqTcTXtcPQORTbEkZNDQTCDOgTz1fe6aLSyqrQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 h1:Jok/dG8kfp+yod29XKYV/blWgYPlMuRUoRHljrXMF5E= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0/go.mod h1:ku/EpGk44S5lyVMbtJRK2KFOnXEehxf6SDnhu1eZmjA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 h1:Co7WDtZosbvNcG4Nqbs3AEVuHNsN6EMc1/1uGKAvyJk= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0/go.mod h1:IohbtCIY5Erb6wKnDddXOMNlG7GwyZnkrgcqjPmhpaA= +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.59.0 h1:iQZYNQ7WwIcYXzOPR46FQv9O0dS1PW16RjvR0TjDOe8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0/go.mod h1:54CaSNqYEXvpzDh8KPjiMVoWm60t5R0dZRt0leEPgAs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 h1:Xx1N6cDr8iWy1Cz6OcY7oS0ACdt/6HDYTdu4KskuC7s= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= @@ -347,8 +349,8 @@ golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -392,8 +394,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.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index dea2ddfac2c..f5e31c31b38 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -123,7 +123,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.3.0 // indirect + github.com/elazarl/goproxy v1.7.0 // 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 @@ -133,7 +133,7 @@ require ( github.com/fullstorydev/grpchan v1.1.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gchaincl/sqlhooks v1.3.0 // indirect - github.com/getkin/kin-openapi v0.128.0 // indirect + github.com/getkin/kin-openapi v0.129.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect @@ -182,7 +182,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.263.0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.265.0 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/grafana/sqlds/v4 v4.1.3 // indirect @@ -196,7 +196,7 @@ require ( github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-msgpack v1.1.5 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/go-sockaddr v1.0.6 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -205,7 +205,6 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/invopop/yaml v0.3.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.2 // indirect @@ -256,6 +255,8 @@ require ( github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/natefinch/wrap v0.2.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 // indirect github.com/oklog/run v1.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid/v2 v2.1.0 // indirect @@ -276,7 +277,7 @@ require ( github.com/prometheus/alertmanager v0.27.0 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.13.2 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -319,11 +320,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/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect @@ -340,7 +341,7 @@ require ( golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect @@ -352,7 +353,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.3 // indirect + google.golang.org/protobuf v1.36.4 // 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 7edba45515c..2820fd08a9b 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -328,8 +328,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.3.0 h1:hpDH1r1qJgM3eusz7lP+BiMPnLiWPa6hDjIFF5WVCjE= -github.com/elazarl/goproxy v1.3.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +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/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= @@ -358,8 +358,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gchaincl/sqlhooks v1.3.0 h1:yKPXxW9a5CjXaVf2HkQn6wn7TZARvbAOAelr3H8vK2Y= github.com/gchaincl/sqlhooks v1.3.0/go.mod h1:9BypXnereMT0+Ys8WGWHqzgkkOfHIhyeUCqXC24ra34= -github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= -github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30= +github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -575,8 +575,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.263.0 h1:y8vo7hUm50Ei7rdeNNivgehHNsOmCjc8wRmBat5yA3w= -github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= +github.com/grafana/grafana-plugin-sdk-go v0.265.0 h1:XshoH8R23Jm9jRreW9R3aOrIVr9vxhCWFyrMe7BFSks= +github.com/grafana/grafana-plugin-sdk-go v0.265.0/go.mod h1:nkN6xI08YcX6CGsgvRA2+19nhXA/ZPuneLMUUElOD80= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d h1:aBD5kzsIAh50vjNqUkWK9mNpLGIBYAnKkWtUepGNAiQ= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d/go.mod h1:1sq0guad+G4SUTlBgx7SXfhnzy7D86K/LcVOtiQCiMA= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= @@ -621,8 +621,8 @@ github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YN github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= -github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= @@ -652,8 +652,6 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -829,6 +827,10 @@ github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk= github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -915,8 +917,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/exporter-toolkit v0.13.2 h1:Z02fYtbqTMy2i/f+xZ+UK5jy/bl1Ex3ndzh06T/Q9DQ= @@ -1091,16 +1093,16 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 h1:xwH3QJv6zL4u+gkPUu59NeT1Gyw9nScWT8FQpKLUJJI= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0/go.mod h1:uosvgpqTcTXtcPQORTbEkZNDQTCDOgTz1fe6aLSyqrQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 h1:Jok/dG8kfp+yod29XKYV/blWgYPlMuRUoRHljrXMF5E= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0/go.mod h1:ku/EpGk44S5lyVMbtJRK2KFOnXEehxf6SDnhu1eZmjA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 h1:Co7WDtZosbvNcG4Nqbs3AEVuHNsN6EMc1/1uGKAvyJk= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0/go.mod h1:IohbtCIY5Erb6wKnDddXOMNlG7GwyZnkrgcqjPmhpaA= +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.59.0 h1:iQZYNQ7WwIcYXzOPR46FQv9O0dS1PW16RjvR0TjDOe8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0/go.mod h1:54CaSNqYEXvpzDh8KPjiMVoWm60t5R0dZRt0leEPgAs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 h1:Xx1N6cDr8iWy1Cz6OcY7oS0ACdt/6HDYTdu4KskuC7s= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= @@ -1266,8 +1268,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1527,8 +1529,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.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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 8de932a3388..b5616a63c8c 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-20250120145936-5f0e28e7a87c 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.263.0 + github.com/grafana/grafana-plugin-sdk-go v0.265.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 @@ -25,9 +25,9 @@ require ( go.opentelemetry.io/otel v1.34.0 go.opentelemetry.io/otel/trace v1.34.0 gocloud.dev v0.40.0 - golang.org/x/sync v0.10.0 + golang.org/x/sync v0.11.0 google.golang.org/grpc v1.70.0 - google.golang.org/protobuf v1.36.3 + google.golang.org/protobuf v1.36.4 k8s.io/apimachinery v0.32.1 ) @@ -81,12 +81,12 @@ 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.3.0 // indirect + github.com/elazarl/goproxy v1.7.0 // 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 github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/getkin/kin-openapi v0.128.0 // indirect + github.com/getkin/kin-openapi v0.129.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect @@ -131,13 +131,12 @@ require ( github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-msgpack v1.1.5 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/go-sockaddr v1.0.6 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/memberlist v0.5.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/invopop/yaml v0.3.1 // indirect github.com/jhump/protoreflect v1.15.1 // indirect github.com/jmespath-community/go-jmespath v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -171,6 +170,8 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 // indirect github.com/oklog/run v1.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect @@ -183,7 +184,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/alertmanager v0.27.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/exporter-toolkit v0.13.2 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -204,11 +205,11 @@ require ( github.com/zeebo/xxh3 v1.0.2 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index e2b728b06d0..8d74f2a39db 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -226,8 +226,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.3.0 h1:hpDH1r1qJgM3eusz7lP+BiMPnLiWPa6hDjIFF5WVCjE= -github.com/elazarl/goproxy v1.3.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +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/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= @@ -254,8 +254,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gchaincl/sqlhooks v1.3.0 h1:yKPXxW9a5CjXaVf2HkQn6wn7TZARvbAOAelr3H8vK2Y= github.com/gchaincl/sqlhooks v1.3.0/go.mod h1:9BypXnereMT0+Ys8WGWHqzgkkOfHIhyeUCqXC24ra34= -github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= -github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30= +github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= @@ -429,8 +429,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.263.0 h1:y8vo7hUm50Ei7rdeNNivgehHNsOmCjc8wRmBat5yA3w= -github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= +github.com/grafana/grafana-plugin-sdk-go v0.265.0 h1:XshoH8R23Jm9jRreW9R3aOrIVr9vxhCWFyrMe7BFSks= +github.com/grafana/grafana-plugin-sdk-go v0.265.0/go.mod h1:nkN6xI08YcX6CGsgvRA2+19nhXA/ZPuneLMUUElOD80= 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= @@ -461,8 +461,8 @@ github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YN github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= -github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I= @@ -487,8 +487,6 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -645,6 +643,10 @@ github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk= github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -710,8 +712,8 @@ github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQy github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/exporter-toolkit v0.13.2 h1:Z02fYtbqTMy2i/f+xZ+UK5jy/bl1Ex3ndzh06T/Q9DQ= @@ -846,16 +848,16 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0 h1:xwH3QJv6zL4u+gkPUu59NeT1Gyw9nScWT8FQpKLUJJI= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0/go.mod h1:uosvgpqTcTXtcPQORTbEkZNDQTCDOgTz1fe6aLSyqrQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 h1:Jok/dG8kfp+yod29XKYV/blWgYPlMuRUoRHljrXMF5E= -go.opentelemetry.io/contrib/propagators/jaeger v1.33.0/go.mod h1:ku/EpGk44S5lyVMbtJRK2KFOnXEehxf6SDnhu1eZmjA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0 h1:Co7WDtZosbvNcG4Nqbs3AEVuHNsN6EMc1/1uGKAvyJk= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.27.0/go.mod h1:IohbtCIY5Erb6wKnDddXOMNlG7GwyZnkrgcqjPmhpaA= +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.59.0 h1:iQZYNQ7WwIcYXzOPR46FQv9O0dS1PW16RjvR0TjDOe8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0/go.mod h1:54CaSNqYEXvpzDh8KPjiMVoWm60t5R0dZRt0leEPgAs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= +go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0 h1:Xx1N6cDr8iWy1Cz6OcY7oS0ACdt/6HDYTdu4KskuC7s= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.28.0/go.mod h1:iWS+NvC948FyfnJbVfPN9h/8+vr8CR2FPn6XsLRkvH8= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= @@ -962,8 +964,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1098,8 +1100,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.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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 7c8fddc7291f181b83fee6e6a7be4f1190a38229 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 10 Feb 2025 14:07:04 +0300 Subject: [PATCH 452/894] Chore: remove CVE from code scanning tool (#100312) --- yarn.lock | 128 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 47 deletions(-) diff --git a/yarn.lock b/yarn.lock index 077f50edb1a..d3a579b5ee4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1691,8 +1691,8 @@ __metadata: linkType: hard "@cypress/request@npm:^3.0.0": - version: 3.0.1 - resolution: "@cypress/request@npm:3.0.1" + version: 3.0.7 + resolution: "@cypress/request@npm:3.0.7" dependencies: aws-sign2: "npm:~0.7.0" aws4: "npm:^1.8.0" @@ -1700,19 +1700,19 @@ __metadata: combined-stream: "npm:~1.0.6" extend: "npm:~3.0.2" forever-agent: "npm:~0.6.1" - form-data: "npm:~2.3.2" - http-signature: "npm:~1.3.6" + form-data: "npm:~4.0.0" + http-signature: "npm:~1.4.0" is-typedarray: "npm:~1.0.0" isstream: "npm:~0.1.2" json-stringify-safe: "npm:~5.0.1" mime-types: "npm:~2.1.19" performance-now: "npm:^2.1.0" - qs: "npm:6.10.4" + qs: "npm:6.13.1" safe-buffer: "npm:^5.1.2" - tough-cookie: "npm:^4.1.3" + tough-cookie: "npm:^5.0.0" tunnel-agent: "npm:^0.6.0" uuid: "npm:^8.3.2" - checksum: 10/bf48bed6d6e493c05493902fb08b1d0646e7ec4300cf834816c2616f781db1a7fc447bd6f81de7c3076d738e8a6d75354e21d332f8f7ef8d9101d9b2f8e15b3a + checksum: 10/fdd674caaa0942c8bb9bc90d862932dfccae6a7d63bacb13850b11668274c382356f5649d9264948015727b2362012b3c0c5105a67e107196d8b8c3b3d673fec languageName: node linkType: hard @@ -11348,7 +11348,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2": +"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2": version: 8.14.0 resolution: "acorn@npm:8.14.0" bin: @@ -13389,7 +13389,7 @@ __metadata: languageName: node linkType: hard -"combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8, combined-stream@npm:~1.0.6": +"combined-stream@npm:^1.0.8, combined-stream@npm:~1.0.6": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" dependencies: @@ -14971,13 +14971,20 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:10, decimal.js@npm:^10.4.1": +"decimal.js@npm:10": version: 10.4.3 resolution: "decimal.js@npm:10.4.3" checksum: 10/de663a7bc4d368e3877db95fcd5c87b965569b58d16cdc4258c063d231ca7118748738df17cd638f7e9dd0be8e34cec08d7234b20f1f2a756a52fc5a38b188d0 languageName: node linkType: hard +"decimal.js@npm:^10.4.2": + version: 10.5.0 + resolution: "decimal.js@npm:10.5.0" + checksum: 10/714d49cf2f2207b268221795ede330e51452b7c451a0c02a770837d2d4faed47d603a729c2aa1d952eb6c4102d999e91c9b952c1aa016db3c5cba9fc8bf4cda2 + languageName: node + linkType: hard + "decode-uri-component@npm:^0.2.0": version: 0.2.2 resolution: "decode-uri-component@npm:0.2.2" @@ -17408,14 +17415,14 @@ __metadata: languageName: node linkType: hard -"form-data@npm:~2.3.2": - version: 2.3.3 - resolution: "form-data@npm:2.3.3" +"form-data@npm:~4.0.0": + version: 4.0.1 + resolution: "form-data@npm:4.0.1" dependencies: asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.6" + combined-stream: "npm:^1.0.8" mime-types: "npm:^2.1.12" - checksum: 10/1b6f3ccbf4540e535887b42218a2431a3f6cfdea320119c2affa2a7a374ad8fdd1e60166fc865181f45d49b1684c3e90e7b2190d3fe016692957afb9cf0d0d02 + checksum: 10/6adb1cff557328bc6eb8a68da205f9ae44ab0e88d4d9237aaf91eed591ffc64f77411efb9016af7d87f23d0a038c45a788aa1c6634e51175c4efa36c2bc53774 languageName: node linkType: hard @@ -19110,14 +19117,14 @@ __metadata: languageName: node linkType: hard -"http-signature@npm:~1.3.6": - version: 1.3.6 - resolution: "http-signature@npm:1.3.6" +"http-signature@npm:~1.4.0": + version: 1.4.0 + resolution: "http-signature@npm:1.4.0" dependencies: assert-plus: "npm:^1.0.0" jsprim: "npm:^2.0.2" - sshpk: "npm:^1.14.1" - checksum: 10/5f08e0c82174999da97114facb0d0d47e268d60b6fc10f92cb87b99d5ccccd36f79b9508c29dda0b4f4e3a1b2f7bcaf847e68ecd5da2f1fc465fcd1d054b7884 + sshpk: "npm:^1.18.0" + checksum: 10/f9f5eed4ac5db5e1ec6d00652680c7d8b76d553560017e34505c0c22c37abb2e6d22b9268ed4a8542aa9746852a2d64850531091e443393c9c8e0f4fd4174455 languageName: node linkType: hard @@ -21052,16 +21059,16 @@ __metadata: linkType: hard "jsdom@npm:^20.0.0": - version: 20.0.2 - resolution: "jsdom@npm:20.0.2" + version: 20.0.3 + resolution: "jsdom@npm:20.0.3" dependencies: abab: "npm:^2.0.6" - acorn: "npm:^8.8.0" + acorn: "npm:^8.8.1" acorn-globals: "npm:^7.0.0" cssom: "npm:^0.5.0" cssstyle: "npm:^2.3.0" data-urls: "npm:^3.0.2" - decimal.js: "npm:^10.4.1" + decimal.js: "npm:^10.4.2" domexception: "npm:^4.0.0" escodegen: "npm:^2.0.0" form-data: "npm:^4.0.0" @@ -21074,19 +21081,19 @@ __metadata: saxes: "npm:^6.0.0" symbol-tree: "npm:^3.2.4" tough-cookie: "npm:^4.1.2" - w3c-xmlserializer: "npm:^3.0.0" + w3c-xmlserializer: "npm:^4.0.0" webidl-conversions: "npm:^7.0.0" whatwg-encoding: "npm:^2.0.0" whatwg-mimetype: "npm:^3.0.0" whatwg-url: "npm:^11.0.0" - ws: "npm:^8.9.0" + ws: "npm:^8.11.0" xml-name-validator: "npm:^4.0.0" peerDependencies: canvas: ^2.5.0 peerDependenciesMeta: canvas: optional: true - checksum: 10/3bd8d4ee84a1b8ba4882aee9ec15b4b72576b9e4be43462a3301a10fad020a7ee1a59cfe5e270ae49311c162920f65ebac766a105e1dfe09b5533a671b5e2336 + checksum: 10/a4cdcff5b07eed87da90b146b82936321533b5efe8124492acf7160ebd5b9cf2b3c2435683592bf1cffb479615245756efb6c173effc1906f845a86ed22af985 languageName: node linkType: hard @@ -25598,15 +25605,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.10.4": - version: 6.10.4 - resolution: "qs@npm:6.10.4" - dependencies: - side-channel: "npm:^1.0.4" - checksum: 10/8887a53f63180e0e0291deafef581e550bc3656f2453adc8d3ca34b49c04354d31079962f7faf90ab8f5fd6e3d70ee6645042b27814a757a3a5d5708ae3f58e0 - languageName: node - linkType: hard - "qs@npm:6.13.0, qs@npm:^6.11.2, qs@npm:^6.4.0": version: 6.13.0 resolution: "qs@npm:6.13.0" @@ -25616,6 +25614,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:6.13.1": + version: 6.13.1 + resolution: "qs@npm:6.13.1" + dependencies: + side-channel: "npm:^1.0.6" + checksum: 10/53cf5fdc5f342a9ffd3968f20c8c61624924cf928d86fff525240620faba8ca5cfd6c3f12718cc755561bfc3dc9721bc8924e38f53d8925b03940f0b8a902212 + languageName: node + linkType: hard + "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -28684,7 +28691,7 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.0.4, side-channel@npm:^1.0.6, side-channel@npm:^1.1.0": +"side-channel@npm:^1.0.6, side-channel@npm:^1.1.0": version: 1.1.0 resolution: "side-channel@npm:1.1.0" dependencies: @@ -29311,9 +29318,9 @@ __metadata: languageName: node linkType: hard -"sshpk@npm:^1.14.1": - version: 1.17.0 - resolution: "sshpk@npm:1.17.0" +"sshpk@npm:^1.18.0": + version: 1.18.0 + resolution: "sshpk@npm:1.18.0" dependencies: asn1: "npm:~0.2.3" assert-plus: "npm:^1.0.0" @@ -29328,7 +29335,7 @@ __metadata: sshpk-conv: bin/sshpk-conv sshpk-sign: bin/sshpk-sign sshpk-verify: bin/sshpk-verify - checksum: 10/668c2a279a6ce66fd739ce5684e37927dd75427cc020c828a208f85890a4c400705d4ba09f32fa44efca894339dc6931941664f6f6ba36dfa543de6d006cbe9c + checksum: 10/858339d43e3c6b6a848772a66f69442ce74f1a37655d9f35ba9d1f85329499ff0000af9f8ab83dbb39ad24c0c370edabe0be1e39863f70c6cded9924b8458c34 languageName: node linkType: hard @@ -30398,6 +30405,24 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^6.1.76": + version: 6.1.76 + resolution: "tldts-core@npm:6.1.76" + checksum: 10/eebb67d4efba10982b9d4ae2f2edaccf79c6f3354e21088446edaa06eab804c15eda662680892b5463df801adda4fe54fe02773be55b8b54a4377007be7bab01 + languageName: node + linkType: hard + +"tldts@npm:^6.1.32": + version: 6.1.76 + resolution: "tldts@npm:6.1.76" + dependencies: + tldts-core: "npm:^6.1.76" + bin: + tldts: bin/cli.js + checksum: 10/eeca7529fc4c1f4af08582e7b61d7a517bd2c2f9f12b295154f5dddbf87f7cb96085df6e8f18f15ca0fac579b27f9a73212e61d0b1b911b941fabe5cf9f9f4cd + languageName: node + linkType: hard + "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" @@ -30497,7 +30522,7 @@ __metadata: languageName: node linkType: hard -"tough-cookie@npm:^4.1.2, tough-cookie@npm:^4.1.3, tough-cookie@npm:^4.1.4": +"tough-cookie@npm:^4.1.2, tough-cookie@npm:^4.1.4": version: 4.1.4 resolution: "tough-cookie@npm:4.1.4" dependencies: @@ -30509,6 +30534,15 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^5.0.0": + version: 5.1.1 + resolution: "tough-cookie@npm:5.1.1" + dependencies: + tldts: "npm:^6.1.32" + checksum: 10/6cb1e38216ce579406ecb1790cfa208754995b2cb48a8a787e0a1d7b0750300020a541fd5df5c497bc5a2db895b618151c416f9a584c6f725a56655c66910ab8 + languageName: node + linkType: hard + "tr46@npm:^3.0.0": version: 3.0.0 resolution: "tr46@npm:3.0.0" @@ -31564,12 +31598,12 @@ __metadata: languageName: node linkType: hard -"w3c-xmlserializer@npm:^3.0.0": - version: 3.0.0 - resolution: "w3c-xmlserializer@npm:3.0.0" +"w3c-xmlserializer@npm:^4.0.0": + version: 4.0.0 + resolution: "w3c-xmlserializer@npm:4.0.0" dependencies: xml-name-validator: "npm:^4.0.0" - checksum: 10/b4d73e20be283cc9975573a88979d15c08daa9c00911f8c777ef2af74eea11ba635fec18647ff0374ce880ec32ae573d17bd0f787053fc3085a530345b2feab6 + checksum: 10/9a00c412b5496f4f040842c9520bc0aaec6e0c015d06412a91a723cd7d84ea605ab903965f546b4ecdb3eae267f5145ba08565222b1d6cb443ee488cda9a0aee languageName: node linkType: hard @@ -32326,7 +32360,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.18.0, ws@npm:^8.2.3, ws@npm:^8.9.0": +"ws@npm:^8.11.0, ws@npm:^8.18.0, ws@npm:^8.2.3": version: 8.18.0 resolution: "ws@npm:8.18.0" peerDependencies: From 2518012569bf6d3a46fd69df6cb89c420b16b202 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Mon, 10 Feb 2025 13:46:46 +0200 Subject: [PATCH 453/894] grafana-ui: Update InlineField error prop type to React.ReactNode (#100347) --- packages/grafana-ui/src/components/Forms/InlineField.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/Forms/InlineField.tsx b/packages/grafana-ui/src/components/Forms/InlineField.tsx index 2eb4b5c1f25..ec63fc93c3f 100644 --- a/packages/grafana-ui/src/components/Forms/InlineField.tsx +++ b/packages/grafana-ui/src/components/Forms/InlineField.tsx @@ -1,5 +1,5 @@ import { cx, css } from '@emotion/css'; -import { cloneElement } from 'react'; +import { cloneElement, ReactNode } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; @@ -23,7 +23,7 @@ export interface Props extends Omit Date: Mon, 10 Feb 2025 13:15:41 +0100 Subject: [PATCH 454/894] Advisor: Create checks following a schedule (#100282) --- apps/advisor/pkg/app/app.go | 13 +- apps/advisor/pkg/app/checks/utils.go | 5 + .../pkg/app/checkscheduler/checkscheduler.go | 129 +++++++++++++++++ .../app/checkscheduler/checkscheduler_test.go | 131 ++++++++++++++++++ apps/advisor/pkg/app/utils.go | 12 +- apps/advisor/pkg/app/utils_test.go | 14 +- 6 files changed, 286 insertions(+), 18 deletions(-) create mode 100644 apps/advisor/pkg/app/checkscheduler/checkscheduler.go create mode 100644 apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go diff --git a/apps/advisor/pkg/app/app.go b/apps/advisor/pkg/app/app.go index 7982661c3b9..f645466c4e1 100644 --- a/apps/advisor/pkg/app/app.go +++ b/apps/advisor/pkg/app/app.go @@ -11,16 +11,12 @@ import ( advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkscheduler" "github.com/grafana/grafana/apps/advisor/pkg/app/checktyperegisterer" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" ) -const ( - typeLabel = "advisor.grafana.app/type" - statusAnnotation = "advisor.grafana.app/status" -) - func New(cfg app.Config) (app.App, error) { // Read config checkRegistry, ok := cfg.SpecificConfig.(checkregistry.CheckService) @@ -94,6 +90,13 @@ func New(cfg app.Config) (app.App, error) { } a.AddRunnable(ctr) + // Start scheduler + csch, err := checkscheduler.New(cfg) + if err != nil { + return nil, err + } + a.AddRunnable(csch) + return a, nil } diff --git a/apps/advisor/pkg/app/checks/utils.go b/apps/advisor/pkg/app/checks/utils.go index ee33d57076b..a02357cf013 100644 --- a/apps/advisor/pkg/app/checks/utils.go +++ b/apps/advisor/pkg/app/checks/utils.go @@ -4,6 +4,11 @@ import ( advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" ) +const ( + TypeLabel = "advisor.grafana.app/type" + StatusAnnotation = "advisor.grafana.app/status" +) + func NewCheckReportFailure( severity advisor.CheckReportFailureSeverity, reason string, diff --git a/apps/advisor/pkg/app/checkscheduler/checkscheduler.go b/apps/advisor/pkg/app/checkscheduler/checkscheduler.go new file mode 100644 index 00000000000..392ec2d792d --- /dev/null +++ b/apps/advisor/pkg/app/checkscheduler/checkscheduler.go @@ -0,0 +1,129 @@ +package checkscheduler + +import ( + "context" + "fmt" + "time" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/k8s" + "github.com/grafana/grafana-app-sdk/resource" + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" +) + +const evaluateChecksInterval = 24 * time.Hour + +// Runner is a "runnable" app used to be able to expose and API endpoint +// with the existing checks types. This does not need to be a CRUD resource, but it is +// the only way existing at the moment to expose the check types. +type Runner struct { + checkRegistry checkregistry.CheckService + client resource.Client +} + +// NewRunner creates a new Runner. +func New(cfg app.Config) (app.Runnable, error) { + // Read config + checkRegistry, ok := cfg.SpecificConfig.(checkregistry.CheckService) + if !ok { + return nil, fmt.Errorf("invalid config type") + } + + // Prepare storage client + clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{}) + client, err := clientGenerator.ClientFor(advisorv0alpha1.CheckKind()) + if err != nil { + return nil, err + } + + return &Runner{ + checkRegistry: checkRegistry, + client: client, + }, nil +} + +func (r *Runner) Run(ctx context.Context) error { + lastCreated, err := r.checkLastCreated(ctx) + if err != nil { + return err + } + + // do an initial creation if necessary + if lastCreated.IsZero() { + err = r.createChecks(ctx) + if err != nil { + klog.Error("Error creating new check reports", "error", err) + } else { + lastCreated = time.Now() + } + } + + nextSendInterval := time.Until(lastCreated.Add(evaluateChecksInterval)) + if nextSendInterval < time.Minute { + nextSendInterval = 1 * time.Minute + } + + ticker := time.NewTicker(nextSendInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + err = r.createChecks(ctx) + if err != nil { + klog.Error("Error creating new check reports", "error", err) + } + + if nextSendInterval != evaluateChecksInterval { + nextSendInterval = evaluateChecksInterval + } + ticker.Reset(nextSendInterval) + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// checkLastCreated returns the creation time of the last check created +// regardless of its ID. This assumes that the checks are created in batches +// so a batch will have a similar creation time. +func (r *Runner) checkLastCreated(ctx context.Context) (time.Time, error) { + list, err := r.client.List(ctx, metav1.NamespaceDefault, resource.ListOptions{}) + if err != nil { + return time.Time{}, err + } + lastCreated := time.Time{} + for _, item := range list.GetItems() { + itemCreated := item.GetCreationTimestamp().Time + if itemCreated.After(lastCreated) { + lastCreated = itemCreated + } + } + return lastCreated, nil +} + +// createChecks creates a new check for each check type in the registry. +func (r *Runner) createChecks(ctx context.Context) error { + for _, check := range r.checkRegistry.Checks() { + obj := &advisorv0alpha1.Check{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "check-", + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{ + checks.TypeLabel: check.ID(), + }, + }, + Spec: advisorv0alpha1.CheckSpec{}, + } + id := obj.GetStaticMetadata().Identifier() + _, err := r.client.Create(ctx, id, obj, resource.CreateOptions{}) + if err != nil { + return fmt.Errorf("error creating check: %w", err) + } + } + return nil +} diff --git a/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go b/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go new file mode 100644 index 00000000000..8b1c7fb47e5 --- /dev/null +++ b/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go @@ -0,0 +1,131 @@ +package checkscheduler + +import ( + "context" + "errors" + "testing" + + "github.com/grafana/grafana-app-sdk/resource" + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/stretchr/testify/assert" +) + +type MockCheckService struct { + checks []checks.Check +} + +func (m *MockCheckService) Checks() []checks.Check { + return m.checks +} + +type MockClient struct { + resource.Client + listFunc func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) + createFunc func(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) +} + +func (m *MockClient) List(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + return m.listFunc(ctx, namespace, options) +} + +func (m *MockClient) Create(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) { + return m.createFunc(ctx, identifier, obj, options) +} + +type mockCheck struct { + checks.Check + + id string + steps []checks.Step +} + +func (m *mockCheck) ID() string { + return m.id +} + +func (m *mockCheck) Steps() []checks.Step { + return m.steps +} + +func TestRunner_Run_ErrorOnList(t *testing.T) { + mockCheckService := &MockCheckService{} + mockClient := &MockClient{ + listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + return nil, errors.New("list error") + }, + createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { + return &advisorv0alpha1.Check{}, nil + }, + } + + runner := &Runner{ + checkRegistry: mockCheckService, + client: mockClient, + } + + err := runner.Run(context.Background()) + assert.Error(t, err) +} + +func TestRunner_checkLastCreated_ErrorOnList(t *testing.T) { + mockClient := &MockClient{ + listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + return nil, errors.New("list error") + }, + } + + runner := &Runner{ + client: mockClient, + } + + lastCreated, err := runner.checkLastCreated(context.Background()) + assert.Error(t, err) + assert.True(t, lastCreated.IsZero()) +} + +func TestRunner_createChecks_ErrorOnCreate(t *testing.T) { + mockCheckService := &MockCheckService{ + checks: []checks.Check{ + &mockCheck{ + id: "check-1", + }, + }, + } + mockClient := &MockClient{ + createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { + return nil, errors.New("create error") + }, + } + + runner := &Runner{ + checkRegistry: mockCheckService, + client: mockClient, + } + + err := runner.createChecks(context.Background()) + assert.Error(t, err) +} + +func TestRunner_createChecks_Success(t *testing.T) { + mockCheckService := &MockCheckService{ + checks: []checks.Check{ + &mockCheck{ + id: "check-1", + }, + }, + } + mockClient := &MockClient{ + createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { + return &advisorv0alpha1.Check{}, nil + }, + } + + runner := &Runner{ + checkRegistry: mockCheckService, + client: mockClient, + } + + err := runner.createChecks(context.Background()) + assert.NoError(t, err) +} diff --git a/apps/advisor/pkg/app/utils.go b/apps/advisor/pkg/app/utils.go index 731d16e8737..8c1b6c00eb8 100644 --- a/apps/advisor/pkg/app/utils.go +++ b/apps/advisor/pkg/app/utils.go @@ -15,16 +15,16 @@ import ( "github.com/grafana/grafana/pkg/services/user" ) -func getCheck(obj resource.Object, checks map[string]checks.Check) (checks.Check, error) { +func getCheck(obj resource.Object, checkMap map[string]checks.Check) (checks.Check, error) { labels := obj.GetLabels() - objTypeLabel, ok := labels[typeLabel] + objTypeLabel, ok := labels[checks.TypeLabel] if !ok { return nil, errors.New("missing check type as label") } - c, ok := checks[objTypeLabel] + c, ok := checkMap[objTypeLabel] if !ok { supportedTypes := "" - for k := range checks { + for k := range checkMap { supportedTypes += k + ", " } return nil, fmt.Errorf("unknown check type %s. Supported types are: %s", objTypeLabel, supportedTypes) @@ -34,12 +34,12 @@ func getCheck(obj resource.Object, checks map[string]checks.Check) (checks.Check } func getStatusAnnotation(obj resource.Object) string { - return obj.GetAnnotations()[statusAnnotation] + return obj.GetAnnotations()[checks.StatusAnnotation] } func setStatusAnnotation(ctx context.Context, client resource.Client, obj resource.Object, status string) error { annotations := obj.GetAnnotations() - annotations[statusAnnotation] = status + annotations[checks.StatusAnnotation] = status return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ Operations: []resource.PatchOperation{{ Operation: resource.PatchOpAdd, diff --git a/apps/advisor/pkg/app/utils_test.go b/apps/advisor/pkg/app/utils_test.go index ada844fdba4..090397333d0 100644 --- a/apps/advisor/pkg/app/utils_test.go +++ b/apps/advisor/pkg/app/utils_test.go @@ -15,7 +15,7 @@ import ( func TestGetCheck(t *testing.T) { obj := &advisorv0alpha1.Check{} - obj.SetLabels(map[string]string{typeLabel: "testType"}) + obj.SetLabels(map[string]string{checks.TypeLabel: "testType"}) checkMap := map[string]checks.Check{ "testType": &mockCheck{}, @@ -37,7 +37,7 @@ func TestGetCheck_MissingLabel(t *testing.T) { func TestGetCheck_UnknownType(t *testing.T) { obj := &advisorv0alpha1.Check{} - obj.SetLabels(map[string]string{typeLabel: "unknownType"}) + obj.SetLabels(map[string]string{checks.TypeLabel: "unknownType"}) checkMap := map[string]checks.Check{ "testType": &mockCheck{}, @@ -56,7 +56,7 @@ func TestSetStatusAnnotation(t *testing.T) { err := setStatusAnnotation(ctx, client, obj, "processed") assert.NoError(t, err) - assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) + assert.Equal(t, "processed", obj.GetAnnotations()[checks.StatusAnnotation]) } func TestProcessCheck(t *testing.T) { @@ -75,7 +75,7 @@ func TestProcessCheck(t *testing.T) { err = processCheck(ctx, client, obj, check) assert.NoError(t, err) - assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) + assert.Equal(t, "processed", obj.GetAnnotations()[checks.StatusAnnotation]) } func TestProcessMultipleCheckItems(t *testing.T) { @@ -102,7 +102,7 @@ func TestProcessMultipleCheckItems(t *testing.T) { err = processCheck(ctx, client, obj, check) assert.NoError(t, err) - assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) + assert.Equal(t, "processed", obj.GetAnnotations()[checks.StatusAnnotation]) r := client.lastValue.(advisorv0alpha1.CheckV0alpha1StatusReport) assert.Equal(t, r.Count, int64(100)) assert.Len(t, r.Failures, 50) @@ -110,7 +110,7 @@ func TestProcessMultipleCheckItems(t *testing.T) { func TestProcessCheck_AlreadyProcessed(t *testing.T) { obj := &advisorv0alpha1.Check{} - obj.SetAnnotations(map[string]string{statusAnnotation: "processed"}) + obj.SetAnnotations(map[string]string{checks.StatusAnnotation: "processed"}) client := &mockClient{} ctx := context.TODO() check := &mockCheck{} @@ -137,7 +137,7 @@ func TestProcessCheck_RunError(t *testing.T) { err = processCheck(ctx, client, obj, check) assert.Error(t, err) - assert.Equal(t, "error", obj.GetAnnotations()[statusAnnotation]) + assert.Equal(t, "error", obj.GetAnnotations()[checks.StatusAnnotation]) } type mockClient struct { From 9697a699f24924a6e7e056e759fc58472035af62 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 10 Feb 2025 12:40:45 +0000 Subject: [PATCH 455/894] Chore: Remove unused knip dependency (#100351) --- package.json | 1 - yarn.lock | 132 ++------------------------------------------------- 2 files changed, 3 insertions(+), 130 deletions(-) diff --git a/package.json b/package.json index 8457c7db5d2..fc88ff26ab6 100644 --- a/package.json +++ b/package.json @@ -208,7 +208,6 @@ "jest-watch-typeahead": "^2.2.2", "jimp": "^1.6.0", "jsdom-testing-mocks": "^1.13.1", - "knip": "^5.10.0", "lerna": "8.1.8", "mini-css-extract-plugin": "2.9.2", "msw": "2.7.0", diff --git a/yarn.lock b/yarn.lock index d3a579b5ee4..de701500eda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5487,7 +5487,7 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.walk@npm:1.2.8, @nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.6": +"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.6": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" dependencies: @@ -7601,19 +7601,6 @@ __metadata: languageName: node linkType: hard -"@snyk/github-codeowners@npm:1.1.0": - version: 1.1.0 - resolution: "@snyk/github-codeowners@npm:1.1.0" - dependencies: - commander: "npm:^4.1.1" - ignore: "npm:^5.1.8" - p-map: "npm:^4.0.0" - bin: - github-codeowners: dist/cli.js - checksum: 10/34120ef622616fef1ed8af12869d8c1803842aafa3fbacca263805ee7c85f58d11bdc301ef698c9b41268b275b9fd090f5d9f6d89c556abe9d52196e72d1c510 - languageName: node - linkType: hard - "@socket.io/component-emitter@npm:~3.1.0": version: 3.1.2 resolution: "@socket.io/component-emitter@npm:3.1.2" @@ -13454,13 +13441,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^4.1.1": - version: 4.1.1 - resolution: "commander@npm:4.1.1" - checksum: 10/3b2dc4125f387dab73b3294dbcb0ab2a862f9c0ad748ee2b27e3544d25325b7a8cdfbcc228d103a98a716960b14478114a5206b5415bd48cdafa38797891562c - languageName: node - linkType: hard - "commander@npm:^6.2.0, commander@npm:^6.2.1": version: 6.2.1 resolution: "commander@npm:6.2.1" @@ -15536,19 +15516,6 @@ __metadata: languageName: node linkType: hard -"easy-table@npm:1.2.0": - version: 1.2.0 - resolution: "easy-table@npm:1.2.0" - dependencies: - ansi-regex: "npm:^5.0.1" - wcwidth: "npm:^1.0.1" - dependenciesMeta: - wcwidth: - optional: true - checksum: 10/0d1be7cd9419cd1b56ca5a978646b3cff241ccd8cf95bdb2742f36854084b3aef2e9af6ec14142855aa80e4cab1f4baad0f610a99c77509f23676b8330730177 - languageName: node - linkType: hard - "ecc-jsbn@npm:~0.1.1": version: 0.1.2 resolution: "ecc-jsbn@npm:0.1.2" @@ -18382,7 +18349,6 @@ __metadata: json-source-map: "npm:0.6.1" jsurl: "npm:^0.1.5" kbar: "npm:0.1.0-beta.45" - knip: "npm:^5.10.0" lerna: "npm:8.1.8" leven: "npm:^4.0.0" lodash: "npm:4.17.21" @@ -19310,7 +19276,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.0.4, ignore@npm:^5.1.1, ignore@npm:^5.1.8, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1": +"ignore@npm:^5.0.4, ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 @@ -20953,15 +20919,6 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^2.4.0": - version: 2.4.0 - resolution: "jiti@npm:2.4.0" - bin: - jiti: lib/jiti-cli.mjs - checksum: 10/10aa999a4f9bccc82b1dab9ebaf4484a8770450883c1bf7fafc07f8fca1e417fd8e7731e651337d1060c9e2ff3f97362dcdfd27e86d1f385db97f4adf7b5a21d - languageName: node - linkType: hard - "jju@npm:^1.4.0": version: 1.4.0 resolution: "jju@npm:1.4.0" @@ -21429,36 +21386,6 @@ __metadata: languageName: node linkType: hard -"knip@npm:^5.10.0": - version: 5.37.1 - resolution: "knip@npm:5.37.1" - dependencies: - "@nodelib/fs.walk": "npm:1.2.8" - "@snyk/github-codeowners": "npm:1.1.0" - easy-table: "npm:1.2.0" - enhanced-resolve: "npm:^5.17.1" - fast-glob: "npm:^3.3.2" - jiti: "npm:^2.4.0" - js-yaml: "npm:^4.1.0" - minimist: "npm:^1.2.8" - picocolors: "npm:^1.1.0" - picomatch: "npm:^4.0.1" - pretty-ms: "npm:^9.0.0" - smol-toml: "npm:^1.3.0" - strip-json-comments: "npm:5.0.1" - summary: "npm:2.1.0" - zod: "npm:^3.22.4" - zod-validation-error: "npm:^3.0.3" - peerDependencies: - "@types/node": ">=18" - typescript: ">=5.0.4" - bin: - knip: bin/knip.js - knip-bun: bin/knip-bun.js - checksum: 10/e73962f7daac5eb3d275654bbf719e91001aca8d03da9d0dc303cbe0cd47000114cf99d341b377b866f815e7d6821e3fdb1fca24e0a178e8204c1adaa2f2f41c - languageName: node - linkType: hard - "known-css-properties@npm:^0.29.0": version: 0.29.0 resolution: "known-css-properties@npm:0.29.0" @@ -24411,13 +24338,6 @@ __metadata: languageName: node linkType: hard -"parse-ms@npm:^4.0.0": - version: 4.0.0 - resolution: "parse-ms@npm:4.0.0" - checksum: 10/673c801d9f957ff79962d71ed5a24850163f4181a90dd30c4e3666b3a804f53b77f1f0556792e8b2adbb5d58757907d1aa51d7d7dc75997c2a56d72937cbc8b7 - languageName: node - linkType: hard - "parse-path@npm:^7.0.0": version: 7.0.0 resolution: "parse-path@npm:7.0.0" @@ -24677,13 +24597,6 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^4.0.1": - version: 4.0.2 - resolution: "picomatch@npm:4.0.2" - checksum: 10/ce617b8da36797d09c0baacb96ca8a44460452c89362d7cb8f70ca46b4158ba8bc3606912de7c818eb4a939f7f9015cef3c766ec8a0c6bfc725fdc078e39c717 - languageName: node - linkType: hard - "pify@npm:5.0.0": version: 5.0.0 resolution: "pify@npm:5.0.0" @@ -25344,15 +25257,6 @@ __metadata: languageName: node linkType: hard -"pretty-ms@npm:^9.0.0": - version: 9.1.0 - resolution: "pretty-ms@npm:9.1.0" - dependencies: - parse-ms: "npm:^4.0.0" - checksum: 10/3622a8999e4b2aa05ff64bf48c7e58143b3ede6e3434f8ce5588def90ebcf6af98edf79532344c4c9e14d5ad25deb3f0f5ca9f9b91e5d2d1ac26dad9cf428fc0 - languageName: node - linkType: hard - "pretty-time@npm:^1.1.0": version: 1.1.0 resolution: "pretty-time@npm:1.1.0" @@ -28942,13 +28846,6 @@ __metadata: languageName: node linkType: hard -"smol-toml@npm:^1.3.0": - version: 1.3.0 - resolution: "smol-toml@npm:1.3.0" - checksum: 10/dc3e49a9202abca4a60c352ff48c8088a0c48886924b81a0fbab8c66d4575aaa1867f4eac783acea00dcfa34047ccd22634040d2282f9ccb05dc73511c1b0e4e - languageName: node - linkType: hard - "smtp-server@npm:^3.11.0": version: 3.13.4 resolution: "smtp-server@npm:3.13.4" @@ -29739,13 +29636,6 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:5.0.1": - version: 5.0.1 - resolution: "strip-json-comments@npm:5.0.1" - checksum: 10/b314af70c6666a71133e309a571bdb87687fc878d9fd8b38ebed393a77b89835b92f191aa6b0bc10dfd028ba99eed6b6365985001d64c5aef32a4a82456a156b - languageName: node - linkType: hard - "strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -29904,13 +29794,6 @@ __metadata: languageName: node linkType: hard -"summary@npm:2.1.0": - version: 2.1.0 - resolution: "summary@npm:2.1.0" - checksum: 10/10ac12ce12c013b56ad44c37cfac206961f0993d98867b33b1b03a27b38a1cf8dd2db0b788883356c5335bbbb37d953772ef4a381d6fc8f408faf99f2bc54af5 - languageName: node - linkType: hard - "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -32605,16 +32488,7 @@ __metadata: languageName: node linkType: hard -"zod-validation-error@npm:^3.0.3": - version: 3.2.0 - resolution: "zod-validation-error@npm:3.2.0" - peerDependencies: - zod: ^3.18.0 - checksum: 10/2e2aec95e43cc34b741faf2863019be2558da5eb902b7eeb8bbf4c11c24c1b1f91e92e6f97077584b8301bc927061b9b2f1c0ede562f0bc350726c26870f75c1 - languageName: node - linkType: hard - -"zod@npm:^3.22.4, zod@npm:^3.23.8": +"zod@npm:^3.23.8": version: 3.23.8 resolution: "zod@npm:3.23.8" checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1 From 30abff9998e2bac31ca046d7a4a1f0c7248709ab Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 10 Feb 2025 13:05:48 +0000 Subject: [PATCH 456/894] Chore: Bump undici (#100352) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index de701500eda..23df6d88201 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30995,9 +30995,9 @@ __metadata: linkType: hard "undici@npm:^6.19.5": - version: 6.19.8 - resolution: "undici@npm:6.19.8" - checksum: 10/19ae4ba38b029a664d99fd330935ef59136cf99edb04ed821042f27b5a9e84777265fb744c8a7abc83f2059afb019446c69a4ebef07bbc0ed6b2de8d67ef4090 + version: 6.21.1 + resolution: "undici@npm:6.21.1" + checksum: 10/eeccc07e9073ae8e755fdc0dc8cdfaa426c01ec6f815425c3ecedba2e5394cea4993962c040dd168951714a82f0d001a13018c3ae3ad4534f0fa97afe425c08d languageName: node linkType: hard From 68700e3d7de19ff3702d6f738fa6610e7de7cbc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Feb 2025 14:12:23 +0100 Subject: [PATCH 457/894] TimeRangePicker: Options list padding (#100343) --- .../TimeRangePicker/TimeRangeList.tsx | 11 ++++---- .../TimeRangePicker/TimeRangeOption.tsx | 28 +++++++++++++------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeList.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeList.tsx index cfc070c51c7..1e63f4c1921 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeList.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeList.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import { useRef, ReactNode } from 'react'; -import { TimeOption } from '@grafana/data'; +import { GrafanaTheme2, TimeOption } from '@grafana/data'; import { useStyles2 } from '../../../themes'; import { t } from '../../../utils/i18n'; @@ -55,6 +55,7 @@ const Options = ({ options, value, onChange, title }: Props) => { onKeyDown={handleKeys} ref={localRef} aria-roledescription={t('time-picker.time-range.aria-role', 'Time range selection')} + className={styles.list} > {options.map((option, index) => ( { /> ))} -
); }; @@ -91,9 +91,8 @@ const getStyles = () => ({ }), }); -const getOptionsStyles = () => ({ - grow: css({ - flexGrow: 1, - alignItems: 'flex-start', +const getOptionsStyles = (theme: GrafanaTheme2) => ({ + list: css({ + padding: theme.spacing(0.5), }), }); diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeOption.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeOption.tsx index a85a11f948a..37ee5248792 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeOption.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeOption.tsx @@ -14,27 +14,39 @@ const getStyles = (theme: GrafanaTheme2) => { alignItems: 'center', flexDirection: 'row-reverse', justifyContent: 'space-between', - }), - selected: css({ - background: theme.colors.action.selected, - fontWeight: theme.typography.fontWeightMedium, + position: 'relative', }), radio: css({ opacity: 0, width: '0 !important', - '&:focus-visible + label': getFocusStyles(theme), }), label: css({ cursor: 'pointer', flex: 1, - padding: '7px 9px 7px 9px', + padding: theme.spacing(1), + borderRadius: theme.shape.radius.default, '&:hover': { background: theme.colors.action.hover, cursor: 'pointer', }, }), + labelSelected: css({ + background: theme.colors.action.selected, + + '&::before': { + backgroundImage: theme.colors.gradients.brandVertical, + borderRadius: theme.shape.radius.default, + content: '" "', + display: 'block', + height: '100%', + position: 'absolute', + width: theme.spacing(0.5), + left: 0, + top: 0, + }, + }), }; }; @@ -54,7 +66,7 @@ export const TimeRangeOption = memo(({ value, onSelect, selected = false, const id = uuidv4(); return ( -
  • +
  • (({ value, onSelect, selected = false, id={id} onChange={() => onSelect(value)} /> -
  • From 02caf915a5d1474b52095cb0c455f222cde58914 Mon Sep 17 00:00:00 2001 From: beejeebus Date: Mon, 10 Feb 2025 08:43:31 -0500 Subject: [PATCH 458/894] Don't remove DWARF info from Go binaries in dev (#100328) Only ask the linker to strip DWARF information if we're not in dev, to avoid seeing stuff like this when using delve: ~ $ dlv attach $(pgrep grafana) (dlv) l main.main Command failed: location "main.main" not found After this change: ~ $ dlv attach $(pgrep grafana) Type 'help' for list of commands. (dlv) l main.main Showing /home/justin/code/grafana/pkg/cmd/grafana/main.go:23 (PC: 0xac93533) 18: var commit = gcli.DefaultCommitValue 19: var enterpriseCommit = gcli.DefaultCommitValue 20: var buildBranch = "main" 21: var buildstamp string 22: 23: func main() { 24: app := MainApp() 25: 26: if err := app.Run(os.Args); err != nil { --- pkg/build/cmd.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/build/cmd.go b/pkg/build/cmd.go index 7b775ad0f44..9796a1428da 100644 --- a/pkg/build/cmd.go +++ b/pkg/build/cmd.go @@ -243,7 +243,16 @@ func ldflags(opts BuildOpts) (string, error) { buildBranch = v } var b bytes.Buffer - b.WriteString("-w") + if !opts.isDev { + // Only ask the linker to strip DWARF information if we're not in + // dev, to avoid seeing stuff like this when using delve: + // + // ~ $ dlv attach $(pgrep grafana) + // (dlv) l main.main + // Command failed: location "main.main" not found + // + b.WriteString("-w") + } b.WriteString(fmt.Sprintf(" -X main.version=%s", opts.version)) b.WriteString(fmt.Sprintf(" -X main.commit=%s", commitSha)) if enterpriseCommitSha != "" { From dec07c4c3496b0ada0a8a439d45b8d1f6eee9135 Mon Sep 17 00:00:00 2001 From: Marco Schaefer <47627413+codecapitano@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:07:32 +0100 Subject: [PATCH 459/894] Update-faro-versions-in-grafana-packages (#100354) * update faro versions in grafana-runtime * update faro packages in root * update Faro version in Grafana UI * upgrade faro version in grafana-prometheus * replace deprecated type --- package.json | 6 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../GrafanaJavascriptAgentBackend.ts | 4 +- yarn.lock | 56 +++++++++++-------- 6 files changed, 41 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index fc88ff26ab6..c3d81624ad9 100644 --- a/package.json +++ b/package.json @@ -263,9 +263,9 @@ "@grafana/azure-sdk": "0.0.5", "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", - "@grafana/faro-core": "^1.13.1", - "@grafana/faro-web-sdk": "^1.13.1", - "@grafana/faro-web-tracing": "^1.13.1", + "@grafana/faro-core": "^1.13.2", + "@grafana/faro-web-sdk": "^1.13.2", + "@grafana/faro-web-tracing": "^1.13.2", "@grafana/flamegraph": "workspace:*", "@grafana/google-sdk": "0.1.2", "@grafana/lezer-logql": "0.2.7", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 6a0b4e81593..27577e85c5b 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -39,7 +39,7 @@ "@emotion/css": "11.13.5", "@floating-ui/react": "0.27.3", "@grafana/data": "11.6.0-pre", - "@grafana/faro-web-sdk": "1.12.3", + "@grafana/faro-web-sdk": "^1.13.2", "@grafana/llm": "0.12.0", "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "11.6.0-pre", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 085b10800f2..9a83e3cde67 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -39,7 +39,7 @@ "dependencies": { "@grafana/data": "11.6.0-pre", "@grafana/e2e-selectors": "11.6.0-pre", - "@grafana/faro-web-sdk": "^1.3.6", + "@grafana/faro-web-sdk": "^1.13.2", "@grafana/schema": "11.6.0-pre", "@grafana/ui": "11.6.0-pre", "history": "4.10.1", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index c7bc33a0a57..b693a8fb5ac 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -52,7 +52,7 @@ "@floating-ui/react": "0.27.3", "@grafana/data": "11.6.0-pre", "@grafana/e2e-selectors": "11.6.0-pre", - "@grafana/faro-web-sdk": "^1.3.6", + "@grafana/faro-web-sdk": "^1.13.2", "@grafana/schema": "11.6.0-pre", "@hello-pangea/dnd": "17.0.0", "@leeoniya/ufuzzy": "1.0.18", diff --git a/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.ts b/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.ts index fb181d2030e..3af977436d0 100644 --- a/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.ts +++ b/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.ts @@ -5,12 +5,12 @@ import { BrowserConfig, ErrorsInstrumentation, ConsoleInstrumentation, - ConsoleInstrumentationOptions, WebVitalsInstrumentation, SessionInstrumentation, FetchTransport, type Instrumentation, getWebInstrumentations, + Config, } from '@grafana/faro-web-sdk'; import { TracingInstrumentation } from '@grafana/faro-web-tracing'; import { EchoBackend, EchoEvent, EchoEventType } from '@grafana/runtime'; @@ -61,7 +61,7 @@ export class GrafanaJavascriptAgentBackend ]; const transports: BaseTransport[] = [new EchoSrvTransport({ ignoreUrls })]; - const consoleInstrumentationOptions: ConsoleInstrumentationOptions = + const consoleInstrumentationOptions: Config['consoleInstrumentation'] = options.allInstrumentationsEnabled || options.consoleInstrumentalizationEnabled ? { serializeErrors: true, diff --git a/yarn.lock b/yarn.lock index 23df6d88201..4f4260e4e94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3331,7 +3331,28 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:1.12.3, @grafana/faro-web-sdk@npm:^1.3.6": +"@grafana/faro-core@npm:^1.13.2": + version: 1.13.2 + resolution: "@grafana/faro-core@npm:1.13.2" + dependencies: + "@opentelemetry/api": "npm:^1.9.0" + "@opentelemetry/otlp-transformer": "npm:^0.57.1" + checksum: 10/513fabf5e90121e1e0a28d36b61aa994b5ad556635366be518e2c23acbaa43e1d0ac744929bfb5930fb1776a2fc6eba33190ce08c058007555e136078987332c + languageName: node + linkType: hard + +"@grafana/faro-web-sdk@npm:^1.13.2": + version: 1.13.2 + resolution: "@grafana/faro-web-sdk@npm:1.13.2" + dependencies: + "@grafana/faro-core": "npm:^1.13.1" + ua-parser-js: "npm:^1.0.32" + web-vitals: "npm:^4.0.1" + checksum: 10/a6896cbbe3b89867a23ffa4f482eb9510d0e9cd637c92a9e04e06157471bb7ffd35d68b7f6077791393e2203cad69f48ade00e86e54d454da10bbb6179cdc357 + languageName: node + linkType: hard + +"@grafana/faro-web-sdk@npm:^1.3.6": version: 1.12.3 resolution: "@grafana/faro-web-sdk@npm:1.12.3" dependencies: @@ -3342,22 +3363,11 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:^1.13.1": - version: 1.13.1 - resolution: "@grafana/faro-web-sdk@npm:1.13.1" +"@grafana/faro-web-tracing@npm:^1.13.2": + version: 1.13.2 + resolution: "@grafana/faro-web-tracing@npm:1.13.2" dependencies: - "@grafana/faro-core": "npm:^1.13.1" - ua-parser-js: "npm:^1.0.32" - web-vitals: "npm:^4.0.1" - checksum: 10/c06e0b5eb179ab3e5d29ec856a45f1c0e1e8980f1fc5b03500e3ca3ed725b74aae709abb0749f2053b532c20c28b937fa916101f3466f5fd30e979bbb826f151 - languageName: node - linkType: hard - -"@grafana/faro-web-tracing@npm:^1.13.1": - version: 1.13.1 - resolution: "@grafana/faro-web-tracing@npm:1.13.1" - dependencies: - "@grafana/faro-web-sdk": "npm:^1.13.1" + "@grafana/faro-web-sdk": "npm:^1.13.2" "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/context-zone": "npm:1.30.1" "@opentelemetry/core": "npm:^1.30.0" @@ -3369,7 +3379,7 @@ __metadata: "@opentelemetry/resources": "npm:^1.30.0" "@opentelemetry/sdk-trace-web": "npm:^1.30.0" "@opentelemetry/semantic-conventions": "npm:^1.28.0" - checksum: 10/378f235b384d4b53c32d5f7779a57114e48f7044cd6205fede8e368fc26f5d872d0146803159e82ed6b7a19b43b4317e867af7f0b8e9f90f63aa0dcb42db156d + checksum: 10/bfc67e073457f50d3f5c95d47dfadc900358c08bd1b7d65db2c08bd2b45edcd6ac661bfa82683f1c9ea3710946152aa441a5db665e1e3e578de186fde51fe887 languageName: node linkType: hard @@ -3622,7 +3632,7 @@ __metadata: "@floating-ui/react": "npm:0.27.3" "@grafana/data": "npm:11.6.0-pre" "@grafana/e2e-selectors": "npm:11.6.0-pre" - "@grafana/faro-web-sdk": "npm:1.12.3" + "@grafana/faro-web-sdk": "npm:^1.13.2" "@grafana/llm": "npm:0.12.0" "@grafana/plugin-ui": "npm:0.10.1" "@grafana/runtime": "npm:11.6.0-pre" @@ -3729,7 +3739,7 @@ __metadata: dependencies: "@grafana/data": "npm:11.6.0-pre" "@grafana/e2e-selectors": "npm:11.6.0-pre" - "@grafana/faro-web-sdk": "npm:^1.3.6" + "@grafana/faro-web-sdk": "npm:^1.13.2" "@grafana/schema": "npm:11.6.0-pre" "@grafana/tsconfig": "npm:^2.0.0" "@grafana/ui": "npm:11.6.0-pre" @@ -4030,7 +4040,7 @@ __metadata: "@floating-ui/react": "npm:0.27.3" "@grafana/data": "npm:11.6.0-pre" "@grafana/e2e-selectors": "npm:11.6.0-pre" - "@grafana/faro-web-sdk": "npm:^1.3.6" + "@grafana/faro-web-sdk": "npm:^1.13.2" "@grafana/schema": "npm:11.6.0-pre" "@grafana/tsconfig": "npm:^2.0.0" "@hello-pangea/dnd": "npm:17.0.0" @@ -18142,9 +18152,9 @@ __metadata: "@grafana/e2e-selectors": "workspace:*" "@grafana/eslint-config": "npm:8.0.0" "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules" - "@grafana/faro-core": "npm:^1.13.1" - "@grafana/faro-web-sdk": "npm:^1.13.1" - "@grafana/faro-web-tracing": "npm:^1.13.1" + "@grafana/faro-core": "npm:^1.13.2" + "@grafana/faro-web-sdk": "npm:^1.13.2" + "@grafana/faro-web-tracing": "npm:^1.13.2" "@grafana/flamegraph": "workspace:*" "@grafana/google-sdk": "npm:0.1.2" "@grafana/lezer-logql": "npm:0.2.7" From 1b8db233a77c4ac8541151908b43fb0bde7c181e Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Mon, 10 Feb 2025 09:20:35 -0500 Subject: [PATCH 460/894] Alerting: Rule Version API to Ignore versions without diff (#100093) --- pkg/services/ngalert/models/testing.go | 6 ++ pkg/services/ngalert/store/alert_rule.go | 18 ++++- pkg/services/ngalert/store/alert_rule_test.go | 68 +++++++++++++++++++ pkg/services/ngalert/store/models.go | 23 +++++++ 4 files changed, 114 insertions(+), 1 deletion(-) diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 515b8eed07c..8feca77ec27 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -563,6 +563,12 @@ func (a *AlertRuleMutators) WithKey(key AlertRuleKey) AlertRuleMutator { } } +func (a *AlertRuleMutators) WithVersion(version int64) AlertRuleMutator { + return func(r *AlertRule) { + r.Version = version + } +} + func (g *AlertRuleGenerator) GenerateLabels(min, max int, prefix string) data.Labels { count := max if min > max { diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index fbc9e061385..17f3f294214 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -121,11 +121,12 @@ func (st DBstore) GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAler func (st DBstore) GetAlertRuleVersions(ctx context.Context, key ngmodels.AlertRuleKey) ([]*ngmodels.AlertRule, error) { alertRules := make([]*ngmodels.AlertRule, 0) err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { - rows, err := sess.Table(new(alertRuleVersion)).Where("rule_org_id = ? AND rule_uid = ?", key.OrgID, key.UID).Desc("id").Rows(new(alertRuleVersion)) + rows, err := sess.Table(new(alertRuleVersion)).Where("rule_org_id = ? AND rule_uid = ?", key.OrgID, key.UID).Asc("id").Rows(new(alertRuleVersion)) if err != nil { return err } // Deserialize each rule separately in case any of them contain invalid JSON. + var previousVersion *alertRuleVersion for rows.Next() { rule := new(alertRuleVersion) err = rows.Scan(rule) @@ -133,11 +134,17 @@ func (st DBstore) GetAlertRuleVersions(ctx context.Context, key ngmodels.AlertRu st.Logger.Error("Invalid rule version found in DB store, ignoring it", "func", "GetAlertRuleVersions", "error", err) continue } + // skip version that has no diff with previous version + // this is pretty basic comparison, it may have false negatives + if previousVersion != nil && previousVersion.EqualSpec(*rule) { + continue + } converted, err := alertRuleToModelsAlertRule(alertRuleVersionToAlertRule(*rule), st.Logger) if err != nil { st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "GetAlertRuleVersions", "error", err, "version_id", rule.ID) continue } + previousVersion = rule alertRules = append(alertRules, &converted) } return nil @@ -145,6 +152,15 @@ func (st DBstore) GetAlertRuleVersions(ctx context.Context, key ngmodels.AlertRu if err != nil { return nil, err } + slices.SortFunc(alertRules, func(a, b *ngmodels.AlertRule) int { + if a.ID > b.ID { + return -1 + } + if a.ID < b.ID { + return 1 + } + return 0 + }) return alertRules, nil } diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 10ccf9b9faf..0eba646ab46 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -1543,6 +1543,74 @@ func TestIncreaseVersionForAllRulesInNamespaces(t *testing.T) { }) } +func TestGetRuleVersions(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + cfg := setting.NewCfg() + cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{BaseInterval: time.Duration(rand.Int63n(100)+1) * time.Second} + sqlStore := db.InitTestDB(t) + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b) + orgID := int64(1) + gen := models.RuleGen + gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval), gen.WithOrgID(orgID), gen.WithVersion(1)) + + inserted, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, []models.AlertRule{gen.Generate()}) + require.NoError(t, err) + ruleV1, err := store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{UID: inserted[0].UID}) + require.NoError(t, err) + ruleV2 := models.CopyRule(ruleV1, gen.WithTitle(util.GenerateShortUID()), gen.WithGroupIndex(rand.Int())) + + err = store.UpdateAlertRules(context.Background(), &models.AlertingUserUID, []models.UpdateRule{ + { + Existing: ruleV1, + New: *ruleV2, + }, + }) + require.NoError(t, err) + + t.Run("should return rule versions sorted in decreasing order", func(t *testing.T) { + versions, err := store.GetAlertRuleVersions(context.Background(), ruleV2.GetKey()) + require.NoError(t, err) + assert.Len(t, versions, 2) + assert.IsDecreasing(t, versions[0].ID, versions[1].ID) + diff := versions[1].Diff(versions[0], AlertRuleFieldsToIgnoreInDiff[:]...) + assert.ElementsMatch(t, []string{"Title", "RuleGroupIndex"}, diff.Paths()) + }) + + t.Run("should not remove versions without diff", func(t *testing.T) { + for i := 0; i < rand.Intn(2)+1; i++ { + r, err := store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{UID: ruleV2.UID}) + require.NoError(t, err) + rn := models.CopyRule(r) + err = store.UpdateAlertRules(context.Background(), &models.AlertingUserUID, []models.UpdateRule{ + { + Existing: r, + New: *rn, + }, + }) + require.NoError(t, err) + } + ruleV2, err = store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{UID: ruleV2.UID}) + ruleV3 := models.CopyRule(ruleV2, gen.WithGroupName(util.GenerateShortUID()), gen.WithNamespaceUID(util.GenerateShortUID())) + + err = store.UpdateAlertRules(context.Background(), &models.AlertingUserUID, []models.UpdateRule{ + { + Existing: ruleV2, + New: *ruleV3, + }, + }) + + versions, err := store.GetAlertRuleVersions(context.Background(), ruleV3.GetKey()) + require.NoError(t, err) + assert.Len(t, versions, 3) + diff := versions[0].Diff(versions[1], AlertRuleFieldsToIgnoreInDiff[:]...) + assert.ElementsMatch(t, []string{"RuleGroup", "NamespaceUID"}, diff.Paths()) + }) +} + // createAlertRule creates an alert rule in the database and returns it. // If a generator is not specified, uniqueness of primary key is not guaranteed. func createRule(t *testing.T, store *DBstore, generator *models.AlertRuleGenerator) *models.AlertRule { diff --git a/pkg/services/ngalert/store/models.go b/pkg/services/ngalert/store/models.go index 3e47ab90fa4..ed9456036c2 100644 --- a/pkg/services/ngalert/store/models.go +++ b/pkg/services/ngalert/store/models.go @@ -65,6 +65,29 @@ type alertRuleVersion struct { Metadata string `xorm:"metadata"` } +// EqualSpec compares two alertRuleVersion objects for equality based on their specifications and returns true if they match. +// The comparison is very basic and can produce false-negative. Fields excluded: ID, ParentVersion, RestoredFrom, Version, Created and CreatedBy +func (a alertRuleVersion) EqualSpec(b alertRuleVersion) bool { + return a.RuleOrgID == b.RuleOrgID && + a.RuleUID == b.RuleUID && + a.RuleNamespaceUID == b.RuleNamespaceUID && + a.RuleGroup == b.RuleGroup && + a.RuleGroupIndex == b.RuleGroupIndex && + a.Title == b.Title && + a.Condition == b.Condition && + a.Data == b.Data && + a.IntervalSeconds == b.IntervalSeconds && + a.Record == b.Record && + a.NoDataState == b.NoDataState && + a.ExecErrState == b.ExecErrState && + a.For == b.For && + a.Annotations == b.Annotations && + a.Labels == b.Labels && + a.IsPaused == b.IsPaused && + a.NotificationSettings == b.NotificationSettings && + a.Metadata == b.Metadata +} + func (a alertRuleVersion) TableName() string { return "alert_rule_version" } From 55e7c4ae6d65d05519c6f494af25195c91d45d3d Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Mon, 10 Feb 2025 16:11:06 +0100 Subject: [PATCH 461/894] Prometheus: Get the utcOffset value of timezone when it's specified (#99910) * get the utcOffset value of timezone when it's specified * Update packages/grafana-prometheus/src/datasource.ts Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * calculate utcOffset from timezone when the request timezone is not browser * lint --------- Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> --- packages/grafana-prometheus/src/datasource.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index cb4c80f0afd..6c8b1d3ec55 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -1,5 +1,6 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/datasource.ts import { defaults } from 'lodash'; +import { tz } from 'moment-timezone'; import { lastValueFrom, Observable, throwError } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import semver from 'semver/preload'; @@ -373,13 +374,32 @@ export class PrometheusDatasource } processTargetV2(target: PromQuery, request: DataQueryRequest) { + // The `utcOffsetSec` parameter is required by the backend to correctly align time ranges. + // This alignment ensures that relative time ranges (e.g., "Last N hours/days/years") are adjusted + // according to the user's selected time zone, rather than defaulting to UTC. + // + // Example: If the user selects "Last 5 days," each day should begin at 00:00 in the chosen time zone, + // rather than at 00:00 UTC, ensuring an accurate breakdown. + // + // This adjustment does not apply to absolute time ranges, where users explicitly set + // the start and end timestamps. + // + // Handling `utcOffsetSec`: + // - When using the browser's time zone, the UTC offset is derived from the request range object. + // - When the user selects a custom time zone, the UTC offset must be calculated accordingly. + // More details: + // - Issue that led to the introduction of utcOffsetSec: https://github.com/grafana/grafana/issues/17278 + // - Implementation PR: https://github.com/grafana/grafana/pull/17477 + let utcOffset = request.range.to.utcOffset(); + if (request.timezone !== 'browser') { + utcOffset = tz(request.timezone).utcOffset(); + } const processedTargets: PromQuery[] = []; const processedTarget = { ...target, exemplar: this.shouldRunExemplarQuery(target, request), requestId: request.panelId + target.refId, - // We need to pass utcOffsetSec to backend to calculate aligned range - utcOffsetSec: request.range.to.utcOffset() * 60, + utcOffsetSec: utcOffset * 60, }; if (config.featureToggles.promQLScope) { From 7dee4d18088c6edec061df6c7fd222a25cdf38a1 Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Mon, 10 Feb 2025 10:28:34 -0500 Subject: [PATCH 462/894] Alerting: Allow specifying uid for new rules added to groups (#99858) When modifying rule groups the `uid` can be specified but only if the rule already existed in the DB. If the rule is new the update would be rejected. This updates the RuleGroup provisioning apis to allow specifying the `uid` when creating/updating rule groups. Additionally, the RuleGroupIdx was not being updated when rules were reordered in the group. Context: https://github.com/grafana/terraform-provider-grafana/pull/1971#issuecomment-2599223897 Relates to: https://github.com/grafana/terraform-provider-grafana/issues/1928 Fixes: #98283 --- .../api/api_alertmanager_guards_test.go | 1 + pkg/services/ngalert/models/alert_rule.go | 4 + .../ngalert/provisioning/alert_rules.go | 15 + pkg/services/ngalert/store/deltas.go | 21 +- pkg/services/ngalert/store/deltas_test.go | 11 +- .../api/alerting/api_provisioning_test.go | 172 +++++++++++- pkg/tests/api/alerting/api_ruler_test.go | 261 ++++++++++++++++-- pkg/tests/api/alerting/testing.go | 24 ++ 8 files changed, 467 insertions(+), 42 deletions(-) diff --git a/pkg/services/ngalert/api/api_alertmanager_guards_test.go b/pkg/services/ngalert/api/api_alertmanager_guards_test.go index a2330d6d533..6912baed2c5 100644 --- a/pkg/services/ngalert/api/api_alertmanager_guards_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_guards_test.go @@ -693,6 +693,7 @@ func TestCheckMuteTimes(t *testing.T) { } func gettableMuteIntervals(t *testing.T, muteTimeIntervals []amConfig.MuteTimeInterval, provenances map[string]definitions.Provenance) definitions.GettableUserConfig { + t.Helper() return definitions.GettableUserConfig{ AlertmanagerConfig: definitions.GettableApiAlertingConfig{ MuteTimeProvenances: provenances, diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index b7da813141f..26f7d84eee1 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/cmputil" ) @@ -571,6 +572,9 @@ func (alertRule *AlertRule) PreSave(timeNow func() time.Time, userUID *UserUID) // ValidateAlertRule validates various alert rule fields. func (alertRule *AlertRule) ValidateAlertRule(cfg setting.UnifiedAlertingSettings) error { + if err := util.ValidateUID(alertRule.UID); err != nil { + return errors.Join(ErrAlertRuleFailedValidation, fmt.Errorf("cannot create rule with UID '%s': %w", alertRule.UID, err)) + } if len(alertRule.Data) == 0 { return fmt.Errorf("%w: no queries or expressions are found", ErrAlertRuleFailedValidation) } diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index c31413b3bdc..e58e674eff2 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -368,6 +368,16 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user iden return err } + for _, rule := range group.Rules { + if rule.UID == "" { + // if empty the UID will be generated before save + continue + } + if err := util.ValidateUID(rule.UID); err != nil { + return fmt.Errorf("%w: cannot create rule with UID %q: %w", models.ErrAlertRuleFailedValidation, rule.UID, err) + } + } + delta, err := service.calcDelta(ctx, user, group) if err != nil { return err @@ -575,6 +585,10 @@ func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, user ident // No changes to the rule. return rule, nil } + // new rules not allowed in update for a single rule + if len(delta.New) > 0 { + return models.AlertRule{}, fmt.Errorf("failed to update rule with UID %s because %w", rule.UID, models.ErrAlertRuleNotFound) + } for _, d := range delta.Update { if d.Existing.GetKey() == rule.GetKey() { storedRule = d.Existing @@ -817,6 +831,7 @@ func syncGroupRuleFields(group *models.AlertRuleGroup, orgID int64) *models.Aler group.Rules[i].RuleGroup = group.Title group.Rules[i].NamespaceUID = group.FolderUID group.Rules[i].OrgID = orgID + group.Rules[i].RuleGroupIndex = i } return group } diff --git a/pkg/services/ngalert/store/deltas.go b/pkg/services/ngalert/store/deltas.go index 27469f5733a..e08bc3c5cfc 100644 --- a/pkg/services/ngalert/store/deltas.go +++ b/pkg/services/ngalert/store/deltas.go @@ -113,10 +113,9 @@ func calculateChanges(ctx context.Context, ruleReader RuleReader, groupKey model } loadedRulesByUID[rule.UID] = rule } - if existing == nil { - return nil, fmt.Errorf("failed to update rule with UID %s because %w", r.UID, models.ErrAlertRuleNotFound) + if existing != nil { + affectedGroups[existing.GetGroupKey()] = ruleList } - affectedGroups[existing.GetGroupKey()] = ruleList } } @@ -126,18 +125,14 @@ func calculateChanges(ctx context.Context, ruleReader RuleReader, groupKey model } models.PatchPartialAlertRule(existing, r) - diff := existing.Diff(&r.AlertRule, AlertRuleFieldsToIgnoreInDiff[:]...) - if len(diff) == 0 { - continue + if len(diff) > 0 { + toUpdate = append(toUpdate, RuleDelta{ + Existing: existing, + New: &r.AlertRule, + Diff: diff, + }) } - - toUpdate = append(toUpdate, RuleDelta{ - Existing: existing, - New: &r.AlertRule, - Diff: diff, - }) - continue } toDelete := make([]*models.AlertRule, 0, len(existingGroupRulesUIDs)) diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index 2b5c9760575..904c0da5164 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -240,14 +240,19 @@ func TestCalculateChanges(t *testing.T) { require.Len(t, changes.AffectedGroups[sourceGroupKey], len(inDatabase)) }) - t.Run("should fail when submitted rule has UID that does not exist in db", func(t *testing.T) { + t.Run("should add rule when submitted rule has UID that does not exist in db", func(t *testing.T) { fakeStore := fakes.NewRuleStore(t) groupKey := models.GenerateGroupKey(orgId) submitted := gen.With(gen.WithOrgID(orgId), simulateSubmitted).Generate() require.NotEqual(t, "", submitted.UID) - _, err := CalculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRuleWithOptionals{{AlertRule: submitted}}) - require.Error(t, err) + diff, err := CalculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRuleWithOptionals{{AlertRule: submitted}}) + require.NoError(t, err) + + require.Len(t, diff.New, 1) + require.Empty(t, diff.Delete) + require.Empty(t, diff.Update) + require.Equal(t, submitted, *diff.New[0]) }) t.Run("should fail if cannot fetch current rules in the group", func(t *testing.T) { diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index f4bc4dab890..f177c2b849f 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -508,7 +508,34 @@ func TestIntegrationProvisioning(t *testing.T) { t.Run("when provisioning alert rules", func(t *testing.T) { url := fmt.Sprintf("http://%s/api/v1/provisioning/alert-rules", grafanaListedAddr) - body := `{"orgID":1,"folderUID":"default","ruleGroup":"Test Group","title":"Provisioned","condition":"A","data":[{"refId":"A","queryType":"","relativeTimeRange":{"from":600,"to":0},"datasourceUid":"f558c85f-66ad-4fd1-b31d-7979e6c93db4","model":{"editorMode":"code","exemplar":false,"expr":"sum(rate(low_card[5m])) \u003e 0","format":"time_series","instant":true,"intervalMs":1000,"legendFormat":"__auto","maxDataPoints":43200,"range":false,"refId":"A"}}],"noDataState":"NoData","execErrState":"Error","for":"0s"}` + body := ` + { + "orgID":1, + "folderUID":"default", + "ruleGroup":"Test Group", + "title":"Provisioned", + "condition":"A", + "data":[{ + "refId":"A", + "queryType":"", + "relativeTimeRange":{"from":600,"to":0}, + "datasourceUid":"f558c85f-66ad-4fd1-b31d-7979e6c93db4", + "model":{ + "editorMode":"code", + "exemplar":false, + "expr":"sum(rate(low_card[5m])) \u003e 0", + "format":"time_series", + "instant":true, + "intervalMs":1000, + "legendFormat":"__auto", + "maxDataPoints":43200, + "range":false,"refId":"A" + } + }], + "noDataState":"NoData", + "execErrState":"Error", + "for":"0s" + }` req := createTestRequest("POST", url, "admin", body) resp, err := http.DefaultClient.Do(req) require.NoError(t, err) @@ -535,6 +562,149 @@ func TestIntegrationProvisioning(t *testing.T) { }) } +func TestIntegrationProvisioningRules(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + + // Create a users to make authenticated requests + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + Password: "viewer", + Login: "viewer", + }) + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleEditor), + Password: "editor", + Login: "editor", + }) + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "admin", + Login: "admin", + }) + + apiClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor") + // Create the namespace we'll save our alerts to. + namespaceUID := "default" + apiClient.CreateFolder(t, namespaceUID, namespaceUID) + + t.Run("when provisioning alert rules", func(t *testing.T) { + originalRuleGroup := definitions.AlertRuleGroup{ + Title: "TestGroup", + Interval: 60, + FolderUID: "default", + Rules: []definitions.ProvisionedAlertRule{ + { + UID: "rule1", + Title: "Rule1", + OrgID: 1, + RuleGroup: "TestGroup", + Condition: "A", + NoDataState: definitions.Alerting, + ExecErrState: definitions.AlertingErrState, + For: model.Duration(time.Duration(60) * time.Second), + Data: []definitions.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: definitions.RelativeTimeRange{ + From: definitions.Duration(time.Duration(5) * time.Hour), + To: definitions.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)), + }, + }, + }, + { + UID: "rule2", + Title: "Rule2", + OrgID: 1, + RuleGroup: "TestGroup", + Condition: "A", + NoDataState: definitions.Alerting, + ExecErrState: definitions.AlertingErrState, + For: model.Duration(time.Duration(60) * time.Second), + Data: []definitions.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: definitions.RelativeTimeRange{ + From: definitions.Duration(time.Duration(5) * time.Hour), + To: definitions.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)), + }, + }, + }, + { + UID: "rule3", + Title: "Rule3", + OrgID: 1, + RuleGroup: "TestGroup", + Condition: "A", + NoDataState: definitions.Alerting, + ExecErrState: definitions.AlertingErrState, + For: model.Duration(time.Duration(60) * time.Second), + Data: []definitions.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: definitions.RelativeTimeRange{ + From: definitions.Duration(time.Duration(5) * time.Hour), + To: definitions.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)), + }, + }, + }, + }, + } + + result, status, raw := apiClient.CreateOrUpdateRuleGroupProvisioning(t, originalRuleGroup) + t.Run("should create a new rule group with UIDs specified", func(t *testing.T) { + requireStatusCode(t, http.StatusOK, status, raw) + require.Equal(t, originalRuleGroup, result) + }) + + t.Run("should remove a rule when updating group with a rule removed", func(t *testing.T) { + existingRuleGroup, status, raw := apiClient.GetRuleGroupProvisioning(t, "default", "TestGroup") + requireStatusCode(t, http.StatusOK, status, raw) + require.Len(t, existingRuleGroup.Rules, 3) + + updatedRuleGroup := existingRuleGroup + updatedRuleGroup.Rules = updatedRuleGroup.Rules[:2] + result, status, raw := apiClient.CreateOrUpdateRuleGroupProvisioning(t, updatedRuleGroup) + requireStatusCode(t, http.StatusOK, status, raw) + require.Equal(t, updatedRuleGroup, result) + + // Check that the rule was removed + rules, status, raw := apiClient.GetRuleGroupProvisioning(t, existingRuleGroup.FolderUID, existingRuleGroup.Title) + requireStatusCode(t, http.StatusOK, status, raw) + require.Len(t, rules.Rules, 2) + }) + + t.Run("should recreate a rule when updating group with the rule added back", func(t *testing.T) { + result, status, raw := apiClient.CreateOrUpdateRuleGroupProvisioning(t, originalRuleGroup) + requireStatusCode(t, http.StatusOK, status, raw) + require.Equal(t, originalRuleGroup, result) + require.Len(t, result.Rules, 3) + + // Check that the rule was re-added + rules, status, raw := apiClient.GetRuleGroupProvisioning(t, originalRuleGroup.FolderUID, originalRuleGroup.Title) + requireStatusCode(t, http.StatusOK, status, raw) + require.Len(t, rules.Rules, 3) + }) + }) +} + func TestMuteTimings(t *testing.T) { dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index c177c6c4412..9f03a070f53 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -3098,6 +3098,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { assert.Empty(t, resp.Deleted) } + createdRuleUIDs := make(map[string]string) // With the rules created, let's make sure that rule definition is stored correctly. { u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) @@ -3228,9 +3229,11 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { ] }` assert.JSONEq(t, expectedGetNamespaceResponseBody, body) + createdRuleUIDs["AlwaysFiring"] = generatedUIDs[0] + createdRuleUIDs["AlwaysFiringButSilenced"] = generatedUIDs[1] } - // try to update by pass an invalid UID + // validate that a rulegroup with a new rule with a user specified UID can be created while others updated { interval, err := model.ParseDuration("30s") require.NoError(t, err) @@ -3238,6 +3241,57 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { rules := apimodels.PostableRuleGroupConfig{ Name: "arulegroup", Rules: []apimodels.PostableExtendedRuleNode{ + { + ApiRuleNode: &apimodels.ApiRuleNode{ + For: &interval, + Labels: map[string]string{"label1": "val1"}, + Annotations: map[string]string{"annotation1": "val1"}, + }, + // this rule does not explicitly set no data and error states + // therefore it should get the default values + GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ + Title: "AlwaysFiring", + UID: createdRuleUIDs["AlwaysFiring"], + Condition: "A", + Data: []apimodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: apimodels.RelativeTimeRange{ + From: apimodels.Duration(time.Duration(5) * time.Hour), + To: apimodels.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage(`{ + "type": "math", + "expression": "2 + 3 > 1" + }`), + }, + }, + }, + }, + { + GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ + Title: "AlwaysFiringButSilenced", + UID: createdRuleUIDs["AlwaysFiringButSilenced"], + Condition: "A", + Data: []apimodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: apimodels.RelativeTimeRange{ + From: apimodels.Duration(time.Duration(5) * time.Hour), + To: apimodels.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage(`{ + "type": "math", + "expression": "2 + 3 > 1" + }`), + }, + }, + NoDataState: apimodels.NoDataState(ngmodels.Alerting), + ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState), + }, + }, { ApiRuleNode: &apimodels.ApiRuleNode{ For: &interval, @@ -3276,31 +3330,83 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { Interval: interval, } - _, status, body := apiClient.PostRulesGroupWithStatus(t, "default", &rules) - assert.Equal(t, http.StatusNotFound, status) - var res map[string]any - assert.NoError(t, json.Unmarshal([]byte(body), &res)) - require.Equal(t, "failed to update rule group: failed to update rule with UID unknown because could not find alert rule", res["message"]) + response, status, _ := apiClient.PostRulesGroupWithStatus(t, "default", &rules) + assert.Equal(t, http.StatusAccepted, status) - // let's make sure that rule definitions are not affected by the failed POST request. - u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) - // nolint:gosec - resp, err := http.Get(u) - require.NoError(t, err) - t.Cleanup(func() { - err := resp.Body.Close() - require.NoError(t, err) - }) - b, err := io.ReadAll(resp.Body) + require.Len(t, response.Created, 1) + require.Len(t, response.Updated, 2) + require.Len(t, response.Deleted, 0) + } + + // remove the added rule and set the interval back to 1m + { + interval, err := model.ParseDuration("1m") require.NoError(t, err) - assert.Equal(t, resp.StatusCode, 202) + rules := apimodels.PostableRuleGroupConfig{ + Name: "arulegroup", + Rules: []apimodels.PostableExtendedRuleNode{ + { + ApiRuleNode: &apimodels.ApiRuleNode{ + For: &interval, + Labels: map[string]string{"label1": "val1"}, + Annotations: map[string]string{"annotation1": "val1"}, + }, + // this rule does not explicitly set no data and error states + // therefore it should get the default values + GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ + Title: "AlwaysFiring", + UID: createdRuleUIDs["AlwaysFiring"], + Condition: "A", + Data: []apimodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: apimodels.RelativeTimeRange{ + From: apimodels.Duration(time.Duration(5) * time.Hour), + To: apimodels.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage(`{ + "type": "math", + "expression": "2 + 3 > 1" + }`), + }, + }, + }, + }, + { + GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ + Title: "AlwaysFiringButSilenced", + UID: createdRuleUIDs["AlwaysFiringButSilenced"], + Condition: "A", + Data: []apimodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: apimodels.RelativeTimeRange{ + From: apimodels.Duration(time.Duration(5) * time.Hour), + To: apimodels.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage(`{ + "type": "math", + "expression": "2 + 3 > 1" + }`), + }, + }, + NoDataState: apimodels.NoDataState(ngmodels.Alerting), + ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState), + }, + }, + }, + Interval: interval, + } - body, m := rulesNamespaceWithoutVariableValues(t, b) - returnedUIDs, ok := m["default,arulegroup"] - assert.True(t, ok) - assert.Equal(t, 2, len(returnedUIDs)) - assert.JSONEq(t, expectedGetNamespaceResponseBody, body) + response, status, _ := apiClient.PostRulesGroupWithStatus(t, "default", &rules) + assert.Equal(t, http.StatusAccepted, status) + + require.Len(t, response.Created, 0) + require.Len(t, response.Updated, 2) + require.Len(t, response.Deleted, 1) } // try to update by pass two rules with conflicting UIDs @@ -3406,6 +3512,111 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { returnedUIDs, ok := m["default,arulegroup"] assert.True(t, ok) assert.Equal(t, 2, len(returnedUIDs)) + expectedGetNamespaceResponseBody = ` + { + "default":[ + { + "name":"arulegroup", + "interval":"1m", + "rules":[ + { + "annotations": { + "annotation1": "val1" + }, + "expr":"", + "for": "1m", + "labels": { + "label1": "val1" + }, + "grafana_alert":{ + "title":"AlwaysFiring", + "condition":"A", + "data":[ + { + "refId":"A", + "queryType":"", + "relativeTimeRange":{ + "from":18000, + "to":10800 + }, + "datasourceUid":"__expr__", + "model":{ + "expression":"2 + 3 \u003e 1", + "intervalMs":1000, + "maxDataPoints":43200, + "type":"math" + } + } + ], + "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, + "intervalSeconds":60, + "is_paused": false, + "version":3, + "uid":"uid", + "namespace_uid":"nsuid", + "rule_group":"arulegroup", + "no_data_state":"NoData", + "exec_err_state":"Alerting", + "metadata": { + "editor_settings": { + "simplified_query_and_expressions_section": false, + "simplified_notifications_section": false + } + } + } + }, + { + "expr":"", + "for": "0s", + "grafana_alert":{ + "title":"AlwaysFiringButSilenced", + "condition":"A", + "data":[ + { + "refId":"A", + "queryType":"", + "relativeTimeRange":{ + "from":18000, + "to":10800 + }, + "datasourceUid":"__expr__", + "model":{ + "expression":"2 + 3 \u003e 1", + "intervalMs":1000, + "maxDataPoints":43200, + "type":"math" + } + } + ], + "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, + "intervalSeconds":60, + "is_paused": false, + "version":3, + "uid":"uid", + "namespace_uid":"nsuid", + "rule_group":"arulegroup", + "no_data_state":"Alerting", + "exec_err_state":"Alerting", + "metadata": { + "editor_settings": { + "simplified_query_and_expressions_section": false, + "simplified_notifications_section": false + } + } + } + } + ] + } + ] + }` assert.JSONEq(t, expectedGetNamespaceResponseBody, body) } @@ -3525,7 +3736,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { }, "intervalSeconds":60, "is_paused": false, - "version":2, + "version":4, "uid":"uid", "namespace_uid":"nsuid", "rule_group":"arulegroup", @@ -3642,7 +3853,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { }, "intervalSeconds":60, "is_paused":false, - "version":3, + "version":5, "uid":"uid", "namespace_uid":"nsuid", "rule_group":"arulegroup", @@ -3738,7 +3949,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { }, "intervalSeconds":60, "is_paused":false, - "version":3, + "version":5, "uid":"uid", "namespace_uid":"nsuid", "rule_group":"arulegroup", diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index ed5fb7ace1a..5272883af9e 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -687,6 +687,30 @@ func (a apiClient) ExportRulesWithStatus(t *testing.T, params *apimodels.AlertRu return resp.StatusCode, string(b) } +func (a apiClient) GetRuleGroupProvisioning(t *testing.T, folderUID string, groupName string) (apimodels.AlertRuleGroup, int, string) { + t.Helper() + + 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) +} + +func (a apiClient) CreateOrUpdateRuleGroupProvisioning(t *testing.T, group apimodels.AlertRuleGroup) (apimodels.AlertRuleGroup, int, string) { + t.Helper() + + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + err := enc.Encode(group) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/provisioning/folder/%s/rule-groups/%s", a.url, group.FolderUID, group.Title), &buf) + require.NoError(t, err) + req.Header.Add("Content-Type", "application/json") + + return sendRequest[apimodels.AlertRuleGroup](t, req, http.StatusOK) +} + func (a apiClient) SubmitRuleForBacktesting(t *testing.T, config apimodels.BacktestConfig) (int, string) { t.Helper() buf := bytes.Buffer{} From 27ece859e777b61725db108ee6a7c55e35263837 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Mon, 10 Feb 2025 17:37:17 +0200 Subject: [PATCH 463/894] MultiCombobox: Export from grafana/ui (#100368) * MultiCombobox: Export from grafana/ui * Fix typos * Update options styles --- .../src/components/Combobox/MultiCombobox.internal.story.tsx | 5 +++-- .../grafana-ui/src/components/Combobox/MultiCombobox.tsx | 2 +- packages/grafana-ui/src/components/Combobox/ValuePill.tsx | 5 +++-- packages/grafana-ui/src/components/index.ts | 1 + 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx index 36c368291ee..7358119d986 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx @@ -1,6 +1,7 @@ import { action } from '@storybook/addon-actions'; import { useArgs, useEffect, useState } from '@storybook/preview-api'; import type { Meta, StoryFn, StoryObj } from '@storybook/react'; +import { ComponentProps } from 'react'; import { Field } from '../Forms/Field'; @@ -30,7 +31,7 @@ const commonArgs = { export default meta; -type storyArgs = React.ComponentProps; +type storyArgs = ComponentProps; type ManyOptionsArgs = storyArgs & { numberOfOptions?: number }; type Story = StoryObj; @@ -123,7 +124,7 @@ export const AsyncOptionsWithLabels: Story = { return ( (props: MultiComboboxPro const { getSelectedItemProps, getDropdownProps, setSelectedItems, addSelectedItem, removeSelectedItem, reset } = useMultipleSelection({ - selectedItems, // initally selected items, + selectedItems, // initially selected items, onStateChange: ({ type, selectedItems: newSelectedItems }) => { switch (type) { case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace: diff --git a/packages/grafana-ui/src/components/Combobox/ValuePill.tsx b/packages/grafana-ui/src/components/Combobox/ValuePill.tsx index 96809d281b7..caeb3578df4 100644 --- a/packages/grafana-ui/src/components/Combobox/ValuePill.tsx +++ b/packages/grafana-ui/src/components/Combobox/ValuePill.tsx @@ -40,7 +40,6 @@ export const ValuePill = forwardRef( const getValuePillStyles = (theme: GrafanaTheme2, disabled?: boolean) => ({ wrapper: css({ display: 'inline-flex', - gap: theme.spacing(0.5), borderRadius: theme.shape.radius.default, color: theme.colors.text.primary, background: theme.colors.background.secondary, @@ -49,6 +48,7 @@ const getValuePillStyles = (theme: GrafanaTheme2, disabled?: boolean) => ({ fontSize: theme.typography.bodySmall.fontSize, flexShrink: 0, minWidth: '50px', + alignItems: 'center', '&:first-child:has(+ div)': { flexShrink: 1, @@ -59,12 +59,13 @@ const getValuePillStyles = (theme: GrafanaTheme2, disabled?: boolean) => ({ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', + padding: theme.spacing(0, 1, 0, 0.75), }), separator: css({ background: theme.colors.border.weak, width: '2px', - marginLeft: theme.spacing(0.25), height: '100%', + marginRight: theme.spacing(0.5), }), }); diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 8296a00cb70..b62e96627a0 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -244,6 +244,7 @@ export { getSelectStyles } from './Select/getSelectStyles'; export * from './Select/types'; export { Combobox } from './Combobox/Combobox'; +export { MultiCombobox } from './Combobox/MultiCombobox'; export { type ComboboxOption } from './Combobox/types'; export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout'; From 0152f414f0c5824e1261af60759ef69f03deab05 Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Mon, 10 Feb 2025 12:07:51 -0500 Subject: [PATCH 464/894] DS Apiservers: return 404 when receiving a datasource not found error (#100025) * DS Apiservers should return a k8s 404 error * Do not swallow status codes * Updates from initial CR. * Add test for ds apiserver to retunr 404 when a datasource is not found * Didn't intend for a change here --- pkg/registry/apis/datasource/sub_query.go | 17 +++++++++++ .../apis/datasource/sub_query_test.go | 28 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/pkg/registry/apis/datasource/sub_query.go b/pkg/registry/apis/datasource/sub_query.go index 6ab00ac5c78..51f8b90d430 100644 --- a/pkg/registry/apis/datasource/sub_query.go +++ b/pkg/registry/apis/datasource/sub_query.go @@ -2,6 +2,7 @@ package datasource import ( "context" + "errors" "fmt" "net/http" @@ -9,10 +10,14 @@ import ( data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" query_headers "github.com/grafana/grafana/pkg/registry/apis/query" + "github.com/grafana/grafana/pkg/services/datasources" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/registry/rest" "github.com/grafana/grafana/pkg/web" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" ) type subQueryREST struct { @@ -50,9 +55,21 @@ func (r *subQueryREST) NewConnectOptions() (runtime.Object, bool, string) { func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { pluginCtx, err := r.builder.getPluginContext(ctx, name) + if err != nil { + if errors.Is(err, datasources.ErrDataSourceNotFound) { + return nil, k8serrors.NewNotFound( + schema.GroupResource{ + Group: r.builder.connectionResourceInfo.GroupResource().Group, + Resource: r.builder.connectionResourceInfo.GroupResource().Resource, + }, + name, + ) + } + return nil, err } + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { dqr := data.QueryDataRequest{} err := web.Bind(req, &dqr) diff --git a/pkg/registry/apis/datasource/sub_query_test.go b/pkg/registry/apis/datasource/sub_query_test.go index 684f5f25cb1..6b3dc54b970 100644 --- a/pkg/registry/apis/datasource/sub_query_test.go +++ b/pkg/registry/apis/datasource/sub_query_test.go @@ -2,6 +2,7 @@ package datasource import ( "context" + "errors" "fmt" "net/http" "net/http/httptest" @@ -10,8 +11,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/stretchr/testify/require" + k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ) @@ -60,6 +63,26 @@ func TestSubQueryConnect(t *testing.T) { }, *sqr.builder.client.(mockClient).lastCalledWithHeaders) } +func TestSubQueryConnectWhenDatasourceNotFound(t *testing.T) { + sqr := subQueryREST{ + builder: &DataSourceAPIBuilder{ + client: mockClient{ + lastCalledWithHeaders: &map[string]string{}, + }, + datasources: mockDatasources{}, + contextProvider: mockContextProvider{}, + log: log.NewNopLogger(), + }, + } + + mr := mockResponder{} + _, err := sqr.Connect(context.Background(), "dsname-that-does-not-exist", nil, mr) + require.Error(t, err) + var statusErr *k8serrors.StatusError + require.True(t, errors.As(err, &statusErr)) + require.Equal(t, int32(404), statusErr.Status().Code) +} + type mockClient struct { lastCalledWithHeaders *map[string]string } @@ -108,7 +131,10 @@ func (m mockDatasources) List(ctx context.Context) (*v0alpha1.DataSourceConnecti // Return settings (decrypted!) for a specific plugin // This will require "query" permission for the user in context func (m mockDatasources) GetInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) { - return nil, nil + if uid == "dsname" { + return nil, nil + } + return nil, datasources.ErrDataSourceNotFound } type mockContextProvider struct { From 4e48b7557c79c9353715cd9b5c3c6aa2fd4c2cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Mon, 10 Feb 2025 18:13:26 +0100 Subject: [PATCH 465/894] CloudWatch: Track Logs Insights query language (#100254) Co-authored-by: Kevin Yu --- .../__mocks__/dashboardOnLoadedEvent.ts | 44 ++++++++++++++----- .../components/CheatSheet/LogsCheatSheet.tsx | 38 +++++++++------- .../datasource/cloudwatch/tracking.test.ts | 5 ++- .../plugins/datasource/cloudwatch/tracking.ts | 25 +++++++++++ 4 files changed, 83 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/dashboardOnLoadedEvent.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/dashboardOnLoadedEvent.ts index 8ba9e8149c6..ebe3dcaf1f0 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/dashboardOnLoadedEvent.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/dashboardOnLoadedEvent.ts @@ -2,6 +2,21 @@ import { DashboardLoadedEvent } from '@grafana/data'; import { CloudWatchQuery } from '../types'; +const baseLogsQuery = { + datasource: { + type: 'cloudwatch', + uid: 'P7DC3E4760CFAC4AP', + }, + expression: 'fields @timestamp, @message | sort @timestamp desc | limit 300 ', + id: '', + logGroupNames: ['/aws/lambda/hello-world', '/aws/sagemaker/Endpoints/test', '/aws/sagemaker/test'], + namespace: '', + queryMode: 'Logs', + refId: 'A', + region: 'default', + statsGroups: [], +}; + export const CloudWatchDashboardLoadedEvent = new DashboardLoadedEvent({ dashboardId: 'dashboard123', orgId: 1, @@ -460,18 +475,23 @@ export const CloudWatchDashboardLoadedEvent = new DashboardLoadedEvent({ statistic: 'Average', }, { - datasource: { - type: 'cloudwatch', - uid: 'P7DC3E4760CFAC4AP', - }, - expression: 'fields @timestamp, @message | sort @timestamp desc | limit 300 ', - id: '', - logGroupNames: ['/aws/lambda/hello-world', '/aws/sagemaker/Endpoints/test', '/aws/sagemaker/test'], - namespace: '', - queryMode: 'Logs', - refId: 'A', - region: 'default', - statsGroups: [], + ...baseLogsQuery, + }, + { + ...baseLogsQuery, + queryLanguage: 'PPL', + }, + { + ...baseLogsQuery, + queryLanguage: 'PPL', + }, + { + ...baseLogsQuery, + queryLanguage: 'SQL', + }, + { + ...baseLogsQuery, + queryLanguage: 'CWLI', }, { alias: '', diff --git a/public/app/plugins/datasource/cloudwatch/components/CheatSheet/LogsCheatSheet.tsx b/public/app/plugins/datasource/cloudwatch/components/CheatSheet/LogsCheatSheet.tsx index 3c3448c419d..7f96528011e 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CheatSheet/LogsCheatSheet.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/CheatSheet/LogsCheatSheet.tsx @@ -6,10 +6,12 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Collapse, useStyles2, Text } from '@grafana/ui'; import { flattenTokens } from '@grafana/ui/src/slate-plugins/slate-prism'; +import { trackSampleQuerySelection } from '../../tracking'; import { CloudWatchLogsQuery, CloudWatchQuery, LogsQueryLanguage } from '../../types'; import * as sampleQueries from './sampleQueries'; import { cwliTokenizer, pplTokenizer, sqlTokenizer } from './tokenizer'; + interface QueryExample { category: string; examples: sampleQueries.SampleQuery[]; @@ -89,11 +91,26 @@ type Props = { query: CloudWatchQuery; }; const isLogsQuery = (query: CloudWatchQuery): query is CloudWatchLogsQuery => query.queryMode === 'Logs'; + const LogsCheatSheet = (props: Props) => { const styles = useStyles2(getStyles); - const queryLanugage: LogsQueryLanguage = + const queryLanguage: LogsQueryLanguage = (isLogsQuery(props.query) && props.query.queryLanguage) || LogsQueryLanguage.CWLI; + const onClickExample = (query: sampleQueries.SampleQuery, queryCategory: string) => { + props.onClickExample({ + ...props.query, + refId: props.query.refId ?? 'A', + expression: query.expr[queryLanguage], + queryMode: 'Logs', + region: props.query.region, + id: props.query.refId ?? 'A', + logGroupNames: 'logGroupNames' in props.query ? props.query.logGroupNames : [], + logGroups: 'logGroups' in props.query ? props.query.logGroups : [], + }); + trackSampleQuerySelection({ queryLanguage, queryCategory }); + }; + return (
    @@ -106,7 +123,7 @@ const LogsCheatSheet = (props: Props) => {
    {query.examples.map((item, j) => ( <> - {item.expr[queryLanugage] && ( + {item.expr[queryLanguage] && ( <> {item.title} @@ -114,21 +131,10 @@ const LogsCheatSheet = (props: Props) => { )} diff --git a/public/app/plugins/datasource/cloudwatch/tracking.test.ts b/public/app/plugins/datasource/cloudwatch/tracking.test.ts index 670ec03ed75..ea54f98558b 100644 --- a/public/app/plugins/datasource/cloudwatch/tracking.test.ts +++ b/public/app/plugins/datasource/cloudwatch/tracking.test.ts @@ -27,7 +27,10 @@ describe('onDashboardLoadedHandler', () => { dashboard_id: 'dashboard123', grafana_version: 'v9.0.0', org_id: 1, - logs_queries_count: 1, + logs_queries_count: 5, + logs_cwli_queries_count: 2, + logs_sql_queries_count: 1, + logs_ppl_queries_count: 2, metrics_queries_count: 21, metrics_query_builder_count: 3, metrics_query_code_count: 4, diff --git a/public/app/plugins/datasource/cloudwatch/tracking.ts b/public/app/plugins/datasource/cloudwatch/tracking.ts index 3607b3ef99a..4bf91191f1e 100644 --- a/public/app/plugins/datasource/cloudwatch/tracking.ts +++ b/public/app/plugins/datasource/cloudwatch/tracking.ts @@ -8,6 +8,7 @@ import { CloudWatchLogsQuery, CloudWatchMetricsQuery, CloudWatchQuery, + LogsQueryLanguage, MetricEditorMode, MetricQueryType, } from './types'; @@ -21,6 +22,15 @@ type CloudWatchOnDashboardLoadedTrackingEvent = { /* The number of CloudWatch logs queries present in the dashboard*/ logs_queries_count: number; + /* The number of Logs queries that use Logs Insights query language */ + logs_cwli_queries_count: number; + + /* The number of Logs queries that use SQL language */ + logs_sql_queries_count: number; + + /* The number of Logs queries that use PPL language */ + logs_ppl_queries_count: number; + /* The number of CloudWatch metrics queries present in the dashboard*/ metrics_queries_count: number; @@ -88,6 +98,11 @@ export const onDashboardLoadedHandler = ({ dashboard_id: dashboardId, org_id: orgId, logs_queries_count: logsQueries?.length, + logs_cwli_queries_count: logsQueries?.filter( + (q) => !q.queryLanguage || q.queryLanguage === LogsQueryLanguage.CWLI + ).length, + logs_sql_queries_count: logsQueries?.filter((q) => q.queryLanguage === LogsQueryLanguage.SQL).length, + logs_ppl_queries_count: logsQueries?.filter((q) => q.queryLanguage === LogsQueryLanguage.PPL).length, metrics_queries_count: metricsQueries?.length, metrics_search_count: 0, metrics_search_builder_count: 0, @@ -124,5 +139,15 @@ export const onDashboardLoadedHandler = ({ } }; +type SampleQueryTrackingEvent = { + queryLanguage: LogsQueryLanguage; + queryCategory: string; +}; + +export const trackSampleQuerySelection = (props: SampleQueryTrackingEvent) => { + const { queryLanguage, queryCategory } = props; + reportInteraction('cloudwatch-logs-cheat-sheet-query-clicked', { queryLanguage, queryCategory }); +}; + const isMetricSearchBuilder = (q: CloudWatchMetricsQuery) => Boolean(q.metricQueryType === MetricQueryType.Search && q.metricEditorMode === MetricEditorMode.Builder); From 7092fd269d79e65a20cdb17ee43fcc78dd61a56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Mon, 10 Feb 2025 18:33:36 +0100 Subject: [PATCH 466/894] Elasticsearch: Remove frontend testDatasource method (#99894) --- pkg/tsdb/elasticsearch/client/client.go | 2 +- .../elasticsearch/client/index_pattern.go | 2 +- .../client/index_pattern_test.go | 2 +- pkg/tsdb/elasticsearch/healthcheck.go | 170 ++++++++++++++---- pkg/tsdb/elasticsearch/healthcheck_test.go | 100 ++++++++--- .../datasource/elasticsearch/datasource.ts | 35 +--- 6 files changed, 219 insertions(+), 92 deletions(-) diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index 6bffc08ba72..73d03dcba7b 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -59,7 +59,7 @@ type Client interface { var NewClient = func(ctx context.Context, ds *DatasourceInfo, logger log.Logger) (Client, error) { logger = logger.FromContext(ctx).With("entity", "client") - ip, err := newIndexPattern(ds.Interval, ds.Database) + ip, err := NewIndexPattern(ds.Interval, ds.Database) if err != nil { logger.Error("Failed creating index pattern", "error", err, "interval", ds.Interval, "index", ds.Database) return nil, err diff --git a/pkg/tsdb/elasticsearch/client/index_pattern.go b/pkg/tsdb/elasticsearch/client/index_pattern.go index a7a7a6096f8..ff01d759c5c 100644 --- a/pkg/tsdb/elasticsearch/client/index_pattern.go +++ b/pkg/tsdb/elasticsearch/client/index_pattern.go @@ -73,7 +73,7 @@ type IndexPattern interface { GetIndices(timeRange backend.TimeRange) ([]string, error) } -var newIndexPattern = func(interval string, pattern string) (IndexPattern, error) { +var NewIndexPattern = func(interval string, pattern string) (IndexPattern, error) { if interval == noInterval { return &staticIndexPattern{indexName: pattern}, nil } diff --git a/pkg/tsdb/elasticsearch/client/index_pattern_test.go b/pkg/tsdb/elasticsearch/client/index_pattern_test.go index a309c07a472..a30408cf4a8 100644 --- a/pkg/tsdb/elasticsearch/client/index_pattern_test.go +++ b/pkg/tsdb/elasticsearch/client/index_pattern_test.go @@ -290,7 +290,7 @@ func TestIndexPattern(t *testing.T) { func indexPatternScenario(t *testing.T, interval string, pattern string, timeRange backend.TimeRange, fn func(indices []string)) { testName := fmt.Sprintf("Index pattern (interval=%s, index=%s", interval, pattern) t.Run(testName, func(t *testing.T) { - ip, err := newIndexPattern(interval, pattern) + ip, err := NewIndexPattern(interval, pattern) require.NoError(t, err) require.NotNil(t, ip) indices, err := ip.GetIndices(timeRange) diff --git a/pkg/tsdb/elasticsearch/healthcheck.go b/pkg/tsdb/elasticsearch/healthcheck.go index 6218aea2754..3ef6f524a7b 100644 --- a/pkg/tsdb/elasticsearch/healthcheck.go +++ b/pkg/tsdb/elasticsearch/healthcheck.go @@ -8,12 +8,15 @@ import ( "net/http" "net/url" "path" + "strings" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) +const ErrorBodyMaxSize = 200 + func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { logger := s.logger.FromContext(ctx) @@ -22,59 +25,56 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque logger.Error("Failed to get data source info", "error", err) return &backend.CheckHealthResult{ Status: backend.HealthStatusUnknown, - Message: "Failed to get data source info", - }, err + Message: "Health check failed: Failed to get data source info", + }, nil } - esUrl, err := url.Parse(ds.URL) + healthStatusUrl, err := url.Parse(ds.URL) if err != nil { - logger.Error("Failed to parse data source URL", "error", err, "url", ds.URL) + logger.Error("Failed to parse data source URL", "error", err) return &backend.CheckHealthResult{ Status: backend.HealthStatusUnknown, Message: "Failed to parse data source URL", - }, err + }, nil } - esUrl.Path = path.Join(esUrl.Path, "_cluster/health") - esUrl.RawQuery = "wait_for_status=yellow" + // check that ES is healthy + healthStatusUrl.Path = path.Join(healthStatusUrl.Path, "_cluster/health") + healthStatusUrl.RawQuery = "wait_for_status=yellow" - request, err := http.NewRequestWithContext(ctx, "GET", esUrl.String(), nil) + request, err := http.NewRequestWithContext(ctx, http.MethodGet, healthStatusUrl.String(), nil) if err != nil { - logger.Error("Failed to create request", "error", err, "url", esUrl.String()) + logger.Error("Failed to create request", "error", err, "url", healthStatusUrl.String()) return &backend.CheckHealthResult{ Status: backend.HealthStatusUnknown, Message: "Failed to create request", - }, err + }, nil } start := time.Now() - logger.Debug("Sending healthcheck request to Elasticsearch", "url", esUrl.String()) + logger.Debug("Sending healthcheck request to Elasticsearch", "url", healthStatusUrl.String()) response, err := ds.HTTPClient.Do(request) if err != nil { - logger.Error("Failed to do healthcheck request", "error", err, "url", esUrl.String()) - if backend.IsDownstreamHTTPError(err) { - err = errorsource.DownstreamError(err, false) - } + logger.Error("Failed to connect to Elasticsearch", "error", err, "url", healthStatusUrl.String()) return &backend.CheckHealthResult{ - Status: backend.HealthStatusUnknown, - Message: "Failed to do healthcheck request", - }, err + Status: backend.HealthStatusError, + Message: "Health check failed: Failed to connect to Elasticsearch", + }, nil } if response.StatusCode == http.StatusRequestTimeout { return &backend.CheckHealthResult{ Status: backend.HealthStatusError, - Message: "Elasticsearch data source is not healthy", + Message: "Health check failed: Elasticsearch data source is not healthy. Request timed out", }, nil } if response.StatusCode >= 400 { - errWithSource := errorsource.SourceError(backend.ErrorSourceFromHTTPStatus(response.StatusCode), fmt.Errorf("unexpected status code: %d", response.StatusCode), false) return &backend.CheckHealthResult{ Status: backend.HealthStatusError, - Message: fmt.Sprintf("Elasticsearch data source is not healthy. Status: %s", response.Status), - }, errWithSource + Message: fmt.Sprintf("Health check failed: Elasticsearch data source is not healthy. Status: %s", response.Status), + }, nil } logger.Info("Response received from Elasticsearch", "statusCode", response.StatusCode, "status", "ok", "duration", time.Since(start)) @@ -90,31 +90,131 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque logger.Error("Error reading response body bytes", "error", err) return &backend.CheckHealthResult{ Status: backend.HealthStatusUnknown, - Message: "Failed to read response", - }, err + Message: "Health check failed: Failed to read response", + }, nil } jsonData := map[string]any{} err = json.Unmarshal(body, &jsonData) if err != nil { - logger.Error("Error during json unmarshal of the body", "error", err) + truncatedBody := string(body) + if len(truncatedBody) > ErrorBodyMaxSize { + truncatedBody = truncatedBody[:ErrorBodyMaxSize] + "..." + } return &backend.CheckHealthResult{ Status: backend.HealthStatusUnknown, - Message: "Failed to unmarshal response", - }, err + Message: fmt.Sprintf("Health check failed: Failed to parse response from Elasticsearch. Response received: %s", truncatedBody), + }, nil } - status := backend.HealthStatusOk - message := "Elasticsearch data source is healthy" - if jsonData["status"] == "red" { - status = backend.HealthStatusError - message = "Elasticsearch data source is not healthy" + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "Health check failed: Elasticsearch data source is not healthy", + }, nil + } + + successMessage := "Elasticsearch data source is healthy." + indexWarningMessage := "" + + // validate index and time field + cfg := backend.GrafanaConfigFromContext(ctx) + crossClusterSearchEnabled := cfg.FeatureToggles().IsEnabled("elasticsearchCrossClusterSearch") + + if crossClusterSearchEnabled { + message, level := validateIndex(ctx, ds) + if level == "warning" { + indexWarningMessage = message + } + if level == "error" { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: message, + }, nil + } + } + + if indexWarningMessage != "" { + successMessage = fmt.Sprintf("%s Warning: %s", successMessage, indexWarningMessage) } return &backend.CheckHealthResult{ - Status: status, - Message: message, + Status: backend.HealthStatusOk, + Message: successMessage, }, nil } + +func validateIndex(ctx context.Context, ds *es.DatasourceInfo) (message string, level string) { + // validate that the index exist and has date field + ip, err := es.NewIndexPattern(ds.Interval, ds.Database) + if err != nil { + return fmt.Sprintf("Failed to get build index pattern: %s", err), "error" + } + + indices, err := ip.GetIndices(backend.TimeRange{ + From: time.Now().UTC(), + To: time.Now().UTC(), + }) + if err != nil { + return fmt.Sprintf("Failed to get index pattern: %s", err), "error" + } + + indexList := strings.Join(indices, ",") + + validateUrl := fmt.Sprintf("%s/%s/_field_caps?fields=%s", ds.URL, indexList, ds.ConfiguredFields.TimeField) + if indexList == "" || strings.Replace(indexList, ",", "", -1) == "" { + validateUrl = fmt.Sprintf("%s/_field_caps?fields=%s", ds.URL, ds.ConfiguredFields.TimeField) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, validateUrl, nil) + if err != nil { + return fmt.Sprint("Failed to create request", "error", err, "url", validateUrl), "error" + } + response, err := ds.HTTPClient.Do(request) + if err != nil { + return fmt.Sprint("Failed to fetch field capabilities", "error", err, "url", validateUrl), "error" + } + defer func() { + if err := response.Body.Close(); err != nil { + backend.Logger.Warn("Failed to close response body", "error", err) + } + }() + + fieldCaps := map[string]any{} + body, err := io.ReadAll(response.Body) + if err != nil { + return "Could not read response body while checking time field", "error" + } + err = json.Unmarshal(body, &fieldCaps) + if err != nil { + return "Failed to unmarshal field capabilities response", "error" + } + if fieldCaps["error"] != nil { + if errorMessage, ok := fieldCaps["error"].(map[string]any)["reason"].(string); ok { + return fmt.Sprintf("Error validating index: %s", errorMessage), "warning" + } else { + return "Error validating index", "warning" + } + } + + fields, ok := fieldCaps["fields"].(map[string]any) + if !ok { + return "Failed to parse fields from response", "error" + } + if len(fields) == 0 { + return fmt.Sprintf("Could not find field %s in index", ds.ConfiguredFields.TimeField), "warning" + } + + timeFieldInfo, ok := fields[ds.ConfiguredFields.TimeField].(map[string]any) + if !ok { + return "Failed to parse time field info from response", "error" + } + + dateTypeField, ok := timeFieldInfo["date"].(map[string]any) + if !ok || dateTypeField == nil { + return fmt.Sprintf("Could not find time field '%s' with type date in index", ds.ConfiguredFields.TimeField), "warning" + } + + return "", "" +} diff --git a/pkg/tsdb/elasticsearch/healthcheck_test.go b/pkg/tsdb/elasticsearch/healthcheck_test.go index 93b6c1252cd..b3f6dc97c93 100644 --- a/pkg/tsdb/elasticsearch/healthcheck_test.go +++ b/pkg/tsdb/elasticsearch/healthcheck_test.go @@ -11,74 +11,132 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" "github.com/stretchr/testify/assert" ) +var mockedCfg = backend.WithGrafanaConfig(context.Background(), backend.NewGrafanaCfg(map[string]string{featuretoggles.EnabledFeatures: "elasticsearchCrossClusterSearch"})) + func Test_Healthcheck_OK(t *testing.T) { - service := GetMockService(http.StatusOK, "200 OK") + service := GetMockService(http.StatusOK, "200 OK", `{"status":"green"}`, `{"fields":{"timestamp":{"date":{"metadata_field":true}}}}`) res, _ := service.CheckHealth(context.Background(), &backend.CheckHealthRequest{ PluginContext: backend.PluginContext{}, Headers: nil, }) assert.Equal(t, backend.HealthStatusOk, res.Status) - assert.Equal(t, "Elasticsearch data source is healthy", res.Message) + assert.Equal(t, "Elasticsearch data source is healthy.", res.Message) } func Test_Healthcheck_Timeout(t *testing.T) { - service := GetMockService(http.StatusRequestTimeout, "408 Request Timeout") + service := GetMockService(http.StatusRequestTimeout, "408 Request Timeout", `{"status":"red"}`, `{"fields":{"timestamp":{"date":{"metadata_field":true}}}}`) res, _ := service.CheckHealth(context.Background(), &backend.CheckHealthRequest{ PluginContext: backend.PluginContext{}, Headers: nil, }) assert.Equal(t, backend.HealthStatusError, res.Status) - assert.Equal(t, "Elasticsearch data source is not healthy", res.Message) + assert.Equal(t, "Health check failed: Elasticsearch data source is not healthy. Request timed out", res.Message) } func Test_Healthcheck_Error(t *testing.T) { - service := GetMockService(http.StatusBadGateway, "502 Bad Gateway") + service := GetMockService(http.StatusBadGateway, "502 Bad Gateway", `{"status":"red"}`, `{"fields":{"timestamp":{"date":{"metadata_field":true}}}}`) res, _ := service.CheckHealth(context.Background(), &backend.CheckHealthRequest{ PluginContext: backend.PluginContext{}, Headers: nil, }) assert.Equal(t, backend.HealthStatusError, res.Status) - assert.Equal(t, "Elasticsearch data source is not healthy. Status: 502 Bad Gateway", res.Message) + assert.Equal(t, "Health check failed: Elasticsearch data source is not healthy. Status: 502 Bad Gateway", res.Message) +} + +func Test_validateIndex_Warning_ErrorValidatingIndex(t *testing.T) { + service := GetMockService(http.StatusOK, "200 OK", `{"status":"green"}`, `{"error":{"reason":"index_not_found"}}`) + res, _ := service.CheckHealth(mockedCfg, &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{}, + Headers: nil, + }) + assert.Equal(t, backend.HealthStatusOk, res.Status) + assert.Equal(t, "Elasticsearch data source is healthy. Warning: Error validating index: index_not_found", res.Message) +} + +func Test_validateIndex_Warning_WrongTimestampType(t *testing.T) { + service := GetMockService(http.StatusOK, "200 OK", `{"status":"green"}`, `{"fields":{"timestamp":{"float":{"metadata_field":true}}}}`) + res, _ := service.CheckHealth(mockedCfg, &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{}, + Headers: nil, + }) + assert.Equal(t, backend.HealthStatusOk, res.Status) + assert.Equal(t, "Elasticsearch data source is healthy. Warning: Could not find time field 'timestamp' with type date in index", res.Message) +} +func Test_validateIndex_Error_FailedToUnmarshalValidateResponse(t *testing.T) { + service := GetMockService(http.StatusOK, "200 OK", `{"status":"green"}`, `\\\///{"fields":null}"`) + res, _ := service.CheckHealth(mockedCfg, &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{}, + Headers: nil, + }) + assert.Equal(t, backend.HealthStatusError, res.Status) + assert.Equal(t, "Failed to unmarshal field capabilities response", res.Message) +} +func Test_validateIndex_Success_SuccessValidatingIndex(t *testing.T) { + service := GetMockService(http.StatusOK, "200 OK", `{"status":"green"}`, `{"fields":{"timestamp":{"date":{"metadata_field":true}}}}`) + res, _ := service.CheckHealth(mockedCfg, &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{}, + Headers: nil, + }) + assert.Equal(t, backend.HealthStatusOk, res.Status) + assert.Equal(t, "Elasticsearch data source is healthy.", res.Message) } type FakeRoundTripper struct { - statusCode int - status string + statusCode int + status string + index int + elasticSearchResponse string + fieldCapsResponse string } func (fakeRoundTripper *FakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { var res *http.Response - if fakeRoundTripper.statusCode == http.StatusOK { + if fakeRoundTripper.index == 0 { + if fakeRoundTripper.statusCode == http.StatusOK { + res = &http.Response{ + StatusCode: http.StatusOK, + Status: "200 OK", + Body: io.NopCloser(bytes.NewBufferString(fakeRoundTripper.elasticSearchResponse)), + } + } else { + res = &http.Response{ + StatusCode: fakeRoundTripper.statusCode, + Status: fakeRoundTripper.status, + Body: io.NopCloser(bytes.NewBufferString(fakeRoundTripper.elasticSearchResponse)), + } + } + fakeRoundTripper.index++ + } else { res = &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", - Body: io.NopCloser(bytes.NewBufferString("{\"status\":\"green\"}")), - } - } else { - res = &http.Response{ - StatusCode: fakeRoundTripper.statusCode, - Status: fakeRoundTripper.status, - Body: io.NopCloser(bytes.NewBufferString("{\"status\":\"red\"}")), + Body: io.NopCloser(bytes.NewBufferString(fakeRoundTripper.fieldCapsResponse)), } } return res, nil } type FakeInstanceManager struct { - statusCode int - status string + statusCode int + status string + elasticSearchResponse string + fieldCapsResponse string } func (fakeInstanceManager *FakeInstanceManager) Get(tx context.Context, pluginContext backend.PluginContext) (instancemgmt.Instance, error) { httpClient, _ := httpclient.New(httpclient.Options{}) - httpClient.Transport = &FakeRoundTripper{statusCode: fakeInstanceManager.statusCode, status: fakeInstanceManager.status} + httpClient.Transport = &FakeRoundTripper{statusCode: fakeInstanceManager.statusCode, status: fakeInstanceManager.status, elasticSearchResponse: fakeInstanceManager.elasticSearchResponse, fieldCapsResponse: fakeInstanceManager.fieldCapsResponse, index: 0} return es.DatasourceInfo{ HTTPClient: httpClient, + ConfiguredFields: es.ConfiguredFields{ + TimeField: "timestamp", + }, }, nil } @@ -86,9 +144,9 @@ func (*FakeInstanceManager) Do(_ context.Context, _ backend.PluginContext, _ ins return nil } -func GetMockService(statusCode int, status string) *Service { +func GetMockService(statusCode int, status string, elasticSearchResponse string, fieldCapsResponse string) *Service { return &Service{ - im: &FakeInstanceManager{statusCode: statusCode, status: status}, + im: &FakeInstanceManager{statusCode: statusCode, status: status, elasticSearchResponse: elasticSearchResponse, fieldCapsResponse: fieldCapsResponse}, logger: log.New(), } } diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 99b68fb3aa8..34d22b1e33a 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -1,4 +1,4 @@ -import { cloneDeep, first as _first, isNumber, isString, map as _map, find, isObject } from 'lodash'; +import { cloneDeep, first as _first, isNumber, isString, map as _map, isObject } from 'lodash'; import { from, generate, lastValueFrom, Observable, of } from 'rxjs'; import { catchError, first, map, mergeMap, skipWhile, throwIfEmpty, tap } from 'rxjs/operators'; import { SemVer } from 'semver'; @@ -83,7 +83,7 @@ import { isElasticsearchResponseWithHits, ElasticsearchHits, } from './types'; -import { getScriptValue, isSupportedVersion, isTimeSeriesQuery, unsupportedVersionMessage } from './utils'; +import { getScriptValue, isTimeSeriesQuery } from './utils'; export const REF_ID_STARTER_LOG_VOLUME = 'log-volume-'; export const REF_ID_STARTER_LOG_SAMPLE = 'log-sample-'; @@ -441,37 +441,6 @@ export class ElasticDatasource return queries.map((q) => this.applyTemplateVariables(q, scopedVars, filters)); } - /** - * @todo Remove as we have health checks in the backend - */ - async testDatasource() { - // we explicitly ask for uncached, "fresh" data here - const dbVersion = await this.getDatabaseVersion(false); - // if we are not able to determine the elastic-version, we assume it is a good version. - const isSupported = dbVersion != null ? isSupportedVersion(dbVersion) : true; - const versionMessage = isSupported ? '' : `WARNING: ${unsupportedVersionMessage} `; - // validate that the index exist and has date field - return lastValueFrom( - this.getFields(['date']).pipe( - mergeMap((dateFields) => { - const timeField = find(dateFields, { text: this.timeField }); - if (!timeField) { - return of({ - status: 'error', - message: 'No date field named ' + this.timeField + ' found', - }); - } - return of({ status: 'success', message: `${versionMessage}Data source successfully connected.` }); - }), - catchError((err) => { - const infoInParentheses = err.message ? ` (${err.message})` : ''; - const message = `Unable to connect with Elasticsearch${infoInParentheses}. Please check the server logs for more details.`; - return of({ status: 'error', message }); - }) - ) - ); - } - // Private method used in `getTerms` to get the header for the Elasticsearch query private getQueryHeader(searchType: string, timeFrom?: DateTime, timeTo?: DateTime): string { const queryHeader = { From 63216a3e6eed561ce042553ded93501095e077db Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:19:44 -0500 Subject: [PATCH 467/894] Docs: Vale fixes (#100277) --- .../variables/add-template-variables/index.md | 124 ++++++++++-------- 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/docs/sources/dashboards/variables/add-template-variables/index.md b/docs/sources/dashboards/variables/add-template-variables/index.md index fa4e49ef5a4..54dc20e2d47 100644 --- a/docs/sources/dashboards/variables/add-template-variables/index.md +++ b/docs/sources/dashboards/variables/add-template-variables/index.md @@ -89,6 +89,8 @@ refs: # Add variables + + The following table lists the types of variables shipped with Grafana. | Variable type | Description | @@ -134,11 +136,13 @@ To create a variable, follow these steps: - [Interval](#add-an-interval-variable) - [Ad hoc filters](#add-ad-hoc-filters) + + ## Add a query variable Query variables enable you to write a data source query that can return a list of metric names, tag values, or keys. For example, a query variable might return a list of server names, sensor IDs, or data centers. The variable values change as they dynamically fetch options with a data source query. -Query variables are generally only supported for strings. If your query returns numbers or any other data type, you might need to convert them to strings in order to use them as variables. For the Azure data source, for example, you can use the [tostring](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/tostringfunction) function for this purpose. +Query variables are generally only supported for strings. If your query returns numbers or any other data type, you might need to convert them to strings to use them as variables. For the Azure data source, for example, you can use the [`tostring`](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/tostringfunction) function for this purpose. Query expressions can contain references to other variables and in effect create linked variables. Grafana detects this and automatically refreshes a variable when one of its linked variables change. @@ -168,7 +172,7 @@ Query expressions are different for each data source. For more information, refe - Some data sources let you provide custom "display names" for the values. For instance, the PostgreSQL, MySQL, and Microsoft SQL Server plugins handle this by looking for fields named `__text` and `__value` in the result. Other data sources may look for `text` and `value` or use a different approach. Always remember to double-check the documentation for the data source. - If you need more room in a single input field query editor, then hover your cursor over the lines in the lower right corner of the field and drag downward to expand. -1. (Optional) In the **Regex** field, type a regex expression to filter or capture specific parts of the names returned by your data source query. To see examples, refer to [Filter variables with regex](#filter-variables-with-regex). +1. (Optional) In the **Regex** field, type a regular expression to filter or capture specific parts of the names returned by your data source query. To see examples, refer to [Filter variables with a regular expression](#filter-variables-with-regex). 1. In the **Sort** drop-down list, select the sort order for values to be displayed in the dropdown list. The default option, **Disabled**, means that the order of options returned by your data source query is used. 1. Under **Refresh**, select when the variable should update options: @@ -240,7 +244,7 @@ _Data source_ variables enable you to quickly change the data source for an enti 1. [Enter general options](#enter-general-options). 1. Under the **Data source options** section of the page, in the **Type** drop-down list, select the target data source for the variable. -1. (Optional) In **Instance name filter**, enter a regex filter for which data source instances to choose from in the variable value drop-down list. +1. (Optional) In **Instance name filter**, enter a regular expression filter for which data source instances to choose from in the variable value drop-down list. Leave this field empty to display all instances. @@ -289,6 +293,9 @@ The following example shows a more complex Graphite example, from the [Graphite groupByNode(summarize(movingAverage(apps.$app.$server.counters.requests.count, 5), '$interval', 'sum', false), 2, 'sum') ``` + + + ## Add ad hoc filters _Ad hoc filters_ are one of the most complex and flexible variable options available. @@ -298,10 +305,10 @@ Ad hoc filters let you add label/value filters that are automatically added to a Unlike other variables, you don't use ad hoc filters in queries. Instead, you use ad hoc filters to write filters for existing queries. -{{% admonition type="note" %}} +{{< admonition type="note" >}} Not all data sources support ad hoc filters. Examples of those that do include Prometheus, Loki, InfluxDB, and Elasticsearch. -{{% /admonition %}} +{{< /admonition >}} To create an ad hoc filter, follow these steps: @@ -318,6 +325,9 @@ To create an ad hoc filter, follow these steps: Now you can [filter data on the dashboard](ref:filter-dashboard). + + + ## Configure variable selection options **Selection Options** are a feature you can use to manage variable option selections. All selection options are optional, and they are off by default. @@ -326,9 +336,9 @@ Now you can [filter data on the dashboard](ref:filter-dashboard). Interpolating a variable with multiple values selected is tricky as it is not straight forward how to format the multiple values into a string that is valid in the given context where the variable is used. Grafana tries to solve this by allowing each data source plugin to inform the templating interpolation engine what format to use for multiple values. -{{% admonition type="note" %}} +{{< admonition type="note" >}} The **Custom all value** option on the variable must be blank for Grafana to format all values into a single string. If it is left blank, then Grafana concatenates (adds together) all the values in the query. Something like `value1,value2,value3`. If a custom `all` value is used, then instead the value is something like `*` or `all`. -{{% /admonition %}} +{{< /admonition >}} #### Multi-value variables with a Graphite data source @@ -336,17 +346,17 @@ Graphite uses glob expressions. A variable with multiple values would, in this c #### Multi-value variables with a Prometheus or InfluxDB data source -InfluxDB and Prometheus use regex expressions, so the same variable would be interpolated as `(host1|host2|host3)`. Every value would also be regex escaped. If not, a value with a regex control character would break the regex expression. +InfluxDB and Prometheus use regular expressions, so the same variable would be interpolated as `(host1|host2|host3)`. Every value would also be regular expression escaped. If not, a value with a regular expression control character would break the regular expression. #### Multi-value variables with an Elastic data source -Elasticsearch uses lucene query syntax, so the same variable would be formatted as `("host1" OR "host2" OR "host3")`. In this case, every value must be escaped so that the value only contains lucene control words and quotation marks. +Elasticsearch uses Lucene query syntax, so the same variable would be formatted as `("host1" OR "host2" OR "host3")`. In this case, every value must be escaped so that the value only contains Lucene control words and quotation marks. #### Troubleshoot multi-value variables -Automatic escaping and formatting can cause problems and it can be tricky to grasp the logic behind it. Especially for InfluxDB and Prometheus where the use of regex syntax requires that the variable is used in regex operator context. +Automatic escaping and formatting can cause problems and it can be tricky to grasp the logic behind it. Especially for InfluxDB and Prometheus where the use of regular expression syntax requires that the variable is used in regular expression operator context. -If you do not want Grafana to do this automatic regex escaping and formatting, then you must do one of the following: +If you do not want Grafana to do this automatic regular expression escaping and formatting, then you must do one of the following: - Turn off the **Multi-value** or **Include All option** options. - Use the [raw variable format](ref:raw-variable-format). @@ -359,28 +369,28 @@ Grafana adds an `All` option to the variable dropdown list. If a user selects th This option is only visible if the **Include All option** is selected. -Enter regex, globs, or lucene syntax in the **Custom all value** field to define the value of the `All` option. +Enter regular expressions, globs, or Lucene syntax in the **Custom all value** field to define the value of the `All` option. -By default the `All` value includes all options in combined expression. This can become very long and can have performance problems. Sometimes it can be better to specify a custom all value, like a wildcard regex. +By default the `All` value includes all options in combined expression. This can become very long and can have performance problems. Sometimes it can be better to specify a custom all value, like a wildcard regular expression. -In order to have custom regex, globs, or lucene syntax in the **Custom all value** option, it is never escaped so you have to think about what is a valid value for your data source. +In order to have custom regular expression, globs, or Lucene syntax in the **Custom all value** option, it is never escaped so you have to think about what is a valid value for your data source. ## Global variables Grafana has global built-in variables that can be used in expressions in the query editor. This topic lists them in alphabetical order and defines them. These variables are useful in queries, dashboard links, panel links, and data links. -### $\_\_dashboard +### `$__dashboard` This variable is the name of the current dashboard. -### $\_\_from and $\_\_to +### `$__from` and `$__to` Grafana has two built-in time range variables: `$__from` and `$__to`. They are currently always interpolated as epoch milliseconds by default, but you can control date formatting. | Syntax | Example result | Description | | ------------------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `${__from}` | 1594671549254 | Unix millisecond epoch | -| `${__from:date}` | 2020-07-13T20:19:09.254Z | No args, defaults to ISO 8601/RFC 3339 | +| `${__from:date}` | 2020-07-13T20:19:09.254Z | No arguments, defaults to ISO 8601/RFC 3339 | | `${__from:date:iso}` | 2020-07-13T20:19:09.254Z | ISO 8601/RFC 3339 | | `${__from:date:seconds}` | 1594671549 | Unix seconds epoch | | `${__from:date:YYYY-MM}` | 2020-07 | Any custom [date format](https://momentjs.com/docs/#/displaying/) that does not include the `:` character. Uses browser time. Use `:date` or `:date:iso` for UTC | @@ -389,7 +399,7 @@ The syntax above also works with `${__to}`. You can use this variable in URLs, as well. For example, you can send a user to a dashboard that shows a time range from six hours ago until now: https://play.grafana.org/d/000000012/grafana-play-home?viewPanel=2&orgId=1?from=now-6h&to=now -### $\_\_interval +### `$__interval` You can use the `$__interval` variable as a parameter to group by time (for InfluxDB, MySQL, Postgres, MSSQL), Date histogram interval (for Elasticsearch), or as a _summarize_ function parameter (for Graphite). @@ -403,42 +413,42 @@ In the InfluxDB data source, the legacy variable `$interval` is the same variabl The InfluxDB and Elasticsearch data sources have `Group by time interval` fields that are used to hard code the interval or to set the minimum limit for the `$__interval` variable (by using the `>` syntax -> `>10m`). -### $\_\_interval_ms +### `$__interval_ms` This variable is the `$__interval` variable in milliseconds, not a time interval formatted string. For example, if the `$__interval` is `20m` then the `$__interval_ms` is `1200000`. -### $\_\_name +### `$__name` -This variable is only available in the Singlestat panel and can be used in the prefix or suffix fields on the Options tab. The variable is replaced with the series name or alias. +This variable is only available in the **Singlestat** panel and can be used in the prefix or suffix fields on the Options tab. The variable is replaced with the series name or alias. -{{% admonition type="note" %}} -The Singlestat panel is no longer available from Grafana 8.0. -{{% /admonition %}} +{{< admonition type="note" >}} +The **Singlestat** panel is no longer available from Grafana 8.0. +{{< /admonition >}} -### $\_\_org +### `$__org` This variable is the ID of the current organization. `${__org.name}` is the name of the current organization. -### $\_\_user +### `$__user` `${__user.id}` is the ID of the current user. `${__user.login}` is the login handle of the current user. `${__user.email}` is the email for the current user. -### $\_\_range +### `$__range` Currently only supported for Prometheus and Loki data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond and a second representation called `$__range_ms` and `$__range_s`. -### $\_\_rate_interval +### `$__rate_interval` Currently only supported for Prometheus data sources. The `$__rate_interval` variable is meant to be used in the rate function. Refer to [Prometheus query variables](ref:prometheus-query-variables) for details. -### $\_\_rate_interval_ms +### `$__rate_interval_ms` This variable is the `$__rate_interval` variable in milliseconds, not a time-interval-formatted string. For example, if the `$__rate_interval` is `20m` then the `$__rate_interval_ms` is `1200000`. -### $timeFilter or $\_\_timeFilter +### `$timeFilter` or `$__timeFilter` The `$timeFilter` variable returns the currently selected time range as an expression. For example, the time range interval `Last 7 days` expression is `time > now() - 7d`. @@ -449,7 +459,7 @@ This is used in several places, including: - SQL queries in MySQL, Postgres, and MSSQL. - The `$__timeFilter` variable is used in the MySQL data source. -### $\_\_timezone +### `$__timezone` The `$__timezone` variable returns the currently selected time zone, either `utc` or an entry of the IANA time zone database (for example, `America/New_York`). @@ -485,9 +495,9 @@ In this example, there are several applications. Each application has a differen Now, you could make separate variables for each metric source, but then you have to know which server goes with which app. A better solution is to use one variable to filter another. In this example, when the user changes the value of the `app` variable, it changes the dropdown options returned by the `server` variable. Both variables use the **Multi-value** option and **Include all option**, enabling users to select some or all options presented at any time. -##### app variable +##### `app` variable -The query for this variable basically says, "Give me all the applications that exist." +The query for this variable basically says, "Find all the applications that exist." ``` apps.* @@ -495,9 +505,9 @@ apps.* The values returned are `backend`, `country`, `fakesite`, and `All`. -##### server variable +##### `server` variable -The query for this variable basically says, "Give me all servers for the currently chosen application." +The query for this variable basically says, "Find all servers for the currently chosen application." ``` apps.$app.* @@ -521,9 +531,9 @@ The query returns all servers associated with `fakesite`, including `web_server_ ##### More variables -{{% admonition type="note" %}} +{{< admonition type="note" >}} This example is theoretical. The Graphite server used in the example does not contain CPU metrics. -{{% /admonition %}} +{{< /admonition >}} The dashboard stops at two levels, but you could keep going. For example, if you wanted to get CPU metrics for selected servers, you could copy the `server` variable and extend the query so that it reads: @@ -531,7 +541,7 @@ The dashboard stops at two levels, but you could keep going. For example, if you apps.$app.$server.cpu.* ``` -This query basically says, "Show me the CPU metrics for the selected server." +This query basically says, "Find the CPU metrics for the selected server." Depending on what variable options the user selects, you could get queries like: @@ -547,9 +557,9 @@ In this example, you have several data centers. Each data center has a different In this example, when the user changes the value of the `datacenter` variable, it changes the dropdown options returned by the `host` variable. The `host` variable uses the **Multi-value** option and **Include all option**, allowing users to select some or all options presented at any time. The `datacenter` does not use either option, so you can only select one data center at a time. -##### datacenter variable +##### `datacenter` variable -The query for this variable basically says, "Give me all the data centers that exist." +The query for this variable basically says, "Find all the data centers that exist." ``` SHOW TAG VALUES WITH KEY = "datacenter" @@ -557,9 +567,9 @@ SHOW TAG VALUES WITH KEY = "datacenter" The values returned are `America`, `Africa`, `Asia`, and `Europe`. -##### host variable +##### `host` variable -The query for this variable basically says, "Give me all hosts for the currently chosen data center." +The query for this variable basically says, "Find all hosts for the currently chosen data center." ``` SHOW TAG VALUES WITH KEY = "hostname" WHERE "datacenter" =~ /^$datacenter$/ @@ -583,9 +593,9 @@ The query returns all servers associated with `Europe`, including `server3`, `se ##### More variables -{{% admonition type="note" %}} +{{< admonition type="note" >}} This example is theoretical. The InfluxDB server used in the example does not contain CPU metrics. -{{% /admonition %}} +{{< /admonition >}} The dashboard stops at two levels, but you could keep going. For example, if you wanted to get CPU metrics for selected hosts, you could copy the `host` variable and extend the query so that it reads: @@ -593,7 +603,7 @@ The dashboard stops at two levels, but you could keep going. For example, if you SHOW TAG VALUES WITH KEY = "cpu" WHERE "datacenter" =~ /^$datacenter$/ AND "host" =~ /^$host$/ ``` -This query basically says, "Show me the CPU metrics for the selected host." +This query basically says, "Find the CPU metrics for the selected host." Depending on what variable options the user selects, you could get queries like: @@ -607,7 +617,7 @@ SHOW TAG VALUES WITH KEY = "cpu" WHERE "datacenter" =~ /^Europe/ AND "host" =~ / The following practices make your dashboards and variables easier to use. -#### Creating new linked variables +#### New linked variables creation - Chaining variables create parent/child dependencies. You can envision them as a ladder or a tree. - The easiest way to create a new chained variable is to copy the variable that you want to base the new one on. In the variable list, click the **Duplicate variable** icon to the right of the variable entry to create a copy. You can then add on to the query for the parent variable. @@ -627,13 +637,17 @@ The more layers of dependency you have in variables, the longer it takes to upda For example, if you have a series of four linked variables (country, region, server, metric) and you change a root variable value (country), then Grafana must run queries for all the dependent variables before it updates the visualizations in the dashboard. -## Filter variables with regex + -Using the Regex Query option, you filter the list of options returned by the variable query or modify the options returned. +## Filter variables with regular expressions {#filter-variables-with-regex} -This page shows how to use regex to filter/modify values in the variable dropdown. + -Using the Regex Query Option, you filter the list of options returned by the Variable query or modify the options returned. For more information, refer to the Mozilla guide on [Regular expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions). +Using the **Regex** query option, you filter the list of options returned by the variable query or modify the options returned. + +This page shows how to use a regular expression to filter/modify values in the variable dropdown. + +Using the **Regex** query option, you filter the list of options returned by the Variable query or modify the options returned. For more information, refer to the Mozilla guide on [Regular expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions). Examples of filtering on the following list of options: @@ -646,7 +660,7 @@ backend_04 ### Filter so that only the options that end with `01` or `02` are returned: -Regex: +**Regex**: ```regex /(01|02)$/ @@ -659,9 +673,9 @@ backend_01 backend_02 ``` -### Filter and modify the options using a regex capture group to return part of the text: +### Filter and modify the options using a regular expression to capture group to return part of the text: -Regex: +**Regex**: ```regex /.*(01|02)/ @@ -684,7 +698,7 @@ up{instance="demo.robustperception.io:9093",job="alertmanager"} 1 1521630638000 up{instance="demo.robustperception.io:9100",job="node"} 1 1521630638000 ``` -Regex: +**Regex**: ```regex /.*instance="([^"]*).*/ @@ -711,7 +725,7 @@ node_hwmon_chip_names{chip="0000:d7:00_0_0000:d8:00_2",chip_name="enp216s0f0np2" node_hwmon_chip_names{chip="0000:d7:00_0_0000:d8:00_3",chip_name="enp216s0f0np3"} 1 ``` -Passed through the following Regex: +Passed through the following regular expression: ```regex /chip_name="(?[^"]+)|chip="(?[^"]+)/g From e4ef71b78e0c84a244eaf4aaf8c39e55278fdbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Tue, 11 Feb 2025 08:25:27 +0100 Subject: [PATCH 468/894] querier: handle datasource-not-found (#100175) --- pkg/registry/apis/query/client/plugin.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/registry/apis/query/client/plugin.go b/pkg/registry/apis/query/client/plugin.go index 2ffad9fa52b..434f3b049f9 100644 --- a/pkg/registry/apis/query/client/plugin.go +++ b/pkg/registry/apis/query/client/plugin.go @@ -2,7 +2,9 @@ package client import ( "context" + "errors" "fmt" + "net/http" "sync" "time" @@ -18,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/setting" + apierrors "k8s.io/apimachinery/pkg/api/errors" ) type pluginClient struct { @@ -69,6 +72,14 @@ func (d *pluginClient) QueryData(ctx context.Context, req data.QueryDataRequest) // NOTE: this depends on uid unique across datasources settings, err := d.pCtxProvider.GetDataSourceInstanceSettings(ctx, dsRef.UID) if err != nil { + if errors.Is(err, datasources.ErrDataSourceNotFound) { + status := metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotFound, + Message: "datasource not found", + } + return nil, &apierrors.StatusError{ErrStatus: status} + } return nil, err } From f2d34254d3b2d7e23e39cf17c99374f96a0d09e7 Mon Sep 17 00:00:00 2001 From: jackyin <648588267@qq.com> Date: Tue, 11 Feb 2025 15:51:59 +0800 Subject: [PATCH 469/894] Panel: Editor theme can't change (#99621) editor theme can't change --- .../src/components/Monaco/ReactMonacoEditor.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/Monaco/ReactMonacoEditor.tsx b/packages/grafana-ui/src/components/Monaco/ReactMonacoEditor.tsx index 2d095d6d182..56714d927c6 100644 --- a/packages/grafana-ui/src/components/Monaco/ReactMonacoEditor.tsx +++ b/packages/grafana-ui/src/components/Monaco/ReactMonacoEditor.tsx @@ -1,6 +1,6 @@ import Editor, { loader as monacoEditorLoader, Monaco } from '@monaco-editor/react'; import * as monaco from 'monaco-editor'; -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import { useTheme2 } from '../../themes'; @@ -16,12 +16,15 @@ export const ReactMonacoEditor = (props: ReactMonacoEditorProps) => { const theme = useTheme2(); const onMonacoBeforeMount = useCallback( (monaco: Monaco) => { - defineThemes(monaco, theme); beforeMount?.(monaco); }, - [beforeMount, theme] + [beforeMount] ); + useEffect(() => { + defineThemes(monaco, theme); + }, [theme]); + return ( Date: Tue, 11 Feb 2025 09:18:57 +0100 Subject: [PATCH 470/894] apiserver: Avoid panic for DELETE requests (#100372) --- pkg/apiserver/endpoints/responsewriter/responsewriter.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/apiserver/endpoints/responsewriter/responsewriter.go b/pkg/apiserver/endpoints/responsewriter/responsewriter.go index 150fb7eeb89..7bba579c9ea 100644 --- a/pkg/apiserver/endpoints/responsewriter/responsewriter.go +++ b/pkg/apiserver/endpoints/responsewriter/responsewriter.go @@ -56,6 +56,12 @@ func NewAdapter(req *http.Request) *ResponseAdapter { writer := bufio.NewWriter(w) reader := bufio.NewReader(r) buffered := bufio.NewReadWriter(reader, writer) + if req.Method == http.MethodDelete && req.Body == nil { + // The apiserver tries to read the body of DELETE requests, + // which causes a panic if the body is nil. + // https://github.com/kubernetes/apiserver/blob/v0.32.1/pkg/endpoints/handlers/delete.go#L88 + req.Body = http.NoBody + } return &ResponseAdapter{ req: req, res: http.Response{ From b6c0db31d9f80236b06a9cd57ca1098db49d861b Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Tue, 11 Feb 2025 09:36:46 +0100 Subject: [PATCH 471/894] Advisor: Clean up old checks (#100375) --- .../pkg/app/checkscheduler/checkscheduler.go | 49 +++++ .../app/checkscheduler/checkscheduler_test.go | 180 ++++++++++++++---- 2 files changed, 192 insertions(+), 37 deletions(-) diff --git a/apps/advisor/pkg/app/checkscheduler/checkscheduler.go b/apps/advisor/pkg/app/checkscheduler/checkscheduler.go index 392ec2d792d..f344326a59f 100644 --- a/apps/advisor/pkg/app/checkscheduler/checkscheduler.go +++ b/apps/advisor/pkg/app/checkscheduler/checkscheduler.go @@ -3,6 +3,7 @@ package checkscheduler import ( "context" "fmt" + "sort" "time" "github.com/grafana/grafana-app-sdk/app" @@ -16,6 +17,7 @@ import ( ) const evaluateChecksInterval = 24 * time.Hour +const maxChecks = 10 // Runner is a "runnable" app used to be able to expose and API endpoint // with the existing checks types. This does not need to be a CRUD resource, but it is @@ -78,6 +80,11 @@ func (r *Runner) Run(ctx context.Context) error { klog.Error("Error creating new check reports", "error", err) } + err = r.cleanupChecks(ctx) + if err != nil { + klog.Error("Error cleaning up old check reports", "error", err) + } + if nextSendInterval != evaluateChecksInterval { nextSendInterval = evaluateChecksInterval } @@ -127,3 +134,45 @@ func (r *Runner) createChecks(ctx context.Context) error { } return nil } + +// cleanupChecks deletes the olders checks if the number of checks exceeds the limit. +func (r *Runner) cleanupChecks(ctx context.Context) error { + list, err := r.client.List(ctx, metav1.NamespaceDefault, resource.ListOptions{Limit: -1}) + if err != nil { + return err + } + + // organize checks by type + checksByType := map[string][]resource.Object{} + for _, check := range list.GetItems() { + labels := check.GetLabels() + checkType, ok := labels[checks.TypeLabel] + if !ok { + klog.Error("Check type not found in labels", "check", check) + continue + } + checksByType[checkType] = append(checksByType[checkType], check) + } + + for _, checks := range checksByType { + if len(checks) > maxChecks { + // Sort checks by creation time + sort.Slice(checks, func(i, j int) bool { + ti := checks[i].GetCreationTimestamp().Time + tj := checks[j].GetCreationTimestamp().Time + return ti.Before(tj) + }) + // Delete the oldest checks + for i := 0; i < len(checks)-maxChecks; i++ { + check := checks[i] + id := check.GetStaticMetadata().Identifier() + err := r.client.Delete(ctx, id, resource.DeleteOptions{}) + if err != nil { + return fmt.Errorf("error deleting check: %w", err) + } + } + } + } + + return nil +} diff --git a/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go b/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go index 8b1c7fb47e5..b969e96745c 100644 --- a/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go +++ b/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go @@ -3,51 +3,18 @@ package checkscheduler import ( "context" "errors" + "fmt" + "math/rand/v2" "testing" + "time" "github.com/grafana/grafana-app-sdk/resource" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type MockCheckService struct { - checks []checks.Check -} - -func (m *MockCheckService) Checks() []checks.Check { - return m.checks -} - -type MockClient struct { - resource.Client - listFunc func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) - createFunc func(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) -} - -func (m *MockClient) List(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { - return m.listFunc(ctx, namespace, options) -} - -func (m *MockClient) Create(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) { - return m.createFunc(ctx, identifier, obj, options) -} - -type mockCheck struct { - checks.Check - - id string - steps []checks.Step -} - -func (m *mockCheck) ID() string { - return m.id -} - -func (m *mockCheck) Steps() []checks.Step { - return m.steps -} - func TestRunner_Run_ErrorOnList(t *testing.T) { mockCheckService := &MockCheckService{} mockClient := &MockClient{ @@ -129,3 +96,142 @@ func TestRunner_createChecks_Success(t *testing.T) { err := runner.createChecks(context.Background()) assert.NoError(t, err) } + +func TestRunner_cleanupChecks_ErrorOnList(t *testing.T) { + mockClient := &MockClient{ + listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + return nil, errors.New("list error") + }, + } + + runner := &Runner{ + client: mockClient, + } + + err := runner.cleanupChecks(context.Background()) + assert.Error(t, err) +} + +func TestRunner_cleanupChecks_WithinMax(t *testing.T) { + mockClient := &MockClient{ + listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + return &advisorv0alpha1.CheckList{ + Items: []advisorv0alpha1.Check{{}, {}}, + }, nil + }, + deleteFunc: func(ctx context.Context, identifier resource.Identifier, options resource.DeleteOptions) error { + return fmt.Errorf("shouldn't be called") + }, + } + + runner := &Runner{ + client: mockClient, + } + + err := runner.cleanupChecks(context.Background()) + assert.NoError(t, err) +} + +func TestRunner_cleanupChecks_ErrorOnDelete(t *testing.T) { + mockClient := &MockClient{ + listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + items := make([]advisorv0alpha1.Check, 0, maxChecks+1) + for i := 0; i < maxChecks+1; i++ { + item := advisorv0alpha1.Check{} + item.ObjectMeta.SetLabels(map[string]string{ + checks.TypeLabel: "mock", + }) + items = append(items, item) + } + return &advisorv0alpha1.CheckList{ + Items: items, + }, nil + }, + deleteFunc: func(ctx context.Context, identifier resource.Identifier, options resource.DeleteOptions) error { + return errors.New("delete error") + }, + } + + runner := &Runner{ + client: mockClient, + } + err := runner.cleanupChecks(context.Background()) + assert.ErrorContains(t, err, "delete error") +} + +func TestRunner_cleanupChecks_Success(t *testing.T) { + itemsDeleted := []string{} + items := make([]advisorv0alpha1.Check, 0, maxChecks+1) + for i := 0; i < maxChecks+1; i++ { + item := advisorv0alpha1.Check{} + item.ObjectMeta.SetName(fmt.Sprintf("check-%d", i)) + item.ObjectMeta.SetLabels(map[string]string{ + checks.TypeLabel: "mock", + }) + item.ObjectMeta.SetCreationTimestamp(metav1.NewTime(time.Time{}.Add(time.Duration(i) * time.Hour))) + items = append(items, item) + } + // shuffle the items to ensure the oldest are deleted + rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] }) + + mockClient := &MockClient{ + listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + return &advisorv0alpha1.CheckList{ + Items: items, + }, nil + }, + deleteFunc: func(ctx context.Context, identifier resource.Identifier, options resource.DeleteOptions) error { + itemsDeleted = append(itemsDeleted, identifier.Name) + return nil + }, + } + + runner := &Runner{ + client: mockClient, + } + err := runner.cleanupChecks(context.Background()) + assert.NoError(t, err) + assert.Equal(t, []string{"check-0"}, itemsDeleted) +} + +type MockCheckService struct { + checks []checks.Check +} + +func (m *MockCheckService) Checks() []checks.Check { + return m.checks +} + +type MockClient struct { + resource.Client + listFunc func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) + createFunc func(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) + deleteFunc func(ctx context.Context, identifier resource.Identifier, options resource.DeleteOptions) error +} + +func (m *MockClient) List(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + return m.listFunc(ctx, namespace, options) +} + +func (m *MockClient) Create(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) { + return m.createFunc(ctx, identifier, obj, options) +} + +func (m *MockClient) Delete(ctx context.Context, identifier resource.Identifier, options resource.DeleteOptions) error { + return m.deleteFunc(ctx, identifier, options) +} + +type mockCheck struct { + checks.Check + + id string + steps []checks.Step +} + +func (m *mockCheck) ID() string { + return m.id +} + +func (m *mockCheck) Steps() []checks.Step { + return m.steps +} From 6eaf702e96e5f294af85a6c12cc1706e72ed11d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Tue, 11 Feb 2025 10:34:56 +0100 Subject: [PATCH 472/894] fix(unified-storage): return legacy data in mode 2 (#100353) --- pkg/apiserver/rest/dualwriter.go | 2 +- pkg/apiserver/rest/dualwriter_mode2.go | 148 ++++++++++---------- pkg/apiserver/rest/dualwriter_mode2_test.go | 45 +++--- 3 files changed, 96 insertions(+), 99 deletions(-) diff --git a/pkg/apiserver/rest/dualwriter.go b/pkg/apiserver/rest/dualwriter.go index b0ee88ad854..a4b6b9c8162 100644 --- a/pkg/apiserver/rest/dualwriter.go +++ b/pkg/apiserver/rest/dualwriter.go @@ -94,7 +94,7 @@ const ( Mode1 // Mode2 is the dual writing mode that represents writing to LegacyStorage and Storage and reading from LegacyStorage. // The objects written to storage will include any labels and annotations. - // When reading values, the results will be from Storage when they exist, otherwise from legacy storage + // When reading values, the results will be from LegacyStorage. Mode2 // Mode3 represents writing to LegacyStorage and Storage and reading from Storage. // NOTE: Requesting mode3 will only happen when after a background sync job succeeds diff --git a/pkg/apiserver/rest/dualwriter_mode2.go b/pkg/apiserver/rest/dualwriter_mode2.go index cc064ff4d5c..fd6aeaf7e23 100644 --- a/pkg/apiserver/rest/dualwriter_mode2.go +++ b/pkg/apiserver/rest/dualwriter_mode2.go @@ -32,10 +32,9 @@ type DualWriterMode2 struct { 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 storage will be returned if they exist -// otherwise the value from legacy will be used +// 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 { return &DualWriterMode2{ Legacy: legacy, @@ -71,7 +70,7 @@ func (d *DualWriterMode2) Create(ctx context.Context, in runtime.Object, createV if err != nil { log.Error(err, "unable to create object in legacy storage") d.recordLegacyDuration(true, mode2Str, d.resource, method, startLegacy) - return createdFromLegacy, err + return nil, err } d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy) @@ -79,7 +78,7 @@ func (d *DualWriterMode2) Create(ctx context.Context, in runtime.Object, createV accCreated, err := meta.Accessor(createdCopy) if err != nil { - return createdFromLegacy, err + return nil, err } accCreated.SetResourceVersion("") @@ -93,51 +92,50 @@ func (d *DualWriterMode2) Create(ctx context.Context, in runtime.Object, createV } d.recordStorageDuration(false, mode2Str, d.resource, method, startStorage) - areEqual := Compare(createdFromStorage, createdFromLegacy) - d.recordOutcome(mode2Str, getName(createdFromStorage), areEqual, method) - if !areEqual { - log.Info("object from legacy and storage are not equal") - } + go func() { + areEqual := Compare(createdFromStorage, createdFromLegacy) + d.recordOutcome(mode2Str, getName(createdFromStorage), areEqual, method) + if !areEqual { + log.Info("object from legacy and storage are not equal") + } + }() return createdFromLegacy, err } -// It retrieves an object from Storage if possible, and if not it falls back to LegacyStorage. +// Get retrieves an object from Storage if possible, and if not it falls back to LegacyStorage. func (d *DualWriterMode2) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { var method = "get" log := d.Log.WithValues("name", name, "resourceVersion", options.ResourceVersion, "method", method) ctx = klog.NewContext(ctx, log) - startStorage := time.Now() - objStorage, err := d.Storage.Get(ctx, name, options) - d.recordStorageDuration(err != nil, mode2Str, d.resource, method, startStorage) - if err != nil { - // if it errors because it's not found, we try to fetch it from the legacy storage - if !apierrors.IsNotFound(err) { - log.Error(err, "unable to fetch object from storage") - return objStorage, err - } - log.Info("object not found in storage, fetching from legacy") - } - startLegacy := time.Now() objLegacy, err := d.Legacy.Get(ctx, name, options) if err != nil { log.Error(err, "unable to fetch object from legacy") d.recordLegacyDuration(true, mode2Str, d.resource, method, startLegacy) - return objLegacy, err + return nil, err } d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy) - areEqual := Compare(objStorage, objLegacy) - d.recordOutcome(mode2Str, name, areEqual, method) - if !areEqual { - log.Info("object from legacy and storage are not equal") + startStorage := time.Now() + objStorage, err := d.Storage.Get(ctx, name, options) + d.recordStorageDuration(err != nil, mode2Str, d.resource, method, startStorage) + if err != nil { + if !apierrors.IsNotFound(err) { + log.Error(err, "unable to fetch object from storage") + return nil, err + } + log.Info("object not found in storage, dual write or migration didn't happen yet") } - if objStorage != nil { - return objStorage, err - } + go func() { + areEqual := Compare(objStorage, objLegacy) + d.recordOutcome(mode2Str, name, areEqual, method) + if !areEqual { + log.Info("object from legacy and storage are not equal") + } + }() return objLegacy, err } @@ -156,6 +154,16 @@ func (d *DualWriterMode2) List(ctx context.Context, options *metainternalversion return ll, err } d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy) + + // Even if we don't compare, we want to fetch from unified storage and check that it doesn't error. + startStorage := time.Now() + if _, err := d.Storage.List(ctx, options); err != nil { + log.Error(err, "unable to list objects from storage") + d.recordStorageDuration(true, mode2Str, d.resource, method, startStorage) + return nil, err + } + d.recordStorageDuration(false, mode2Str, d.resource, method, startStorage) + return ll, nil } @@ -170,36 +178,26 @@ func (d *DualWriterMode2) DeleteCollection(ctx context.Context, deleteValidation if err != nil { log.WithValues("deleted", deletedLegacy).Error(err, "failed to delete collection successfully from legacy storage") d.recordLegacyDuration(true, mode2Str, d.resource, method, startLegacy) - return deletedLegacy, err + return nil, err } d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy) - legacyList, err := meta.ExtractList(deletedLegacy) - if err != nil { - log.Error(err, "unable to extract list from legacy storage") - return nil, err - } - - // Only the items deleted by the legacy DeleteCollection call are selected for deletion by Storage. - _, err = parseList(legacyList) - if err != nil { - return nil, err - } - startStorage := time.Now() deletedStorage, err := d.Storage.DeleteCollection(ctx, deleteValidation, options, listOptions) if err != nil { log.WithValues("deleted", deletedStorage).Error(err, "failed to delete collection successfully from Storage") d.recordStorageDuration(true, mode2Str, d.resource, method, startStorage) - return deletedStorage, err + return nil, err } d.recordStorageDuration(false, mode2Str, d.resource, method, startStorage) - areEqual := Compare(deletedStorage, deletedLegacy) - d.recordOutcome(mode2Str, getName(deletedStorage), areEqual, method) - if !areEqual { - log.Info("object from legacy and storage are not equal") - } + go func() { + areEqual := Compare(deletedStorage, deletedLegacy) + d.recordOutcome(mode2Str, getName(deletedStorage), areEqual, method) + if !areEqual { + log.Info("object from legacy and storage are not equal") + } + }() return deletedLegacy, err } @@ -209,17 +207,6 @@ func (d *DualWriterMode2) Delete(ctx context.Context, name string, deleteValidat log := d.Log.WithValues("name", name, "method", method) ctx = klog.NewContext(ctx, log) - startStorage := time.Now() - deletedS, async, err := d.Storage.Delete(ctx, name, deleteValidation, options) - if err != nil { - if !apierrors.IsNotFound(err) { - log.WithValues("objectList", deletedS).Error(err, "could not delete from duplicate storage") - d.recordStorageDuration(true, mode2Str, d.resource, method, startStorage) - } - return deletedS, async, err - } - d.recordStorageDuration(false, mode2Str, d.resource, method, startStorage) - startLegacy := time.Now() deletedLS, async, err := d.Legacy.Delete(ctx, name, deleteValidation, options) @@ -232,11 +219,24 @@ func (d *DualWriterMode2) Delete(ctx context.Context, name string, deleteValidat } d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy) - areEqual := Compare(deletedS, deletedLS) - d.recordOutcome(mode2Str, name, areEqual, method) - if !areEqual { - log.WithValues("name", name).Info("object from legacy and storage are not equal") + startStorage := time.Now() + deletedS, async, err := d.Storage.Delete(ctx, name, deleteValidation, options) + if err != nil { + if !apierrors.IsNotFound(err) { + log.WithValues("objectList", deletedS).Error(err, "could not delete from duplicate storage") + d.recordStorageDuration(true, mode2Str, d.resource, method, startStorage) + } + return deletedS, async, err } + d.recordStorageDuration(false, mode2Str, d.resource, method, startStorage) + + go func() { + areEqual := Compare(deletedS, deletedLS) + d.recordOutcome(mode2Str, name, areEqual, method) + if !areEqual { + log.WithValues("name", name).Info("object from legacy and storage are not equal") + } + }() return deletedLS, async, err } @@ -268,15 +268,13 @@ func (d *DualWriterMode2) Update(ctx context.Context, name string, objInfo rest. return objFromStorage, created, err } - areEqual := Compare(objFromStorage, objFromLegacy) - d.recordOutcome(mode2Str, name, areEqual, method) - if !areEqual { - log.WithValues("name", name).Info("object from legacy and storage are not equal") - } - - if objFromStorage != nil { - return objFromStorage, created, err - } + go func() { + areEqual := Compare(objFromStorage, objFromLegacy) + d.recordOutcome(mode2Str, name, areEqual, method) + if !areEqual { + log.WithValues("name", name).Info("object from legacy and storage are not equal") + } + }() return objFromLegacy, created, err } diff --git a/pkg/apiserver/rest/dualwriter_mode2_test.go b/pkg/apiserver/rest/dualwriter_mode2_test.go index 74168c2f456..4d150ac2ad7 100644 --- a/pkg/apiserver/rest/dualwriter_mode2_test.go +++ b/pkg/apiserver/rest/dualwriter_mode2_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -69,11 +69,11 @@ func TestMode2_Create(t *testing.T) { obj, err := dw.Create(context.Background(), tt.input, createFn, &metav1.CreateOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, exampleObj, obj) + require.Equal(t, exampleObj, obj) }) } } @@ -142,12 +142,12 @@ func TestMode2_Get(t *testing.T) { obj, err := dw.Get(context.Background(), tt.input, &metav1.GetOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, obj, exampleObj) - assert.NotEqual(t, obj, anotherObj) + require.Equal(t, obj, exampleObj) + require.NotEqual(t, obj, anotherObj) }) } } @@ -195,10 +195,10 @@ func TestMode2_List(t *testing.T) { obj, err := dw.List(context.Background(), &metainternalversion.ListOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, exampleList, obj) + require.Equal(t, exampleList, obj) }) } } @@ -288,12 +288,12 @@ func TestMode2_Delete(t *testing.T) { obj, _, err := dw.Delete(context.Background(), tt.input, func(context.Context, runtime.Object) error { return nil }, &metav1.DeleteOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, obj, exampleObj) - assert.NotEqual(t, obj, anotherObj) + require.Equal(t, obj, exampleObj) + require.NotEqual(t, obj, anotherObj) }) } } @@ -320,7 +320,7 @@ func TestMode2_DeleteCollection(t *testing.T) { }, { name: "error deleting a collection in the storage when legacy store is successful", - input: "foo", + input: "fail", setupLegacyFn: func(m *mock.Mock) { m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, nil) }, @@ -333,7 +333,7 @@ func TestMode2_DeleteCollection(t *testing.T) { name: "deleting a collection when error in legacy store", input: "fail", setupLegacyFn: func(m *mock.Mock) { - m.On("DeleteCollection", mock.Anything, mock.Anything, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: "fail"}}, mock.Anything).Return(nil, errors.New("error")) + m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error")) }, wantErr: true, }, @@ -343,16 +343,15 @@ func TestMode2_DeleteCollection(t *testing.T) { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m) + tt.setupLegacyFn(ls.Mock) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m) + tt.setupStorageFn(us.Mock) } dw := NewDualWriter(Mode2, ls, us, p, kind) @@ -360,10 +359,10 @@ func TestMode2_DeleteCollection(t *testing.T) { obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: tt.input}}, &metainternalversion.ListOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, exampleList, obj) + require.Equal(t, exampleList, obj) }) } } @@ -421,12 +420,12 @@ func TestMode2_Update(t *testing.T) { obj, _, err := dw.Update(context.Background(), tt.input, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, tt.expectedObj, obj) - assert.NotEqual(t, anotherObj, obj) + require.Equal(t, tt.expectedObj, obj) + require.NotEqual(t, anotherObj, obj) }) } } From e5154ce799b7eaedf5eb96c73d8ef3cb957c6322 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 11 Feb 2025 09:44:38 +0000 Subject: [PATCH 473/894] Combobox: Add tests for labels with Combobox (#100044) * Add tests for labels with Combobox * clean --- .../src/components/Combobox/Combobox.test.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx index 7ae3f1c5ec5..e99a2568faa 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx @@ -2,6 +2,8 @@ import { act, render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { Field } from '../Forms/Field'; + import { Combobox } from './Combobox'; import { ComboboxOption } from './types'; @@ -491,4 +493,42 @@ describe('Combobox', () => { }); }); }); + + describe('with RTL selectors', () => { + it('can be selected by label with HTML
    {error ? ( diff --git a/public/app/plugins/datasource/tempo/_importedDependencies/datasources/prometheus/QueryOptionGroup.tsx b/public/app/plugins/datasource/tempo/_importedDependencies/datasources/prometheus/QueryOptionGroup.tsx index f8b6a4273a3..4541016132b 100644 --- a/public/app/plugins/datasource/tempo/_importedDependencies/datasources/prometheus/QueryOptionGroup.tsx +++ b/public/app/plugins/datasource/tempo/_importedDependencies/datasources/prometheus/QueryOptionGroup.tsx @@ -13,9 +13,11 @@ export interface Props { collapsedInfo: string[]; queryStats?: QueryStats | null; children: React.ReactNode; + onToggle?: (isOpen: boolean) => void; + isOpen?: boolean; } -export function QueryOptionGroup({ title, children, collapsedInfo, queryStats }: Props) { +export function QueryOptionGroup({ title, children, collapsedInfo, queryStats, onToggle, isOpen: propsIsOpen }: Props) { const [isOpen, toggleOpen] = useToggle(false); const styles = useStyles2(getStyles); @@ -24,8 +26,8 @@ export function QueryOptionGroup({ title, children, collapsedInfo, queryStats }:
    {title}
    diff --git a/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx b/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx index e3d63e1ae26..d5a389da04a 100644 --- a/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx +++ b/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx @@ -15,6 +15,7 @@ import { FeatureName, featuresToTempoVersion } from '../datasource'; interface StreamingOptions extends DataSourceJsonData { streamingEnabled?: { search?: boolean; + metrics?: boolean; }; } interface Props extends DataSourcePluginOptionsEditorProps {} @@ -27,8 +28,7 @@ export const StreamingSection = ({ options, onOptionsChange }: Props) => { isCollapsible={false} description={ -
    {`Enable streaming for different Tempo features. - Currently supported only for search queries and from Tempo version ${featuresToTempoVersion[FeatureName.streaming]} onwards.`}
    +
    Enable streaming for different Tempo features.
    { { /> + + + ) => { + updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'streamingEnabled', { + ...options.jsonData.streamingEnabled, + metrics: event.currentTarget.checked, + }); + }} + /> + + ); }; diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 603e33f1a4c..a9a4cae07e3 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -58,7 +58,7 @@ import { transformFromOTLP as transformFromOTEL, transformTrace, } from './resultTransformer'; -import { doTempoChannelStream } from './streaming'; +import { doTempoMetricsStreaming, doTempoSearchStreaming } from './streaming'; import { TempoJsonData, TempoQuery } from './types'; import { getErrorMessage, migrateFromSearchToTraceQLSearch } from './utils'; import { TempoVariableSupport } from './variables'; @@ -67,7 +67,8 @@ export const DEFAULT_LIMIT = 20; export const DEFAULT_SPSS = 3; // spans per span set export enum FeatureName { - streaming = 'streaming', + searchStreaming = 'searchStreaming', + metricsStreaming = 'metricsStreaming', } /* Map, for each feature (e.g., streaming), the minimum Tempo version required to have that @@ -75,7 +76,8 @@ export enum FeatureName { ** target version, the feature is disabled in Grafana (frontend). */ export const featuresToTempoVersion = { - [FeatureName.streaming]: '2.2.0', + [FeatureName.searchStreaming]: '2.2.0', + [FeatureName.metricsStreaming]: '2.7.0', }; // The version that we use as default in case we cannot retrieve it from the backend. @@ -115,6 +117,7 @@ export class TempoDatasource extends DataSourceWithBackend - doTempoChannelStream( + doTempoSearchStreaming( { ...target, query }, this, // the datasource options, @@ -699,6 +719,28 @@ export class TempoDatasource extends DataSourceWithBackend, + targets: TempoQuery[], + query: string + ): Observable { + if (query === '') { + return EMPTY; + } + + return merge( + ...targets.map((target) => + doTempoMetricsStreaming( + { ...target, query }, + this, // the datasource + options + ) + ) + ); + } + makeTraceIdRequest(options: DataQueryRequest, targets: TempoQuery[]): DataQueryRequest { const request = { ...options, diff --git a/public/app/plugins/datasource/tempo/streaming.ts b/public/app/plugins/datasource/tempo/streaming.ts index 333faca7388..a1dd6e7ea68 100644 --- a/public/app/plugins/datasource/tempo/streaming.ts +++ b/public/app/plugins/datasource/tempo/streaming.ts @@ -1,31 +1,36 @@ import { capitalize } from 'lodash'; -import { map, Observable, takeWhile } from 'rxjs'; +import { map, Observable, scan, takeWhile } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; import { DataFrame, + dataFrameFromJSON, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings, + FieldCache, FieldType, LiveChannelScope, LoadingState, MutableDataFrame, + sortDataFrame, ThresholdsConfig, ThresholdsMode, } from '@grafana/data'; +import { cloneQueryResponse, combineResponses } from '@grafana/o11y-ds-frontend'; import { getGrafanaLiveSrv } from '@grafana/runtime'; import { SearchStreamingState } from './dataquery.gen'; import { DEFAULT_SPSS, TempoDatasource } from './datasource'; import { formatTraceQLResponse } from './resultTransformer'; import { SearchMetrics, TempoJsonData, TempoQuery } from './types'; +import { stepToNanos } from './utils'; function getLiveStreamKey(): string { return uuidv4(); } -export function doTempoChannelStream( +export function doTempoSearchStreaming( query: TempoQuery, ds: TempoDatasource, options: DataQueryRequest, @@ -67,11 +72,14 @@ export function doTempoChannelStream( if ('message' in evt && evt?.message) { const currentTime = performance.now(); const elapsedTime = currentTime - requestTime; - // Schema should be [traces, metrics, state, error] - const traces = evt.message.data.values[0][0]; - const metrics = evt.message.data.values[1][0]; - const frameState: SearchStreamingState = evt.message.data.values[2][0]; - const error = evt.message.data.values[3][0]; + + const messageFrame = dataFrameFromJSON(evt.message); + const fieldCache = new FieldCache(messageFrame); + + const traces = fieldCache.getFieldByName('result')?.values[0]; + const metrics = fieldCache.getFieldByName('metrics')?.values[0]; + const frameState = fieldCache.getFieldByName('state')?.values[0]; + const error = fieldCache.getFieldByName('error')?.values[0]; switch (frameState) { case SearchStreamingState.Done: @@ -100,6 +108,127 @@ export function doTempoChannelStream( ); } +export function doTempoMetricsStreaming( + query: TempoQuery, + ds: TempoDatasource, + options: DataQueryRequest +): Observable { + const range = options.range; + const key = getLiveStreamKey(); + + let state: LoadingState = LoadingState.NotStarted; + const step = stepToNanos(query.step); + + return getGrafanaLiveSrv() + .getStream({ + scope: LiveChannelScope.DataSource, + namespace: ds.uid, + path: `metrics/${key}`, + data: { + ...query, + step, + timeRange: { + from: range.from.toISOString(), + to: range.to.toISOString(), + }, + }, + }) + .pipe( + takeWhile((evt) => { + if ('message' in evt && evt?.message) { + const frameState: SearchStreamingState = evt.message.data.values[2][0]; + if (frameState === SearchStreamingState.Done || frameState === SearchStreamingState.Error) { + return false; + } + } + return true; + }, true), + map((evt) => { + let newResult: DataQueryResponse = { data: [], state: LoadingState.NotStarted }; + if ('message' in evt && evt?.message) { + const messageFrame = dataFrameFromJSON(evt.message); + const fieldCache = new FieldCache(messageFrame); + + const data = fieldCache.getFieldByName('result')?.values[0]; + const frameState = fieldCache.getFieldByName('state')?.values[0]; + const error = fieldCache.getFieldByName('error')?.values[0]; + + switch (frameState) { + case SearchStreamingState.Done: + state = LoadingState.Done; + break; + case SearchStreamingState.Streaming: + state = LoadingState.Streaming; + break; + case SearchStreamingState.Error: + throw new Error(error); + } + + newResult = { + data: data?.map(dataFrameFromJSON) ?? [], + state, + }; + } + + return newResult; + }), + // Merge results on acc + scan((acc, curr) => { + if (!curr) { + return acc; + } + if (!acc) { + return cloneQueryResponse(curr); + } + return mergeFrames(acc, curr); + }) + ); +} + +function mergeFrames(acc: DataQueryResponse, newResult: DataQueryResponse): DataQueryResponse { + const result = combineResponses(cloneQueryResponse(acc), newResult); + + // Remove duplicate time field values for all frames + result.data = result.data.map((frame: DataFrame) => { + let newFrame = frame; + const timeFieldIndex = frame.fields.findIndex((f) => f.type === FieldType.time); + if (timeFieldIndex >= 0) { + removeDuplicateTimeFieldValues(frame, timeFieldIndex); + newFrame = sortDataFrame(frame, timeFieldIndex); + } + return newFrame; + }); + + result.state = newResult.state; + return result; +} + +/** + * Remove duplicate time field values from the DataFrame. This is necessary because Tempo sends partial results to Grafana + * that we append to an existing DataFrame. This can result in duplicate values for the same timestamp so this function removes + * older values and keeps the latest value. + * @param accFrame + * @param timeFieldIndex + */ +function removeDuplicateTimeFieldValues(accFrame: DataFrame, timeFieldIndex: number) { + const duplicatesMap = accFrame.fields[timeFieldIndex].values.reduce((acc: Record, value, index) => { + if (acc[value]) { + acc[value].push(index); + } else { + acc[value] = [index]; + } + return acc; + }, {}); + + const indexesToRemove = Object.values(duplicatesMap) + .filter((indexes) => indexes.length > 1) + .map((indexes) => indexes.slice(1)) + .flat(); + accFrame.fields.forEach((field) => { + field.values = field.values.filter((_, index) => !indexesToRemove.includes(index)); + }); +} + function metricsDataFrame(metrics: SearchMetrics, state: SearchStreamingState, elapsedTime: number) { const progressThresholds: ThresholdsConfig = { steps: [ diff --git a/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx b/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx index b64aa5865e9..c4c90c78041 100644 --- a/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx +++ b/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx @@ -71,7 +71,8 @@ export function QueryEditor(props: Props) {
    diff --git a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx index a6d0ae61fcd..7552565acc1 100644 --- a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import * as React from 'react'; +import { useToggle } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; import { EditorField, EditorRow } from '@grafana/plugin-ui'; @@ -13,7 +14,8 @@ import { TempoQuery } from '../types'; interface Props { onChange: (value: TempoQuery) => void; query: Partial & TempoQuery; - isStreaming: boolean; + searchStreaming: boolean; + metricsStreaming: boolean; } /** @@ -29,8 +31,9 @@ const parseIntWithFallback = (val: string, fallback: number) => { return isNaN(parsed) ? fallback : parsed; }; -export const TempoQueryBuilderOptions = React.memo(({ onChange, query, isStreaming }) => { +export const TempoQueryBuilderOptions = React.memo(({ onChange, query, searchStreaming, metricsStreaming }) => { const styles = useStyles2(getStyles); + const [isOpen, toggleOpen] = useToggle(false); if (!query.hasOwnProperty('limit')) { query.limit = DEFAULT_LIMIT; @@ -76,19 +79,26 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query, is `Spans Limit: ${query.spss || DEFAULT_SPSS}`, `Table Format: ${query.tableType === SearchTableType.Traces ? 'Traces' : 'Spans'}`, '|', - `Streaming: ${isStreaming ? 'Enabled' : 'Disabled'}`, + `Streaming: ${searchStreaming ? 'Enabled' : 'Disabled'}`, ]; const collapsedMetricsOptions = [ `Step: ${query.step || 'auto'}`, `Type: ${query.metricsQueryType === MetricsQueryType.Range ? 'Range' : 'Instant'}`, + '|', + `Streaming: ${metricsStreaming ? 'Enabled' : 'Disabled'}`, // `Exemplars: ${query.exemplars !== undefined ? query.exemplars : 'auto'}`, ]; return (
    - + (({ onChange, query, is /> } tooltipInteractive> -
    {isStreaming ? 'Enabled' : 'Disabled'}
    +
    {searchStreaming ? 'Enabled' : 'Disabled'}
    - + (({ onChange, query, is onChange={onMetricsQueryTypeChange} /> + + } tooltipInteractive> +
    {metricsStreaming ? 'Enabled' : 'Disabled'}
    +
    {/* { }; return migratedQuery; }; + +export const stepToNanos = (step?: string) => { + if (!step) { + return 0; + } + + const match = step.match(/(\d+)(.+)/); + + const rawLength = match?.[1]; + const unit = match?.[2]; + + if (rawLength) { + if (unit === 'ns') { + return parseInt(rawLength, 10); + } + if (unit === 'µs') { + return parseInt(rawLength, 10) * 1000; + } + if (unit === 'ms') { + return parseInt(rawLength, 10) * 1000000; + } + const duration = parseDuration(step); + return ( + (duration.seconds || 0) * 1000000000 + + (duration.minutes || 0) * 60000000000 + + (duration.hours || 0) * 3600000000000 + ); + } + + return 0; +}; From d87ef806f04f373471be20ef8fa4eb5df74a8708 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 11 Feb 2025 11:17:15 +0000 Subject: [PATCH 480/894] LoadingBar: Use a theme variable instead of hardcoded hex color (#100407) use the theme variable in our loadingbar instead of a hardcoded hex color --- packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx b/packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx index a6e558422eb..7fad091b13d 100644 --- a/packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx +++ b/packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx @@ -47,7 +47,7 @@ const getStyles = (theme: GrafanaTheme2, delay: number, duration: number) => { bar: css({ width: BAR_WIDTH + '%', height: 1, - background: 'linear-gradient(90deg, rgba(110, 159, 255, 0) 0%, #6E9FFF 80.75%, rgba(110, 159, 255, 0) 100%)', + background: `linear-gradient(90deg, transparent 0%, ${theme.colors.primary.main} 80.75%, transparent 100%)`, transform: 'translateX(-100%)', willChange: 'transform', [theme.transitions.handleMotion('no-preference')]: { From 9bdacf3833fde94f11487c03c0f91e2de147707e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Feb 2025 12:27:04 +0100 Subject: [PATCH 481/894] DesignSystem: Menu and popover styling update to use new elevated background token (#100255) * DesignSystem: Menu and popover styling tweak proposal * Fix submenu * Themes: Add elevated prop * Update themes * update * Fixed tests * Update * fix markdown lint * Update packages/grafana-data/src/themes/createColors.ts Co-authored-by: Ashley Harrison * Update contribute/style-guides/themes.md Co-authored-by: Ashley Harrison * Update * Update --------- Co-authored-by: Ashley Harrison --- .betterer.results | 21 +----- contribute/style-guides/themes.md | 11 +-- .../grafana-data/src/themes/createColors.ts | 7 ++ .../src/themes/createComponents.ts | 2 +- .../src/themes/themeDefinitions/aubergine.ts | 1 + .../src/themes/themeDefinitions/debug.ts | 1 + .../themes/themeDefinitions/desertbloom.ts | 1 + .../themes/themeDefinitions/gildedgrove.ts | 1 + .../src/themes/themeDefinitions/gloom.ts | 1 + .../src/themes/themeDefinitions/mars.ts | 1 + .../src/themes/themeDefinitions/matrix.ts | 1 + .../themes/themeDefinitions/sapphiredusk.ts | 1 + .../src/themes/themeDefinitions/synthwave.ts | 1 + .../src/themes/themeDefinitions/tron.ts | 1 + .../src/themes/themeDefinitions/victorian.ts | 1 + .../src/themes/themeDefinitions/zen.ts | 1 + .../ColorPicker/ColorPicker.test.tsx | 5 +- .../ColorPicker/ColorPickerPopover.test.tsx | 2 +- .../ColorPicker/ColorPickerPopover.tsx | 67 ++++++------------- .../ColorPicker/NamedColorsGroup.tsx | 2 +- .../TimeRangePicker/TimePickerContent.tsx | 2 +- .../grafana-ui/src/components/Menu/Menu.tsx | 2 +- .../src/components/Menu/SubMenu.tsx | 2 +- .../src/components/ThemeDemos/ThemeDemo.tsx | 22 ++++-- .../VizTooltip/VizTooltipContent.tsx | 2 +- .../uPlot/plugins/TooltipPlugin2.tsx | 2 +- .../src/themes/ThemeContext.test.tsx | 4 +- .../grafana-ui/src/themes/ThemeContext.tsx | 7 ++ packages/grafana-ui/src/themes/mixins.ts | 2 +- .../annotations2/AnnotationEditor2.tsx | 3 +- .../annotations2/AnnotationTooltip2.tsx | 4 +- 31 files changed, 90 insertions(+), 91 deletions(-) diff --git a/.betterer.results b/.betterer.results index 21ed4ac7d28..bb9b2860e3b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -564,13 +564,6 @@ exports[`better eslint`] = { "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/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.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/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"] @@ -1348,25 +1341,15 @@ exports[`better eslint`] = { [0, 0, 0, "\'@grafana/ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup\' import is restricted from being used by a pattern. Import from the public export instead.", "3"], [0, 0, 0, "\'@grafana/ui/src/components/JSONFormatter/JSONFormatter\' import is restricted from being used by a pattern. Import from the public export instead.", "4"], [0, 0, 0, "\'@grafana/ui/src/themes\' import is restricted from being used by a pattern. Import from the public export instead.", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], + [0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "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"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "13"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "9"] ], "public/app/features/actions/ActionEditorModalContent.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/Button\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], [0, 0, 0, "\'@grafana/ui/src/components/Modal/Modal\' import is restricted from being used by a pattern. Import from the public export instead.", "1"] ], - "public/app/features/actions/ActionsInlineEditor.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Button\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], - [0, 0, 0, "\'@grafana/ui/src/components/Modal/Modal\' import is restricted from being used by a pattern. Import from the public export instead.", "1"], - [0, 0, 0, "\'@grafana/ui/src/themes\' import is restricted from being used by a pattern. Import from the public export instead.", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"] - ], "public/app/features/actions/ParamsEditor.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/IconButton/IconButton\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], [0, 0, 0, "\'@grafana/ui/src/components/Input/Input\' import is restricted from being used by a pattern. Import from the public export instead.", "1"], diff --git a/contribute/style-guides/themes.md b/contribute/style-guides/themes.md index 61c01764429..e4608ec55f1 100644 --- a/contribute/style-guides/themes.md +++ b/contribute/style-guides/themes.md @@ -96,11 +96,12 @@ Example use cases: ### Background colors -| Property | When to use | -| --------------------------------- | ------------------------------------------------------------------------------------------------ | -| theme.colors.background.canvas | Dashboard background. A background surface for panels and panes that use primary background | -| theme.colors.background.primary | The default content background for content panes and panels | -| theme.colors.background.secondary | For cards and other surfaces that need to stand out when placed on top of the primary background | +| Property | When to use | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| theme.colors.background.canvas | Dashboard background. A background surface for panels and panes that use primary background | +| theme.colors.background.primary | The default content background for content panes and panels | +| theme.colors.background.secondary | For cards and other surfaces that need to stand out when placed on top of the primary background | +| theme.colors.background.elevated | For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark themes it has a brighter shade to help give it contrast against the primary background | ### Borders diff --git a/packages/grafana-data/src/themes/createColors.ts b/packages/grafana-data/src/themes/createColors.ts index bffd7e3f9d5..9f9afcbfaa9 100644 --- a/packages/grafana-data/src/themes/createColors.ts +++ b/packages/grafana-data/src/themes/createColors.ts @@ -34,6 +34,11 @@ export interface ThemeColorsBase { primary: string; /** Cards and elements that need to stand out on the primary background */ secondary: string; + /** + * For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark + * themes it has a brighter shade to help give it contrast against the primary background. + **/ + elevated: string; }; border: { @@ -143,6 +148,7 @@ class DarkColors implements ThemeColorsBase> { canvas: palette.gray05, primary: palette.gray10, secondary: palette.gray15, + elevated: palette.gray15, }; action = { @@ -225,6 +231,7 @@ class LightColors implements ThemeColorsBase> { canvas: palette.gray90, primary: palette.white, secondary: palette.gray100, + elevated: palette.white, }; action = { diff --git a/packages/grafana-data/src/themes/createComponents.ts b/packages/grafana-data/src/themes/createComponents.ts index e19a16b327c..70dbcd67cf5 100644 --- a/packages/grafana-data/src/themes/createComponents.ts +++ b/packages/grafana-data/src/themes/createComponents.ts @@ -83,7 +83,7 @@ export function createComponents(colors: ThemeColors, shadows: ThemeShadows): Th background: input.background, }, tooltip: { - background: colors.background.secondary, + background: colors.background.elevated, text: colors.text.primary, }, dashboard: { diff --git a/packages/grafana-data/src/themes/themeDefinitions/aubergine.ts b/packages/grafana-data/src/themes/themeDefinitions/aubergine.ts index 45260c2fae0..aaaa79f51a1 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/aubergine.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/aubergine.ts @@ -28,6 +28,7 @@ const aubergineTheme: NewThemeOptions = { canvas: '#2E1F2D', primary: '#3C2136', secondary: '#4A2D47', + elevated: '#4A2D47', }, action: { hover: '#6A3C4B', diff --git a/packages/grafana-data/src/themes/themeDefinitions/debug.ts b/packages/grafana-data/src/themes/themeDefinitions/debug.ts index 589ca4ce86b..22e577faf2c 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/debug.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/debug.ts @@ -16,6 +16,7 @@ const debugTheme: NewThemeOptions = { canvas: '#000033', primary: '#000044', secondary: '#000055', + elevated: '#000055', }, text: { primary: '#bbbb00', diff --git a/packages/grafana-data/src/themes/themeDefinitions/desertbloom.ts b/packages/grafana-data/src/themes/themeDefinitions/desertbloom.ts index 20e6938ab36..d619226301d 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/desertbloom.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/desertbloom.ts @@ -52,6 +52,7 @@ const desertBloomTheme: NewThemeOptions = { canvas: '#FFF8F0', primary: '#FFFFFF', secondary: '#f9f3e8', + elevated: '#FFFFFF', }, action: { hover: 'rgba(168, 156, 134, 0.12)', diff --git a/packages/grafana-data/src/themes/themeDefinitions/gildedgrove.ts b/packages/grafana-data/src/themes/themeDefinitions/gildedgrove.ts index 546003ba07d..6492aaa906f 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/gildedgrove.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/gildedgrove.ts @@ -40,6 +40,7 @@ const gildedGroveTheme: NewThemeOptions = { canvas: '#111614', primary: '#1d2220', secondary: '#27312E', + elevated: '#27312E', }, action: { hover: 'rgba(200, 200, 180, 0.16)', diff --git a/packages/grafana-data/src/themes/themeDefinitions/gloom.ts b/packages/grafana-data/src/themes/themeDefinitions/gloom.ts index 849d4ece51a..7c4f497e95b 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/gloom.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/gloom.ts @@ -52,6 +52,7 @@ const gloomTheme: NewThemeOptions = { canvas: '#000', primary: '#0d0b14', secondary: '#19171f', + elevated: '#19171f', }, action: { diff --git a/packages/grafana-data/src/themes/themeDefinitions/mars.ts b/packages/grafana-data/src/themes/themeDefinitions/mars.ts index c695e65d992..2d3680aacfe 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/mars.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/mars.ts @@ -28,6 +28,7 @@ const marsTheme: NewThemeOptions = { canvas: '#3C1E1E', primary: '#522626', secondary: '#6A2F2F', + elevated: '#6A2F2F', }, action: { hover: 'rgba(210, 90, 60, 0.16)', diff --git a/packages/grafana-data/src/themes/themeDefinitions/matrix.ts b/packages/grafana-data/src/themes/themeDefinitions/matrix.ts index 3d4350e4f0a..51c58b9b394 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/matrix.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/matrix.ts @@ -8,6 +8,7 @@ const matrixTheme: NewThemeOptions = { canvas: '#000000', primary: '#020202', secondary: '#080808', + elevated: '#080808', }, text: { primary: '#00c017', diff --git a/packages/grafana-data/src/themes/themeDefinitions/sapphiredusk.ts b/packages/grafana-data/src/themes/themeDefinitions/sapphiredusk.ts index 353ff32b712..fa9a9513e02 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/sapphiredusk.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/sapphiredusk.ts @@ -54,6 +54,7 @@ const sapphireDuskTheme: NewThemeOptions = { canvas: '#1e273d', primary: '#12192e', secondary: '#212c47', + elevated: '#212c47', }, action: { hover: '#364057', diff --git a/packages/grafana-data/src/themes/themeDefinitions/synthwave.ts b/packages/grafana-data/src/themes/themeDefinitions/synthwave.ts index c54eaf71731..9e7e227621d 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/synthwave.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/synthwave.ts @@ -28,6 +28,7 @@ const synthwaveTheme: NewThemeOptions = { canvas: '#1A1A2E', primary: '#16213E', secondary: '#0F3460', + elevated: '#0F3460', }, action: { hover: 'rgba(255, 20, 147, 0.16)', diff --git a/packages/grafana-data/src/themes/themeDefinitions/tron.ts b/packages/grafana-data/src/themes/themeDefinitions/tron.ts index e3e761e012b..c95c9dea4fa 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/tron.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/tron.ts @@ -28,6 +28,7 @@ const tronTheme: NewThemeOptions = { canvas: '#0A0F18', primary: '#0F1B2A', secondary: '#152234', + elevated: '#152234', }, action: { hover: 'rgba(0, 255, 255, 0.16)', diff --git a/packages/grafana-data/src/themes/themeDefinitions/victorian.ts b/packages/grafana-data/src/themes/themeDefinitions/victorian.ts index a90879ea1fb..504b06f80ad 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/victorian.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/victorian.ts @@ -28,6 +28,7 @@ const victorianTheme: NewThemeOptions = { canvas: '#1F1510', primary: '#2C1A13', secondary: '#402A21', + elevated: '#402A21', }, action: { hover: '#3A2C22', diff --git a/packages/grafana-data/src/themes/themeDefinitions/zen.ts b/packages/grafana-data/src/themes/themeDefinitions/zen.ts index 0867de1e63e..8fd12542c21 100644 --- a/packages/grafana-data/src/themes/themeDefinitions/zen.ts +++ b/packages/grafana-data/src/themes/themeDefinitions/zen.ts @@ -28,6 +28,7 @@ const zenTheme: NewThemeOptions = { canvas: '#F4F4F4', primary: '#E9E9E9', secondary: '#D8D8D8', + elevated: '#E9E9E9', }, action: { hover: '#D1D1D1', diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.test.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.test.tsx index 5439b95fe68..cdd3ed8eeaf 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.test.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.test.tsx @@ -16,8 +16,11 @@ describe('ColorPicker', () => { mainButton.forEach((button) => expect(button).toHaveAttribute('type', 'button')); await userEvent.click(mainButton[0]); const buttons = screen.getAllByRole('button'); - expect(buttons.length).toBe(35); + expect(buttons.length).toBe(33); buttons.forEach((button) => expect(button).toHaveAttribute('type', 'button')); + + const tabs = screen.getAllByRole('tab'); + expect(tabs.length).toBe(2); }); it('renders custom trigger when supplied', () => { diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx index 71379860992..63af5266ed7 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx @@ -11,7 +11,7 @@ describe('ColorPickerPopover', () => { it('should be tabbable', async () => { render( {}} />); const color = screen.getByRole('button', { name: 'dark-red color' }); - const customTab = screen.getByRole('button', { name: 'Custom' }); + const customTab = screen.getByRole('tab', { name: 'Custom' }); await userEvent.tab(); expect(customTab).toHaveFocus(); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx index 056b7b18d58..1f51cf02cf1 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx @@ -7,7 +7,8 @@ import { GrafanaTheme2, colorManipulator } from '@grafana/data'; import { stylesFactory, withTheme2 } from '../../themes'; import { Themeable2 } from '../../types/theme'; -import { Trans } from '../../utils/i18n'; +import { t } from '../../utils/i18n'; +import { Tab, TabsBar } from '../Tabs'; import { PopoverContentProps } from '../Tooltip'; import { NamedColorsPalette } from './NamedColorsPalette'; @@ -18,7 +19,6 @@ export type ColorPickerChangeHandler = (color: string) => void; export interface ColorPickerProps extends Themeable2 { color: string; onChange: ColorPickerChangeHandler; - enableNamedColors?: boolean; } @@ -47,11 +47,6 @@ class UnThemedColorPickerPopover extends Comp }; } - getTabClassName = (tabName: PickerType | keyof T) => { - const { activePicker } = this.state; - return `ColorPickerPopover__tab ${activePicker === tabName && 'ColorPickerPopover__tab--active'}`; - }; - handleChange = (color: string) => { const { onChange, enableNamedColors, theme } = this.props; if (enableNamedColors) { @@ -101,11 +96,7 @@ class UnThemedColorPickerPopover extends Comp return ( <> {Object.keys(customPickers).map((key) => { - return ( - - ); + return ; })} ); @@ -113,7 +104,10 @@ class UnThemedColorPickerPopover extends Comp render() { const { theme } = this.props; + const { activePicker } = this.state; + const styles = getStyles(theme); + return ( {/* @@ -121,15 +115,19 @@ class UnThemedColorPickerPopover extends Comp see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668 */}
    -
    - - + + + {this.renderCustomPickerTabs()} -
    +
    {this.renderPicker()}
    @@ -145,34 +143,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => { colorPickerPopover: css({ borderRadius: theme.shape.radius.default, boxShadow: theme.shadows.z3, - background: theme.colors.background.primary, + background: theme.colors.background.elevated, + padding: theme.spacing(0.5), border: `1px solid ${theme.colors.border.weak}`, - - '.ColorPickerPopover__tab': { - width: '50%', - textAlign: 'center', - padding: theme.spacing(1, 0), - background: theme.colors.background.secondary, - color: theme.colors.text.secondary, - fontSize: theme.typography.bodySmall.fontSize, - cursor: 'pointer', - border: 'none', - - '&:focus:not(:focus-visible)': { - outline: 'none', - boxShadow: 'none', - }, - - ':focus-visible': { - position: 'relative', - }, - }, - - '.ColorPickerPopover__tab--active': { - color: theme.colors.text.primary, - fontWeight: theme.typography.fontWeightMedium, - background: theme.colors.background.primary, - }, }), colorPickerPopoverContent: css({ width: '246px', diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx index 47500ac910d..3724eccf7ee 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx @@ -54,7 +54,7 @@ const getStyles = (theme: GrafanaTheme2) => { }, }), colorLabel: css({ - paddingLeft: theme.spacing(2), + paddingLeft: theme.spacing(1), display: 'flex', alignItems: 'center', }), diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx index 6b8a47d5932..feea28b7329 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx @@ -277,7 +277,7 @@ const getStyles = ( isFullscreen?: boolean ) => ({ container: css({ - background: theme.colors.background.primary, + background: theme.colors.background.elevated, boxShadow: theme.shadows.z3, width: `${isFullscreen ? '546px' : '262px'}`, borderRadius: theme.shape.radius.default, diff --git a/packages/grafana-ui/src/components/Menu/Menu.tsx b/packages/grafana-ui/src/components/Menu/Menu.tsx index 8d9e794e535..1c9d675cff4 100644 --- a/packages/grafana-ui/src/components/Menu/Menu.tsx +++ b/packages/grafana-ui/src/components/Menu/Menu.tsx @@ -35,7 +35,7 @@ const MenuComp = React.forwardRef( { color: theme.colors.text.secondary, }), itemsWrapper: css({ - background: theme.colors.background.primary, + background: theme.colors.background.elevated, padding: theme.spacing(0.5), boxShadow: theme.shadows.z3, display: 'inline-block', diff --git a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx index cb8880a3df5..ba7146bcf23 100644 --- a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx +++ b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx @@ -15,7 +15,7 @@ import { InlineFieldRow } from '../Forms/InlineFieldRow'; import { RadioButtonGroup } from '../Forms/RadioButtonGroup/RadioButtonGroup'; import { Icon } from '../Icon/Icon'; import { Input } from '../Input/Input'; -import { BackgroundColor, BorderColor, Box } from '../Layout/Box/Box'; +import { BackgroundColor, BorderColor, Box, BoxShadow } from '../Layout/Box/Box'; import { Stack } from '../Layout/Stack/Stack'; import { Select } from '../Select/Select'; import { Switch } from '../Switch/Switch'; @@ -24,12 +24,20 @@ import { Text, TextProps } from '../Text/Text'; interface DemoBoxProps { bg?: BackgroundColor; border?: BorderColor; + shadow?: BoxShadow; textColor?: TextProps['color']; } -const DemoBox = ({ bg, border, children }: React.PropsWithChildren) => { +const DemoBox = ({ bg, border, children, shadow }: React.PropsWithChildren) => { return ( - + {children} ); @@ -91,8 +99,14 @@ export const ThemeDemo = () => { t.colors.background.primary is the main & preferred content - t.colors.background.secondary and t.colors.border.layer1 + t.colors.background.secondary (Used for cards) + + t.colors.background.elevated + + This elevated color should be used for menus and popovers. + + diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx index 89273ea291e..4debad70718 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx @@ -60,7 +60,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ flexDirection: 'column', flex: 1, gap: 2, - borderTop: `1px solid ${theme.colors.border.medium}`, + borderTop: `1px solid ${theme.colors.border.weak}`, padding: theme.spacing(1), }), }); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index b2e142d6a64..b5b0bcef2c4 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -732,7 +732,7 @@ const getStyles = (theme: GrafanaTheme2, maxWidth?: number) => ({ whiteSpace: 'pre', borderRadius: theme.shape.radius.default, position: 'fixed', - background: theme.colors.background.primary, + background: theme.colors.background.elevated, border: `1px solid ${theme.colors.border.weak}`, boxShadow: theme.shadows.z2, userSelect: 'text', diff --git a/packages/grafana-ui/src/themes/ThemeContext.test.tsx b/packages/grafana-ui/src/themes/ThemeContext.test.tsx index 4c1b6a363a5..c534c218205 100644 --- a/packages/grafana-ui/src/themes/ThemeContext.test.tsx +++ b/packages/grafana-ui/src/themes/ThemeContext.test.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import { render, renderHook } from '@testing-library/react'; -import { GrafanaTheme2 } from '@grafana/data'; +import { createTheme, GrafanaTheme2 } from '@grafana/data'; import { mockThemeContext, useStyles2 } from './ThemeContext'; @@ -45,7 +45,7 @@ describe('useStyles', () => { const { rerender, result } = renderHook(() => useStyles2(stylesCreator)); const storedReference = result.current; - const restoreThemeContext = mockThemeContext({}); + const restoreThemeContext = mockThemeContext(createTheme()); rerender(); expect(storedReference).not.toBe(result.current); restoreThemeContext(); diff --git a/packages/grafana-ui/src/themes/ThemeContext.tsx b/packages/grafana-ui/src/themes/ThemeContext.tsx index e83401a04de..a0d1eadf60a 100644 --- a/packages/grafana-ui/src/themes/ThemeContext.tsx +++ b/packages/grafana-ui/src/themes/ThemeContext.tsx @@ -118,6 +118,13 @@ export function useStyles2( ): CSSReturnValue { const theme = useTheme2(); + // Grafana ui can be bundled and used in older versions of Grafana where the theme doesn't have elevated background + // This can be removed post G12 + if (!theme.colors.background.elevated) { + theme.colors.background.elevated = + theme.colors.mode === 'light' ? theme.colors.background.primary : theme.colors.background.secondary; + } + let memoizedStyleCreator: typeof getStyles = memoizedStyleCreators.get(getStyles); if (!memoizedStyleCreator) { diff --git a/packages/grafana-ui/src/themes/mixins.ts b/packages/grafana-ui/src/themes/mixins.ts index 537ccaea44c..7e9e42179b0 100644 --- a/packages/grafana-ui/src/themes/mixins.ts +++ b/packages/grafana-ui/src/themes/mixins.ts @@ -75,7 +75,7 @@ export function getFocusStyles(theme: GrafanaTheme2) { // max-width is set up based on .grafana-tooltip class that's used in dashboard export const getTooltipContainerStyles = (theme: GrafanaTheme2) => ({ overflow: 'hidden', - background: theme.colors.background.secondary, + background: theme.colors.background.elevated, boxShadow: theme.shadows.z2, maxWidth: '800px', padding: theme.spacing(1), diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx index dde832c1fe1..b5b9b960e27 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx @@ -129,8 +129,7 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth const getStyles = (theme: GrafanaTheme2) => { return { editor: css({ - // zIndex: theme.zIndex.tooltip, - background: theme.colors.background.primary, + background: theme.colors.background.elevated, border: `1px solid ${theme.colors.border.weak}`, borderRadius: theme.shape.radius.default, boxShadow: theme.shadows.z3, diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx index 7e14fddb50b..a504e098f4b 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx @@ -106,9 +106,9 @@ const getStyles = (theme: GrafanaTheme2) => ({ zIndex: theme.zIndex.tooltip, whiteSpace: 'initial', borderRadius: theme.shape.radius.default, - background: theme.colors.background.primary, + background: theme.colors.background.elevated, border: `1px solid ${theme.colors.border.weak}`, - boxShadow: theme.shadows.z2, + boxShadow: theme.shadows.z3, userSelect: 'text', }), header: css({ From a1dacc24b90f820cdd06518a72b1fb7c7a2d050a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Feb 2025 12:35:21 +0100 Subject: [PATCH 482/894] CSS: Update generated scss file (#100413) --- public/sass/_variables.light.generated.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/_variables.light.generated.scss b/public/sass/_variables.light.generated.scss index 3ff0e66fab2..1781e899712 100644 --- a/public/sass/_variables.light.generated.scss +++ b/public/sass/_variables.light.generated.scss @@ -138,7 +138,7 @@ $alert-warning-bg: #ff9900; $alert-info-bg: #ff9900; // Tooltips and popovers -$tooltipBackground: #f4f5f5; +$tooltipBackground: #ffffff; $tooltipColor: rgba(36, 41, 46, 1); $popover-bg: #ffffff; From bfa4fa3c68601589068e27c564b5464b67cd80a8 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:36:11 +0100 Subject: [PATCH 483/894] Authz: Refactor folder tree (#99554) * Refactor folder tree to its own structure * Make it possible to json encode the tree * Use iterations for Ancestors and Children --------- Co-authored-by: IevaVasiljeva --- pkg/services/authz/rbac/models.go | 6 - pkg/services/authz/rbac/service.go | 100 +++++---------- pkg/services/authz/rbac/service_test.go | 162 ++++++++---------------- pkg/services/authz/rbac/tree.go | 108 ++++++++++++++++ pkg/services/authz/rbac/tree_test.go | 106 ++++++++++++++++ 5 files changed, 299 insertions(+), 183 deletions(-) create mode 100644 pkg/services/authz/rbac/tree.go create mode 100644 pkg/services/authz/rbac/tree_test.go diff --git a/pkg/services/authz/rbac/models.go b/pkg/services/authz/rbac/models.go index a94311b46d5..0bc402af857 100644 --- a/pkg/services/authz/rbac/models.go +++ b/pkg/services/authz/rbac/models.go @@ -23,9 +23,3 @@ type ListRequest struct { Verb string Action string } - -type FolderNode struct { - UID string - ParentUID *string - ChildrenUIDs []string -} diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index 349ccd53558..429b568a091 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -26,7 +26,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/rbac/store" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/storage/legacysql" ) @@ -59,7 +58,7 @@ type Service struct { permCache *cacheWrap[map[string]bool] teamCache *cacheWrap[[]int64] basicRoleCache *cacheWrap[store.BasicRole] - folderCache *cacheWrap[map[string]FolderNode] + folderCache *cacheWrap[folderTree] } func NewService( @@ -83,7 +82,7 @@ func NewService( permCache: newCacheWrap[map[string]bool](cache, logger, shortCacheTTL), teamCache: newCacheWrap[[]int64](cache, logger, shortCacheTTL), basicRoleCache: newCacheWrap[store.BasicRole](cache, logger, shortCacheTTL), - folderCache: newCacheWrap[map[string]FolderNode](cache, logger, shortCacheTTL), + folderCache: newCacheWrap[folderTree](cache, logger, shortCacheTTL), sf: new(singleflight.Group), } } @@ -517,31 +516,26 @@ func (s *Service) checkInheritedPermissions(ctx context.Context, scopeMap map[st defer span.End() ctxLogger := s.logger.FromContext(ctx) - folderMap, err := s.buildFolderTree(ctx, req.Namespace) + tree, err := s.buildFolderTree(ctx, req.Namespace) if err != nil { ctxLogger.Error("could not build folder and dashboard tree", "error", err) return false, err } - currentUID := req.ParentFolder - for { - if node, has := folderMap[currentUID]; has { - scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(node.UID) - if scopeMap[scope] { - return true, nil - } - if node.ParentUID == nil { - break - } - currentUID = *node.ParentUID - } else { - break + if scopeMap["folders:uid:"+req.ParentFolder] { + return true, nil + } + + for n := range tree.Ancestors(req.ParentFolder) { + if scopeMap["folders:uid:"+n.UID] { + return true, nil } } + return false, nil } -func (s *Service) buildFolderTree(ctx context.Context, ns claims.NamespaceInfo) (map[string]FolderNode, error) { +func (s *Service) buildFolderTree(ctx context.Context, ns claims.NamespaceInfo) (folderTree, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.buildFolderTree") defer span.End() @@ -557,41 +551,16 @@ func (s *Service) buildFolderTree(ctx context.Context, ns claims.NamespaceInfo) } span.SetAttributes(attribute.Int("num_folders", len(folders))) - folderMap := make(map[string]FolderNode, len(folders)) - for _, folder := range folders { - if node, has := folderMap[folder.UID]; !has { - folderMap[folder.UID] = FolderNode{ - UID: folder.UID, - ParentUID: folder.ParentUID, - } - } else { - node.ParentUID = folder.ParentUID - folderMap[folder.UID] = node - } - // Register that the parent has this child node - if folder.ParentUID == nil { - continue - } - if parent, has := folderMap[*folder.ParentUID]; has { - parent.ChildrenUIDs = append(parent.ChildrenUIDs, folder.UID) - folderMap[*folder.ParentUID] = parent - } else { - folderMap[*folder.ParentUID] = FolderNode{ - UID: *folder.ParentUID, - ChildrenUIDs: []string{folder.UID}, - } - } - } - - s.folderCache.Set(ctx, key, folderMap) - return folderMap, nil + tree := newFolderTree(folders) + s.folderCache.Set(ctx, key, tree) + return tree, nil }) if err != nil { - return nil, err + return folderTree{}, err } - return res.(map[string]FolderNode), nil + return res.(folderTree), nil } func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, req *ListRequest) (*authzv1.ListResponse, error) { @@ -609,10 +578,10 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, return nil, status.Error(codes.NotFound, "unsupported resource") } - var folderMap map[string]FolderNode + var tree folderTree if t.folderSupport { var err error - folderMap, err = s.buildFolderTree(ctx, req.Namespace) + tree, err = s.buildFolderTree(ctx, req.Namespace) if err != nil { ctxLogger.Error("could not build folder and dashboard tree", "error", err) return nil, err @@ -621,16 +590,16 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, var res *authzv1.ListResponse if strings.HasPrefix(req.Action, "folders:") { - res = buildFolderList(scopeMap, folderMap) + res = buildFolderList(scopeMap, tree) } else { - res = buildItemList(scopeMap, folderMap, t.prefix()) + res = buildItemList(scopeMap, tree, t.prefix()) } span.SetAttributes(attribute.Int("num_folders", len(res.Folders)), attribute.Int("num_items", len(res.Items))) return res, nil } -func buildFolderList(scopes map[string]bool, tree map[string]FolderNode) *authzv1.ListResponse { +func buildFolderList(scopes map[string]bool, tree folderTree) *authzv1.ListResponse { itemSet := make(map[string]struct{}, len(scopes)) for scope := range scopes { @@ -640,7 +609,9 @@ func buildFolderList(scopes map[string]bool, tree map[string]FolderNode) *authzv } itemSet[identifier] = struct{}{} - getChildren(tree, identifier, itemSet) + for n := range tree.Children(identifier) { + itemSet[n.UID] = struct{}{} + } } itemList := make([]string, 0, len(itemSet)) @@ -651,7 +622,7 @@ func buildFolderList(scopes map[string]bool, tree map[string]FolderNode) *authzv return &authzv1.ListResponse{Items: itemList} } -func buildItemList(scopes map[string]bool, tree map[string]FolderNode, prefix string) *authzv1.ListResponse { +func buildItemList(scopes map[string]bool, tree folderTree, prefix string) *authzv1.ListResponse { folderSet := make(map[string]struct{}, len(scopes)) itemSet := make(map[string]struct{}, len(scopes)) @@ -661,7 +632,9 @@ func buildItemList(scopes map[string]bool, tree map[string]FolderNode, prefix st continue } folderSet[identifier] = struct{}{} - getChildren(tree, identifier, folderSet) + for n := range tree.Children(identifier) { + folderSet[n.UID] = struct{}{} + } } else { identifier := strings.TrimPrefix(scope, prefix) itemSet[identifier] = struct{}{} @@ -678,18 +651,3 @@ func buildItemList(scopes map[string]bool, tree map[string]FolderNode, prefix st return &authzv1.ListResponse{Folders: folderList, Items: itemList} } - -func getChildren(folderMap map[string]FolderNode, folderUID string, folderSet map[string]struct{}) { - folder, has := folderMap[folderUID] - if !has { - return - } - for _, child := range folder.ChildrenUIDs { - // We have already processed all the children of this folder - if _, ok := folderSet[child]; ok { - return - } - folderSet[child] = struct{}{} - getChildren(folderMap, child, folderSet) - } -} diff --git a/pkg/services/authz/rbac/service_test.go b/pkg/services/authz/rbac/service_test.go index a8ad76d61fe..310e7cc8cfc 100644 --- a/pkg/services/authz/rbac/service_test.go +++ b/pkg/services/authz/rbac/service_test.go @@ -25,6 +25,7 @@ func TestService_checkPermission(t *testing.T) { name string permissions []accesscontrol.Permission check CheckRequest + folders []store.Folder expected bool } @@ -146,11 +147,37 @@ func TestService_checkPermission(t *testing.T) { }, expected: false, }, + { + name: "should return true if user has permissions on folder", + permissions: []accesscontrol.Permission{ + { + Scope: "folders:uid:parent", + Kind: "folders", + Attribute: "uid", + Identifier: "parent", + }, + }, + folders: []store.Folder{ + {UID: "parent"}, + {UID: "child", ParentUID: strPtr("parent")}, + }, + check: CheckRequest{ + Action: "dashboards:read", + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "some_dashboard", + ParentFolder: "child", + }, + expected: true, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() + + s.folderCache.Set(context.Background(), folderCacheKey("default"), newFolderTree(tc.folders)) + tc.check.Namespace = claims.NamespaceInfo{Value: "default", OrgID: 1} got, err := s.checkPermission(context.Background(), getScopeMap(tc.permissions), &tc.check) require.NoError(t, err) assert.Equal(t, tc.expected, got) @@ -371,90 +398,11 @@ func TestService_getUserPermissions(t *testing.T) { } } -func TestService_buildFolderTree(t *testing.T) { - type testCase struct { - name string - folders []store.Folder - cacheHit bool - expectedTree map[string]FolderNode - } - - testCases := []testCase{ - { - name: "should return folder tree from cache if available", - folders: []store.Folder{ - {UID: "folder1", ParentUID: nil}, - {UID: "folder2", ParentUID: strPtr("folder1")}, - }, - cacheHit: true, - expectedTree: map[string]FolderNode{ - "folder1": {UID: "folder1", ChildrenUIDs: []string{"folder2"}}, - "folder2": {UID: "folder2", ParentUID: strPtr("folder1")}, - }, - }, - { - name: "should return folder tree from store if not in cache", - folders: []store.Folder{ - {UID: "folder1", ParentUID: nil}, - {UID: "folder2", ParentUID: strPtr("folder1")}, - }, - cacheHit: false, - expectedTree: map[string]FolderNode{ - "folder1": {UID: "folder1", ChildrenUIDs: []string{"folder2"}}, - "folder2": {UID: "folder2", ParentUID: strPtr("folder1")}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - s := setupService() - - ns := claims.NamespaceInfo{Value: "stacks-12", OrgID: 1, StackID: 12} - - if tc.cacheHit { - s.folderCache.Set(ctx, folderCacheKey(ns.Value), tc.expectedTree) - } - - store := &fakeStore{folders: tc.folders} - s.store = store - s.permissionStore = store - - tree, err := s.buildFolderTree(ctx, ns) - - require.NoError(t, err) - require.Len(t, tree, len(tc.expectedTree)) - for _, folder := range tc.folders { - node, ok := tree[folder.UID] - require.True(t, ok) - // Check parent - if folder.ParentUID != nil { - require.NotNil(t, node.ParentUID) - require.Equal(t, *folder.ParentUID, *node.ParentUID) - } else { - require.Nil(t, node.ParentUID) - } - // Check children - if len(node.ChildrenUIDs) > 0 { - epectedChildren := tc.expectedTree[folder.UID].ChildrenUIDs - require.ElementsMatch(t, node.ChildrenUIDs, epectedChildren) - } - } - if tc.cacheHit { - require.Zero(t, store.calls) - } else { - require.Equal(t, 1, store.calls) - } - }) - } -} - func TestService_listPermission(t *testing.T) { type testCase struct { name string permissions []accesscontrol.Permission - folderTree map[string]FolderNode + folders []store.Folder list ListRequest expectedItems []string expectedFolders []string @@ -503,9 +451,9 @@ func TestService_listPermission(t *testing.T) { Identifier: "some_folder_2", }, }, - folderTree: map[string]FolderNode{ - "some_folder_1": {UID: "some_folder_1"}, - "some_folder_2": {UID: "some_folder_2"}, + folders: []store.Folder{ + {UID: "some_folder_1"}, + {UID: "some_folder_2"}, }, list: ListRequest{ Action: "dashboards:read", @@ -526,13 +474,13 @@ func TestService_listPermission(t *testing.T) { Identifier: "some_folder_1", }, }, - folderTree: map[string]FolderNode{ - "some_folder_parent": {UID: "some_folder_parent", ChildrenUIDs: []string{"some_folder_child"}}, - "some_folder_child": {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent"), ChildrenUIDs: []string{"some_folder_subchild1", "some_folder_subchild2"}}, - "some_folder_subchild1": {UID: "some_folder_subchild1", ParentUID: strPtr("some_folder_child")}, - "some_folder_subchild2": {UID: "some_folder_subchild2", ParentUID: strPtr("some_folder_child"), ChildrenUIDs: []string{"some_folder_subsubchild"}}, - "some_folder_subsubchild": {UID: "some_folder_subsubchild", ParentUID: strPtr("some_folder_subchild2")}, - "some_folder_1": {UID: "some_folder_1", ParentUID: strPtr("some_other_folder")}, + folders: []store.Folder{ + {UID: "some_folder_parent"}, + {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, + {UID: "some_folder_subchild1", ParentUID: strPtr("some_folder_child")}, + {UID: "some_folder_subchild2", ParentUID: strPtr("some_folder_child")}, + {UID: "some_folder_subsubchild", ParentUID: strPtr("some_folder_subchild2")}, + {UID: "some_folder_1", ParentUID: strPtr("some_other_folder")}, }, list: ListRequest{ Action: "dashboards:read", @@ -559,9 +507,9 @@ func TestService_listPermission(t *testing.T) { Identifier: "some_folder_parent", }, }, - folderTree: map[string]FolderNode{ - "some_folder_parent": {UID: "some_folder_parent", ChildrenUIDs: []string{"some_folder_child"}}, - "some_folder_child": {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, + folders: []store.Folder{ + {UID: "some_folder_parent"}, + {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, }, list: ListRequest{ Action: "dashboards:read", @@ -589,23 +537,25 @@ func TestService_listPermission(t *testing.T) { Identifier: "some_folder_parent", }, }, - folderTree: map[string]FolderNode{ - "some_folder_parent": {UID: "some_folder_parent", ChildrenUIDs: []string{"some_folder_child"}}, - "some_folder_child": {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent"), ChildrenUIDs: []string{"some_folder_subchild"}}, - "some_folder_subchild": {UID: "some_folder_subchild", ParentUID: strPtr("some_folder_child")}, + folders: []store.Folder{ + {UID: "some_folder_parent"}, + {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, + {UID: "some_folder_subchild", ParentUID: strPtr("some_folder_child")}, + {UID: "some_folder_child2", ParentUID: strPtr("some_folder_parent")}, }, list: ListRequest{ Action: "dashboards:read", Group: "dashboard.grafana.app", Resource: "dashboards", }, - expectedFolders: []string{"some_folder_parent", "some_folder_child", "some_folder_subchild"}, + expectedFolders: []string{"some_folder_parent", "some_folder_child", "some_folder_child2", "some_folder_subchild"}, }, { name: "return no dashboards and folders if the user doesn't have access to any resources", permissions: []accesscontrol.Permission{}, - folderTree: map[string]FolderNode{ - "some_folder_1": {UID: "some_folder_1"}, + + folders: []store.Folder{ + {UID: "some_folder_1"}, }, list: ListRequest{ Action: "dashboards:read", @@ -624,9 +574,9 @@ func TestService_listPermission(t *testing.T) { Identifier: "some_folder_parent", }, }, - folderTree: map[string]FolderNode{ - "some_folder_parent": {UID: "some_folder_parent", ChildrenUIDs: []string{"some_folder_child"}}, - "some_folder_child": {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, + folders: []store.Folder{ + {UID: "some_folder_parent"}, + {UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")}, }, list: ListRequest{ Action: "folders:read", @@ -640,8 +590,8 @@ func TestService_listPermission(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := setupService() - if tc.folderTree != nil { - s.folderCache.Set(context.Background(), folderCacheKey("default"), tc.folderTree) + if tc.folders != nil { + s.folderCache.Set(context.Background(), folderCacheKey("default"), newFolderTree(tc.folders)) } tc.list.Namespace = claims.NamespaceInfo{Value: "default", OrgID: 1} @@ -667,7 +617,7 @@ func setupService() *Service { permCache: newCacheWrap[map[string]bool](cache, logger, shortCacheTTL), teamCache: newCacheWrap[[]int64](cache, logger, shortCacheTTL), basicRoleCache: newCacheWrap[store.BasicRole](cache, logger, longCacheTTL), - folderCache: newCacheWrap[map[string]FolderNode](cache, logger, shortCacheTTL), + folderCache: newCacheWrap[folderTree](cache, logger, shortCacheTTL), store: fStore, permissionStore: fStore, identityStore: &fakeIdentityStore{}, diff --git a/pkg/services/authz/rbac/tree.go b/pkg/services/authz/rbac/tree.go new file mode 100644 index 00000000000..09d28eb37a5 --- /dev/null +++ b/pkg/services/authz/rbac/tree.go @@ -0,0 +1,108 @@ +package rbac + +import ( + "iter" + + "github.com/grafana/grafana/pkg/services/authz/rbac/store" +) + +func newFolderTree(folders []store.Folder) folderTree { + t := folderTree{ + Index: make(map[string]int, len(folders)), + Nodes: make([]folderNode, 0, len(folders)), + } + + for _, f := range folders { + t.Insert(f.UID, f.ParentUID) + } + + return t +} + +type folderTree struct { + // All nodes for the folderTree. + Nodes []folderNode + // Index is a map of folderNode UID to its positons in Nodes. + Index map[string]int +} + +type folderNode struct { + // UID is the uniqiue identifier for folderNode + UID string + // Parent is the position into folderTree nodes for parent, we store -1 for nodes that don't have a parent. + Parent int + // Children is positons into folderTree nodes for all children. + Children []int +} + +func (t *folderTree) Insert(uid string, parentUID *string) int { + parent := -1 + if parentUID != nil { + // find parent + i, ok := t.Index[*parentUID] + if !ok { + // insert parent if it don't exists yet + i = t.Insert(*parentUID, nil) + } + parent = i + } + + i, ok := t.Index[uid] + if !ok { + // this node does not exist yet so we add it to the index and append the new node + i = len(t.Nodes) + t.Index[uid] = i + t.Nodes = append(t.Nodes, folderNode{ + UID: uid, + Parent: parent, + }) + } else { + // if a node is added as a parent node first, its parent will not be set, so we make sure to do it now + t.Nodes[i].Parent = parent + } + + if parent != -1 { + // update parent to include the index of new child node + t.Nodes[parent].Children = append(t.Nodes[parent].Children, i) + } + + return i +} + +// Ancestors returns an iterator that yields ancestors for uid +func (t *folderTree) Ancestors(uid string) iter.Seq[folderNode] { + current, ok := t.Index[uid] + if !ok { + return func(yield func(folderNode) bool) {} + } + + current = t.Nodes[current].Parent + return func(yield func(folderNode) bool) { + for { + if current == -1 || !yield(t.Nodes[current]) { + return + } + + current = t.Nodes[current].Parent + } + } +} + +// Children returns an iterator that yields all children for uid +func (t *folderTree) Children(uid string) iter.Seq[folderNode] { + current, ok := t.Index[uid] + if !ok { + return func(yield func(folderNode) bool) {} + } + + queue := t.Nodes[current].Children + return func(yield func(folderNode) bool) { + for len(queue) > 0 { + current, queue = queue[0], queue[1:] + if !yield(t.Nodes[current]) { + return + } + queue = append(queue, t.Nodes[current].Children...) + } + } +} diff --git a/pkg/services/authz/rbac/tree_test.go b/pkg/services/authz/rbac/tree_test.go new file mode 100644 index 00000000000..aadfae8dd07 --- /dev/null +++ b/pkg/services/authz/rbac/tree_test.go @@ -0,0 +1,106 @@ +package rbac + +import ( + "fmt" + "testing" + + "github.com/grafana/grafana/pkg/services/authz/rbac/store" + "github.com/stretchr/testify/assert" +) + +func Test_Tree(t *testing.T) { + tree := newFolderTree([]store.Folder{ + {UID: "1"}, + {UID: "11", ParentUID: strPtr("1")}, + {UID: "12", ParentUID: strPtr("1")}, + {UID: "111", ParentUID: strPtr("11")}, + {UID: "1111", ParentUID: strPtr("111")}, + {UID: "121", ParentUID: strPtr("12")}, + // not ordered insert to make sure patching works correctly + {UID: "22", ParentUID: strPtr("2")}, + {UID: "222", ParentUID: strPtr("22")}, + {UID: "21", ParentUID: strPtr("2")}, + {UID: "2"}, + }) + + verify := func(t *testing.T, expected []string, visited map[string]bool) { + assert.Len(t, visited, len(expected)) + for _, e := range expected { + assert.True(t, visited[e], fmt.Sprintf("did not visit node: %s", e)) + } + } + + t.Run("should iterate all children of folder 1", func(t *testing.T) { + visited := map[string]bool{} + for n := range tree.Children("1") { + visited[n.UID] = true + } + + expected := []string{"11", "111", "1111", "12", "121"} + verify(t, expected, visited) + }) + + t.Run("should iterate all children of folder 2", func(t *testing.T) { + visited := map[string]bool{} + + for n := range tree.Children("2") { + visited[n.UID] = true + } + + expected := []string{"21", "22", "222"} + verify(t, expected, visited) + }) + + t.Run("should iterate all children of folder 111", func(t *testing.T) { + visited := map[string]bool{} + + for n := range tree.Children("111") { + visited[n.UID] = true + } + + expected := []string{"1111"} + verify(t, expected, visited) + }) + + t.Run("should iterate all children of folder 1111", func(t *testing.T) { + visited := map[string]bool{} + + for n := range tree.Children("1111") { + visited[n.UID] = true + } + + expected := []string{} + verify(t, expected, visited) + }) + + t.Run("should iterate all acestors of folder 1111", func(t *testing.T) { + visited := map[string]bool{} + + for n := range tree.Ancestors("1111") { + visited[n.UID] = true + } + + expected := []string{"1", "11", "111"} + verify(t, expected, visited) + }) + + t.Run("should iterate all acestors of folder 11", func(t *testing.T) { + visited := map[string]bool{} + for n := range tree.Ancestors("11") { + visited[n.UID] = true + } + + expected := []string{"1"} + verify(t, expected, visited) + }) + + t.Run("should iterate all acestors of folder 222", func(t *testing.T) { + visited := map[string]bool{} + for n := range tree.Ancestors("222") { + visited[n.UID] = true + } + + expected := []string{"2", "22"} + verify(t, expected, visited) + }) +} From e17fd5e8ad42d78752909ab90d9c6a73613efc30 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:39:00 +0200 Subject: [PATCH 484/894] DashboardDS: Re-run dashboard queries within MixedDS on transformation reprocessing (#100370) * fix scenario where results subscription is lost due to transformations * fix --- .../DashboardDatasourceBehaviour.test.tsx | 63 +++++++++++++++++++ .../scene/DashboardDatasourceBehaviour.tsx | 29 ++++++--- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx index 1b183577b1f..59a27eacaad 100644 --- a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx @@ -6,6 +6,7 @@ import { DataSourceApi, DataSourceJsonData, DataSourceRef, + getDefaultTimeRange, LoadingState, PanelData, } from '@grafana/data'; @@ -593,6 +594,68 @@ describe('DashboardDatasourceBehaviour', () => { expect(spy).toHaveBeenCalled(); }); }); + + it('Should re-run query after transformations reprocess', async () => { + const sourcePanel = new VizPanel({ + title: 'Panel A', + pluginId: 'table', + key: 'panel-1', + $data: new SceneDataTransformer({ + transformations: [{ id: 'transformA', options: {} }], + $data: new SceneQueryRunner({ + datasource: { uid: 'grafana' }, + queries: [{ refId: 'A', queryType: 'randomWalk' }], + }), + }), + }); + + const dashboardDSPanel = new VizPanel({ + title: 'Panel B', + pluginId: 'table', + key: 'panel-2', + $data: new SceneDataTransformer({ + transformations: [], + $data: new SceneQueryRunner({ + datasource: { uid: MIXED_DATASOURCE_NAME }, + queries: [ + { + datasource: { uid: SHARED_DASHBOARD_QUERY }, + refId: 'B', + panelId: 1, + }, + ], + $behaviors: [new DashboardDatasourceBehaviour({})], + }), + }), + }); + + const scene = new DashboardScene({ + title: 'hello', + uid: 'dash-1', + meta: { + canEdit: true, + }, + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), + }); + + activateFullSceneTree(scene); + + await new Promise((r) => setTimeout(r, 1)); + + // spy on runQueries that will be called by the behaviour + const spy = jest + .spyOn(dashboardDSPanel.state.$data!.state.$data as SceneQueryRunner, 'runQueries') + .mockImplementation(); + + // transformations are reprocessed (e.g. variable change) and data is updated so + // we re-run the queries in the dashboardDS panel because we lose the subscription + // in mixed DS scenario + (sourcePanel.state.$data as SceneDataTransformer).setState({ + data: { state: LoadingState.Done, series: [], timeRange: getDefaultTimeRange() }, + }); + + expect(spy).toHaveBeenCalled(); + }); }); async function buildTestScene() { diff --git a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx index fcbab5cc59b..940b4077d8d 100644 --- a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx @@ -1,6 +1,6 @@ import { Unsubscribable } from 'rxjs'; -import { SceneObjectBase, SceneObjectState, SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { SceneDataTransformer, SceneObjectBase, SceneObjectState, SceneQueryRunner, VizPanel } from '@grafana/scenes'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; @@ -28,6 +28,7 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase { + if (newState.data !== oldState.data) { + queryRunner.runQueries(); + } + }); + } + if (this.prevRequestId && this.prevRequestId !== sourcePanelQueryRunner.state.data?.request?.requestId) { queryRunner.runQueries(); } @@ -82,6 +97,10 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase query.datasource?.uid === SHARED_DASHBOARD_QUERY) - ) { - return true; - } - - return false; + ); } private handleLibPanelStateUpdates( From 6ee3c71ffe5560572a853e50c8826340adeddc4e Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Tue, 11 Feb 2025 13:08:07 +0100 Subject: [PATCH 485/894] Dashboards: refactor transform scene layout to save model and transform save model to scene layout, schema v2 (#100322) * Add tests * refactor transformSaveModelToSchemaV2 and transformSceneToSaveModelV2 * move default grid serializer functions outside of class * simplify layoutmanager descriptor * add test for SaveModel -> Scene * Fix lint issues * remove auto added import * Fix name * Fix test typo --- .../DefaultGridLayoutManager.tsx | 4 +- .../ResponsiveGridLayoutManager.tsx | 5 +- .../scene/layout-rows/RowsLayoutManager.tsx | 5 +- .../scene/types/DashboardLayoutManager.ts | 10 + .../scene/types/LayoutRegistryItem.ts | 6 + .../DefaultGridLayoutSerializer.ts | 354 ++++++++++++++++++ .../ResponsiveGridLayoutSerializer.ts | 65 ++++ .../layoutSerializers/RowsLayoutSerializer.ts | 51 +++ .../layoutSerializerRegistry.ts | 20 + .../serialization/layoutSerializers/utils.ts | 154 ++++++++ .../transformSaveModelSchemaV2ToScene.test.ts | 111 ++++++ .../transformSaveModelSchemaV2ToScene.ts | 284 +------------- .../transformSceneToSaveModelSchemaV2.test.ts | 162 ++++++++ .../transformSceneToSaveModelSchemaV2.ts | 284 +------------- 14 files changed, 953 insertions(+), 562 deletions(-) create mode 100644 public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts create mode 100644 public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts create mode 100644 public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts create mode 100644 public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts create mode 100644 public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index 567df0709bf..97b268757d9 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -28,6 +28,7 @@ import { } from '../../utils/utils'; import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; +import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; import { DashboardGridItem } from './DashboardGridItem'; import { RowRepeaterBehavior } from './RowRepeaterBehavior'; @@ -45,7 +46,7 @@ export class DefaultGridLayoutManager public readonly isDashboardLayoutManager = true; - public static readonly descriptor = { + public static readonly descriptor: LayoutRegistryItem = { get name() { return t('dashboard.default-layout.name', 'Default grid'); }, @@ -54,6 +55,7 @@ export class DefaultGridLayoutManager }, id: 'default-grid', createFromLayout: DefaultGridLayoutManager.createFromLayout, + kind: 'GridLayout', }; public readonly descriptor = DefaultGridLayoutManager.descriptor; diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index 928ac786861..bde10fa3374 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -7,6 +7,7 @@ import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelI import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; +import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; import { ResponsiveGridItem } from './ResponsiveGridItem'; import { getEditOptions } from './ResponsiveGridLayoutManagerEditor'; @@ -23,7 +24,7 @@ export class ResponsiveGridLayoutManager public readonly isDashboardLayoutManager = true; - public static readonly descriptor = { + public static readonly descriptor: LayoutRegistryItem = { get name() { return t('dashboard.responsive-layout.name', 'Responsive grid'); }, @@ -32,6 +33,8 @@ export class ResponsiveGridLayoutManager }, id: 'responsive-grid', createFromLayout: ResponsiveGridLayoutManager.createFromLayout, + + kind: 'ResponsiveGridLayout', }; public readonly descriptor = ResponsiveGridLayoutManager.descriptor; 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 0c770a333b1..fd3150af3e8 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -9,6 +9,7 @@ import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutMan import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior'; import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; +import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; import { RowItem } from './RowItem'; import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior'; @@ -23,7 +24,7 @@ export class RowsLayoutManager extends SceneObjectBase i public readonly isDashboardLayoutManager = true; - public static readonly descriptor = { + public static readonly descriptor: LayoutRegistryItem = { get name() { return t('dashboard.rows-layout.name', 'Rows'); }, @@ -32,6 +33,8 @@ export class RowsLayoutManager extends SceneObjectBase i }, id: 'rows-layout', createFromLayout: RowsLayoutManager.createFromLayout, + + kind: 'RowsLayout', }; public readonly descriptor = RowsLayoutManager.descriptor; diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts index 96d0174a210..18fc2c74064 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts @@ -1,4 +1,5 @@ import { SceneObject, VizPanel } from '@grafana/scenes'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; import { LayoutRegistryItem } from './LayoutRegistryItem'; @@ -82,6 +83,15 @@ export interface DashboardLayoutManager extends SceneObject { cloneLayout?(ancestorKey: string, isSource: boolean): DashboardLayoutManager; } +export interface LayoutManagerSerializer { + serialize(layout: DashboardLayoutManager, isSnapshot?: boolean): DashboardV2Spec['layout']; + deserialize( + layout: DashboardV2Spec['layout'], + elements: DashboardV2Spec['elements'], + preload: boolean + ): DashboardLayoutManager; +} + export function isDashboardLayoutManager(obj: SceneObject): obj is DashboardLayoutManager { return 'isDashboardLayoutManager' in obj; } diff --git a/public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts b/public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts index f35b4c62d3c..c22b37c2c53 100644 --- a/public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts +++ b/public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts @@ -1,4 +1,5 @@ import { RegistryItem } from '@grafana/data'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { DashboardLayoutManager } from './DashboardLayoutManager'; @@ -17,4 +18,9 @@ export interface LayoutRegistryItem extends RegistryItem { * @param saveModel */ createFromSaveModel?(saveModel: S): void; + + /** + * Schema kind of layout + */ + kind?: DashboardV2Spec['layout']['kind']; } diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts new file mode 100644 index 00000000000..b5da4a118f2 --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts @@ -0,0 +1,354 @@ +import { config } from '@grafana/runtime'; +import { + SceneGridItemLike, + SceneGridLayout, + SceneGridRow, + SceneObject, + VizPanel, + VizPanelMenu, + VizPanelState, +} from '@grafana/scenes'; +import { + DashboardV2Spec, + GridLayoutItemKind, + GridLayoutKind, + GridLayoutRowKind, + RepeatOptions, + Element, + GridLayoutItemSpec, + PanelKind, + LibraryPanelKind, +} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; +import { contextSrv } from 'app/core/core'; + +import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior'; +import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; +import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior'; +import { PanelNotices } from '../../scene/PanelNotices'; +import { AngularDeprecation } from '../../scene/angular/AngularDeprecation'; +import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; +import { RowRepeaterBehavior } from '../../scene/layout-default/RowRepeaterBehavior'; +import { RowActions } from '../../scene/layout-default/row-actions/RowActions'; +import { setDashboardPanelContext } from '../../scene/setDashboardPanelContext'; +import { DashboardLayoutManager, LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager'; +import { isClonedKey } from '../../utils/clone'; +import { calculateGridItemDimensions, getVizPanelKeyForPanelId, isLibraryPanel } from '../../utils/utils'; +import { GRID_ROW_HEIGHT } from '../const'; + +import { buildVizPanel } from './utils'; + +export class DefaultGridLayoutManagerSerializer implements LayoutManagerSerializer { + serialize(layoutManager: DefaultGridLayoutManager, isSnapshot?: boolean): DashboardV2Spec['layout'] { + return { + kind: 'GridLayout', + spec: { + items: getGridLayoutItems(layoutManager, isSnapshot), + }, + }; + } + + deserialize( + layout: DashboardV2Spec['layout'], + elements: DashboardV2Spec['elements'], + preload: boolean + ): DashboardLayoutManager { + if (layout.kind !== 'GridLayout') { + throw new Error('Invalid layout kind'); + } + return new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + isLazy: !(preload || contextSrv.user.authenticatedBy === 'render'), + children: createSceneGridLayoutForItems(layout, elements), + }), + }); + } +} + +function getGridLayoutItems( + body: DefaultGridLayoutManager, + isSnapshot?: boolean +): Array { + let items: Array = []; + for (const child of body.state.grid.state.children) { + if (child instanceof DashboardGridItem) { + // TODO: handle panel repeater scenario + if (child.state.variableName) { + items = items.concat(repeaterToLayoutItems(child, isSnapshot)); + } else { + items.push(gridItemToGridLayoutItemKind(child)); + } + } else if (child instanceof SceneGridRow) { + if (isClonedKey(child.state.key!) && !isSnapshot) { + // Skip repeat rows + continue; + } + items.push(gridRowToLayoutRowKind(child, isSnapshot)); + } + } + + return items; +} + +function getRowRepeat(row: SceneGridRow): RepeatOptions | undefined { + if (row.state.$behaviors) { + for (const behavior of row.state.$behaviors) { + if (behavior instanceof RowRepeaterBehavior) { + return { value: behavior.state.variableName, mode: 'variable' }; + } + } + } + return undefined; +} + +function gridRowToLayoutRowKind(row: SceneGridRow, isSnapshot = false): GridLayoutRowKind { + const children = row.state.children.map((child) => { + if (!(child instanceof DashboardGridItem)) { + throw new Error('Unsupported row child type'); + } + const y = (child.state.y ?? 0) - (row.state.y ?? 0) - GRID_ROW_HEIGHT; + return gridItemToGridLayoutItemKind(child, y); + }); + + return { + kind: 'GridLayoutRow', + spec: { + title: row.state.title, + y: row.state.y ?? 0, + collapsed: Boolean(row.state.isCollapsed), + elements: children, + repeat: getRowRepeat(row), + }, + }; +} + +function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, yOverride?: number): GridLayoutItemKind { + let elementGridItem: GridLayoutItemKind | undefined; + let x = 0, + y = 0, + width = 0, + height = 0; + + let gridItem_ = gridItem; + + if (!(gridItem_.state.body instanceof VizPanel)) { + throw new Error('DashboardGridItem body expected to be VizPanel'); + } + + // Get the grid position and size + height = (gridItem_.state.variableName ? gridItem_.state.itemHeight : gridItem_.state.height) ?? 0; + x = gridItem_.state.x ?? 0; + y = gridItem_.state.y ?? 0; + width = gridItem_.state.width ?? 0; + const repeatVar = gridItem_.state.variableName; + + // FIXME: which name should we use for the element reference, key or something else ? + const elementName = gridItem_.state.body.state.key ?? 'DefaultName'; + elementGridItem = { + kind: 'GridLayoutItem', + spec: { + x, + y: yOverride ?? y, + width: width, + height: height, + element: { + kind: 'ElementReference', + name: elementName, + }, + }, + }; + + if (repeatVar) { + const repeat: RepeatOptions = { + mode: 'variable', + value: repeatVar, + }; + + if (gridItem_.state.maxPerRow) { + repeat.maxPerRow = gridItem_.getMaxPerRow(); + } + + if (gridItem_.state.repeatDirection) { + repeat.direction = gridItem_.getRepeatDirection(); + } + + elementGridItem.spec.repeat = repeat; + } + + if (!elementGridItem) { + throw new Error('Unsupported grid item type'); + } + + return elementGridItem; +} + +function repeaterToLayoutItems(repeater: DashboardGridItem, isSnapshot = false): GridLayoutItemKind[] { + if (!isSnapshot) { + return [gridItemToGridLayoutItemKind(repeater)]; + } else { + if (repeater.state.body instanceof VizPanel && isLibraryPanel(repeater.state.body)) { + // TODO: implement + // const { x = 0, y = 0, width: w = 0, height: h = 0 } = repeater.state; + // return [vizPanelToPanel(repeater.state.body, { x, y, w, h }, isSnapshot)]; + return []; + } + + if (repeater.state.repeatedPanels) { + const { h, w, columnCount } = calculateGridItemDimensions(repeater); + const panels = repeater.state.repeatedPanels!.map((panel, index) => { + let x = 0, + y = 0; + if (repeater.state.repeatDirection === 'v') { + x = repeater.state.x!; + y = index * h; + } else { + x = (index % columnCount) * w; + y = repeater.state.y! + Math.floor(index / columnCount) * h; + } + + const gridPos = { x, y, w, h }; + + const result: GridLayoutItemKind = { + kind: 'GridLayoutItem', + spec: { + x: gridPos.x, + y: gridPos.y, + width: gridPos.w, + height: gridPos.h, + repeat: { + mode: 'variable', + value: repeater.state.variableName!, + maxPerRow: repeater.getMaxPerRow(), + direction: repeater.state.repeatDirection, + }, + element: { + kind: 'ElementReference', + name: panel.state.key!, + }, + }, + }; + return result; + }); + + return panels; + } + return []; + } +} + +function createSceneGridLayoutForItems(layout: GridLayoutKind, elements: Record): SceneGridItemLike[] { + const gridElements = layout.spec.items; + + return gridElements.map((element) => { + if (element.kind === 'GridLayoutItem') { + const panel = elements[element.spec.element.name]; + + if (!panel) { + throw new Error(`Panel with uid ${element.spec.element.name} not found in the dashboard elements`); + } + + if (panel.kind === 'Panel') { + return buildGridItem(element.spec, panel); + } else if (panel.kind === 'LibraryPanel') { + const libraryPanel = buildLibraryPanel(panel); + + return new DashboardGridItem({ + key: `grid-item-${panel.spec.id}`, + x: element.spec.x, + y: element.spec.y, + width: element.spec.width, + height: element.spec.height, + itemHeight: element.spec.height, + body: libraryPanel, + }); + } else { + throw new Error(`Unknown element kind: ${element.kind}`); + } + } else if (element.kind === 'GridLayoutRow') { + const children = element.spec.elements.map((gridElement) => { + const panel = elements[gridElement.spec.element.name]; + if (panel.kind === 'Panel') { + return buildGridItem(gridElement.spec, panel, element.spec.y + GRID_ROW_HEIGHT + gridElement.spec.y); + } else { + throw new Error(`Unknown element kind: ${gridElement.kind}`); + } + }); + let behaviors: SceneObject[] | undefined; + if (element.spec.repeat) { + behaviors = [new RowRepeaterBehavior({ variableName: element.spec.repeat.value })]; + } + return new SceneGridRow({ + y: element.spec.y, + isCollapsed: element.spec.collapsed, + title: element.spec.title, + $behaviors: behaviors, + actions: new RowActions({}), + children, + }); + } else { + // If this has been validated by the schema we should never reach this point, which is why TS is telling us this is an error. + //@ts-expect-error + throw new Error(`Unknown layout element kind: ${element.kind}`); + } + }); +} + +function buildGridItem(gridItem: GridLayoutItemSpec, panel: PanelKind, yOverride?: number): DashboardGridItem { + const vizPanel = buildVizPanel(panel); + return new DashboardGridItem({ + key: `grid-item-${panel.spec.id}`, + x: gridItem.x, + y: yOverride ?? gridItem.y, + width: gridItem.repeat?.direction === 'h' ? 24 : gridItem.width, + height: gridItem.height, + itemHeight: gridItem.height, + body: vizPanel, + variableName: gridItem.repeat?.value, + repeatDirection: gridItem.repeat?.direction, + maxPerRow: gridItem.repeat?.maxPerRow, + }); +} + +function buildLibraryPanel(panel: LibraryPanelKind): VizPanel { + const titleItems: SceneObject[] = []; + + if (config.featureToggles.angularDeprecationUI) { + titleItems.push(new AngularDeprecation()); + } + + titleItems.push( + new VizPanelLinks({ + rawLinks: [], + menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }), + }) + ); + + titleItems.push(new PanelNotices()); + + const vizPanelState: VizPanelState = { + key: getVizPanelKeyForPanelId(panel.spec.id), + titleItems, + $behaviors: [ + new LibraryPanelBehavior({ + uid: panel.spec.libraryPanel.uid, + name: panel.spec.libraryPanel.name, + }), + ], + extendPanelContext: setDashboardPanelContext, + pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID, + title: panel.spec.title, + options: {}, + fieldConfig: { + defaults: {}, + overrides: [], + }, + }; + + if (!config.publicDashboardAccessToken) { + vizPanelState.menu = new VizPanelMenu({ + $behaviors: [panelMenuBehavior], + }); + } + + return new VizPanel(vizPanelState); +} diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts new file mode 100644 index 00000000000..f97b7dd34c6 --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts @@ -0,0 +1,65 @@ +import { SceneCSSGridLayout } from '@grafana/scenes'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; + +import { ResponsiveGridItem } from '../../scene/layout-responsive-grid/ResponsiveGridItem'; +import { ResponsiveGridLayoutManager } from '../../scene/layout-responsive-grid/ResponsiveGridLayoutManager'; +import { DashboardLayoutManager, LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager'; +import { getGridItemKeyForPanelId } from '../../utils/utils'; + +import { buildVizPanel } from './utils'; + +export class ResponsiveGridLayoutSerializer implements LayoutManagerSerializer { + serialize(layoutManager: ResponsiveGridLayoutManager): DashboardV2Spec['layout'] { + return { + kind: 'ResponsiveGridLayout', + spec: { + col: + layoutManager.state.layout.state.templateColumns?.toString() ?? + ResponsiveGridLayoutManager.defaultCSS.templateColumns, + row: layoutManager.state.layout.state.autoRows?.toString() ?? ResponsiveGridLayoutManager.defaultCSS.autoRows, + items: layoutManager.state.layout.state.children.map((child) => { + if (!(child instanceof ResponsiveGridItem)) { + throw new Error('Expected ResponsiveGridItem'); + } + return { + kind: 'ResponsiveGridLayoutItem', + spec: { + element: { + kind: 'ElementReference', + name: child.state?.body?.state.key ?? 'DefaultName', + }, + }, + }; + }), + }, + }; + } + + deserialize(layout: DashboardV2Spec['layout'], elements: DashboardV2Spec['elements']): DashboardLayoutManager { + if (layout.kind !== 'ResponsiveGridLayout') { + throw new Error('Invalid layout kind'); + } + + const children = layout.spec.items.map((item) => { + const panel = elements[item.spec.element.name]; + if (!panel) { + throw new Error(`Panel with uid ${item.spec.element.name} not found in the dashboard elements`); + } + if (panel.kind !== 'Panel') { + throw new Error(`Unsupported element kind: ${panel.kind}`); + } + return new ResponsiveGridItem({ + key: getGridItemKeyForPanelId(panel.spec.id), + body: buildVizPanel(panel), + }); + }); + + return new ResponsiveGridLayoutManager({ + layout: new SceneCSSGridLayout({ + templateColumns: layout.spec.col, + autoRows: layout.spec.row, + children, + }), + }); + } +} diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts new file mode 100644 index 00000000000..d1823d6b1a7 --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts @@ -0,0 +1,51 @@ +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; + +import { RowItem } from '../../scene/layout-rows/RowItem'; +import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager'; +import { LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager'; + +import { layoutSerializerRegistry } from './layoutSerializerRegistry'; +import { getLayout } from './utils'; + +export class RowsLayoutSerializer implements LayoutManagerSerializer { + serialize(layoutManager: RowsLayoutManager): DashboardV2Spec['layout'] { + return { + kind: 'RowsLayout', + spec: { + rows: layoutManager.state.rows.map((row) => { + const layout = getLayout(row.state.layout); + if (layout.kind === 'RowsLayout') { + throw new Error('Nested RowsLayout is not supported'); + } + return { + kind: 'RowsLayoutRow', + spec: { + title: row.state.title, + collapsed: row.state.isCollapsed ?? false, + layout: layout, + }, + }; + }), + }, + }; + } + + deserialize( + layout: DashboardV2Spec['layout'], + elements: DashboardV2Spec['elements'], + preload: boolean + ): RowsLayoutManager { + if (layout.kind !== 'RowsLayout') { + throw new Error('Invalid layout kind'); + } + const rows = layout.spec.rows.map((row) => { + const layout = row.spec.layout; + return new RowItem({ + title: row.spec.title, + isCollapsed: row.spec.collapsed, + layout: layoutSerializerRegistry.get(layout.kind).serializer.deserialize(layout, elements, preload), + }); + }); + return new RowsLayoutManager({ rows }); + } +} diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts new file mode 100644 index 00000000000..4c3ec416bf3 --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts @@ -0,0 +1,20 @@ +import { Registry, RegistryItem } from '@grafana/data'; + +import { LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager'; + +import { DefaultGridLayoutManagerSerializer } from './DefaultGridLayoutSerializer'; +import { ResponsiveGridLayoutSerializer } from './ResponsiveGridLayoutSerializer'; +import { RowsLayoutSerializer } from './RowsLayoutSerializer'; + +interface LayoutSerializerRegistryItem extends RegistryItem { + serializer: LayoutManagerSerializer; +} + +export const layoutSerializerRegistry: Registry = + new Registry(() => { + return [ + { id: 'GridLayout', name: 'Grid Layout', serializer: new DefaultGridLayoutManagerSerializer() }, + { id: 'ResponsiveGridLayout', name: 'Responsive Grid Layout', serializer: new ResponsiveGridLayoutSerializer() }, + { id: 'RowsLayout', name: 'Rows Layout', serializer: new RowsLayoutSerializer() }, + ]; + }); diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts new file mode 100644 index 00000000000..70adca4f5f6 --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts @@ -0,0 +1,154 @@ +import { config } from '@grafana/runtime'; +import { + SceneDataProvider, + SceneDataQuery, + SceneDataTransformer, + SceneObject, + SceneQueryRunner, + VizPanel, + VizPanelMenu, + VizPanelState, +} from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema/dist/esm/index.gen'; +import { DashboardV2Spec, PanelKind, PanelQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; + +import { DashboardDatasourceBehaviour } from '../../scene/DashboardDatasourceBehaviour'; +import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; +import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior'; +import { PanelNotices } from '../../scene/PanelNotices'; +import { PanelTimeRange } from '../../scene/PanelTimeRange'; +import { AngularDeprecation } from '../../scene/angular/AngularDeprecation'; +import { setDashboardPanelContext } from '../../scene/setDashboardPanelContext'; +import { DashboardLayoutManager } from '../../scene/types/DashboardLayoutManager'; +import { getVizPanelKeyForPanelId } from '../../utils/utils'; +import { transformMappingsToV1 } from '../transformToV1TypesUtils'; + +import { layoutSerializerRegistry } from './layoutSerializerRegistry'; + +export function buildVizPanel(panel: PanelKind): VizPanel { + const titleItems: SceneObject[] = []; + + if (config.featureToggles.angularDeprecationUI) { + titleItems.push(new AngularDeprecation()); + } + + titleItems.push( + new VizPanelLinks({ + rawLinks: panel.spec.links, + menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }), + }) + ); + + titleItems.push(new PanelNotices()); + + const queryOptions = panel.spec.data.spec.queryOptions; + const timeOverrideShown = (queryOptions.timeFrom || queryOptions.timeShift) && !queryOptions.hideTimeOverride; + + const vizPanelState: VizPanelState = { + key: getVizPanelKeyForPanelId(panel.spec.id), + title: panel.spec.title, + description: panel.spec.description, + pluginId: panel.spec.vizConfig.kind, + options: panel.spec.vizConfig.spec.options, + fieldConfig: transformMappingsToV1(panel.spec.vizConfig.spec.fieldConfig), + pluginVersion: panel.spec.vizConfig.spec.pluginVersion, + displayMode: panel.spec.transparent ? 'transparent' : 'default', + hoverHeader: !panel.spec.title && !timeOverrideShown, + hoverHeaderOffset: 0, + $data: createPanelDataProvider(panel), + titleItems, + $behaviors: [], + extendPanelContext: setDashboardPanelContext, + // _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), //FIXME: Angular Migration + }; + + if (!config.publicDashboardAccessToken) { + vizPanelState.menu = new VizPanelMenu({ + $behaviors: [panelMenuBehavior], + }); + } + + if (queryOptions.timeFrom || queryOptions.timeShift) { + vizPanelState.$timeRange = new PanelTimeRange({ + timeFrom: queryOptions.timeFrom, + timeShift: queryOptions.timeShift, + hideTimeOverride: queryOptions.hideTimeOverride, + }); + } + + return new VizPanel(vizPanelState); +} + +export function createPanelDataProvider(panelKind: PanelKind): SceneDataProvider | undefined { + const panel = panelKind.spec; + const targets = panel.data?.spec.queries ?? []; + // Skip setting query runner for panels without queries + if (!targets?.length) { + return undefined; + } + + // Skip setting query runner for panel plugins with skipDataQuery + if (config.panels[panel.vizConfig.kind]?.skipDataQuery) { + return undefined; + } + + let dataProvider: SceneDataProvider | undefined = undefined; + const datasource = getPanelDataSource(panelKind); + + dataProvider = new SceneQueryRunner({ + datasource, + queries: targets.map(panelQueryKindToSceneQuery), + maxDataPoints: panel.data.spec.queryOptions.maxDataPoints ?? undefined, + maxDataPointsFromWidth: true, + cacheTimeout: panel.data.spec.queryOptions.cacheTimeout, + queryCachingTTL: panel.data.spec.queryOptions.queryCachingTTL, + minInterval: panel.data.spec.queryOptions.interval ?? undefined, + dataLayerFilter: { + panelId: panel.id, + }, + $behaviors: [new DashboardDatasourceBehaviour({})], + }); + + // Wrap inner data provider in a data transformer + return new SceneDataTransformer({ + $data: dataProvider, + transformations: panel.data.spec.transformations.map((transformation) => transformation.spec), + }); +} + +function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined { + if (!panel.spec.data?.spec.queries?.length) { + return undefined; + } + + let datasource: DataSourceRef | undefined = undefined; + let isMixedDatasource = false; + + panel.spec.data.spec.queries.forEach((query) => { + if (!datasource) { + datasource = query.spec.datasource; + } else if (datasource.uid !== query.spec.datasource?.uid || datasource.type !== query.spec.datasource?.type) { + isMixedDatasource = true; + } + }); + + return isMixedDatasource ? { type: 'mixed', uid: MIXED_DATASOURCE_NAME } : undefined; +} + +function panelQueryKindToSceneQuery(query: PanelQueryKind): SceneDataQuery { + return { + refId: query.spec.refId, + datasource: query.spec.datasource, + hide: query.spec.hidden, + ...query.spec.query.spec, + }; +} + +export function getLayout(sceneState: DashboardLayoutManager): DashboardV2Spec['layout'] { + const registryItem = layoutSerializerRegistry.get(sceneState.descriptor.kind ?? ''); + if (!registryItem) { + throw new Error(`Layout serializer not found for kind: ${sceneState.descriptor.kind}`); + } + return registryItem.serializer.serialize(sceneState); +} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index f57944b68d7..90033cc9856 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -14,6 +14,7 @@ import { AdHocFiltersVariable, SceneDataTransformer, SceneGridRow, + SceneGridItem, } from '@grafana/scenes'; import { AdhocVariableKind, @@ -34,6 +35,9 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; +import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem'; +import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager'; +import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager'; import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getQueryRunnerFor } from '../utils/utils'; @@ -495,5 +499,112 @@ describe('transformSaveModelSchemaV2ToScene', () => { expect(scene.state.meta.canDelete).toBe(true); }); }); + describe('dynamic dashboard layouts', () => { + it('should build a dashboard scene with a responsive grid layout', () => { + const dashboard = cloneDeep(defaultDashboard); + dashboard.spec.layout = { + kind: 'ResponsiveGridLayout', + spec: { + col: 'colString', + row: 'rowString', + items: [ + { + kind: 'ResponsiveGridLayoutItem', + spec: { + element: { + kind: 'ElementReference', + name: 'panel-1', + }, + }, + }, + ], + }, + }; + const scene = transformSaveModelSchemaV2ToScene(dashboard); + const layoutManager = scene.state.body as ResponsiveGridLayoutManager; + expect(layoutManager.descriptor.kind).toBe('ResponsiveGridLayout'); + expect(layoutManager.state.layout.state.templateColumns).toBe('colString'); + expect(layoutManager.state.layout.state.autoRows).toBe('rowString'); + expect(layoutManager.state.layout.state.children.length).toBe(1); + const gridItem = layoutManager.state.layout.state.children[0] as ResponsiveGridItem; + expect(gridItem.state.body.state.key).toBe('panel-1'); + }); + + it('should build a dashboard scene with rows layout', () => { + const dashboard = cloneDeep(defaultDashboard); + dashboard.spec.layout = { + kind: 'RowsLayout', + spec: { + rows: [ + { + kind: 'RowsLayoutRow', + spec: { + title: 'row1', + collapsed: false, + layout: { + kind: 'ResponsiveGridLayout', + spec: { + col: 'colString', + row: 'rowString', + items: [ + { + kind: 'ResponsiveGridLayoutItem', + spec: { + element: { + kind: 'ElementReference', + name: 'panel-1', + }, + }, + }, + ], + }, + }, + }, + }, + { + kind: 'RowsLayoutRow', + spec: { + title: 'row2', + collapsed: true, + layout: { + kind: 'GridLayout', + spec: { + items: [ + { + kind: 'GridLayoutItem', + spec: { + y: 0, + x: 0, + height: 10, + width: 10, + element: { + kind: 'ElementReference', + name: 'panel-2', + }, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }; + const scene = transformSaveModelSchemaV2ToScene(dashboard); + const layoutManager = scene.state.body as RowsLayoutManager; + expect(layoutManager.descriptor.kind).toBe('RowsLayout'); + expect(layoutManager.state.rows.length).toBe(2); + const row1Manager = layoutManager.state.rows[0].state.layout as ResponsiveGridLayoutManager; + expect(row1Manager.descriptor.kind).toBe('ResponsiveGridLayout'); + const row1GridItem = row1Manager.state.layout.state.children[0] as ResponsiveGridItem; + expect(row1GridItem.state.body.state.key).toBe('panel-1'); + + const row2Manager = layoutManager.state.rows[1].state.layout as DefaultGridLayoutManager; + expect(row2Manager.descriptor.kind).toBe('GridLayout'); + const row2GridItem = row2Manager.state.grid.state.children[0] as SceneGridItem; + expect(row2GridItem.state.body!.state.key).toBe('panel-2'); + }); + }); }); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 74ed404eeb2..a8f97ad4287 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -10,15 +10,10 @@ import { GroupByVariable, IntervalVariable, QueryVariable, - SceneCSSGridLayout, SceneDataLayerControls, SceneDataProvider, SceneDataQuery, SceneDataTransformer, - SceneGridItemLike, - SceneGridLayout, - SceneGridRow, - SceneObject, SceneQueryRunner, SceneRefreshPicker, SceneTimePicker, @@ -27,9 +22,6 @@ import { SceneVariableSet, TextBoxVariable, VariableValueSelectors, - VizPanel, - VizPanelMenu, - VizPanelState, } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema/dist/esm/index.gen'; import { @@ -46,9 +38,6 @@ import { defaultIntervalVariableKind, defaultQueryVariableKind, defaultTextVariableKind, - GridLayoutItemSpec, - GridLayoutKind, - Element, GroupByVariableKind, IntervalVariableKind, LibraryPanelKind, @@ -56,9 +45,7 @@ import { PanelQueryKind, QueryVariableKind, TextVariableKind, - ResponsiveGridLayoutItemKind, } from '@grafana/schema/src/schema/dashboard/v2alpha0'; -import { contextSrv } from 'app/core/core'; import { AnnoKeyCreatedBy, AnnoKeyFolder, @@ -80,32 +67,16 @@ import { registerDashboardMacro } from '../scene/DashboardMacro'; import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior'; import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScopesFacade } from '../scene/DashboardScopesFacade'; -import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; -import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; -import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior'; -import { PanelNotices } from '../scene/PanelNotices'; -import { PanelTimeRange } from '../scene/PanelTimeRange'; -import { AngularDeprecation } from '../scene/angular/AngularDeprecation'; -import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; -import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; -import { RowActions } from '../scene/layout-default/row-actions/RowActions'; -import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem'; -import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager'; -import { RowItem } from '../scene/layout-rows/RowItem'; -import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager'; -import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState'; -import { getGridItemKeyForPanelId, getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils'; +import { getIntervalsFromQueryString } from '../utils/utils'; -import { GRID_ROW_HEIGHT } from './const'; import { SnapshotVariable } from './custom-variables/SnapshotVariable'; +import { layoutSerializerRegistry } from './layoutSerializers/layoutSerializerRegistry'; import { registerPanelInteractionsReporter } from './transformSaveModelToScene'; import { transformCursorSyncV2ToV1, transformSortVariableToEnumV1, - transformMappingsToV1, transformVariableHideToEnumV1, transformVariableRefreshToEnumV1, } from './transformToV1TypesUtils'; @@ -179,7 +150,11 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo { - let layout: DashboardLayoutManager | undefined = undefined; - - if (row.spec.layout.kind === 'GridLayout') { - layout = new DefaultGridLayoutManager({ - grid: new SceneGridLayout({ - children: createSceneGridLayoutForItems(row.spec.layout, dashboard.elements), - }), - }); - } - - if (row.spec.layout.kind === 'ResponsiveGridLayout') { - layout = new ResponsiveGridLayoutManager({ - layout: new SceneCSSGridLayout({ - templateColumns: row.spec.layout.spec.col, - autoRows: row.spec.layout.spec.row, - children: createResponsiveGridItems(row.spec.layout.spec.items, dashboard.elements), - }), - }); - } - - if (!layout) { - throw new Error(`Unsupported layout kind: ${row.spec.layout.kind} in row`); - } - return new RowItem({ - title: row.spec.title, - isCollapsed: row.spec.collapsed, - layout: layout, - }); - }), - }); - } else if (dashboard.layout.kind === 'ResponsiveGridLayout') { - return new ResponsiveGridLayoutManager({ - layout: new SceneCSSGridLayout({ - templateColumns: dashboard.layout.spec.col, - autoRows: dashboard.layout.spec.row, - children: createResponsiveGridItems(dashboard.layout.spec.items, dashboard.elements), - }), - }); - } - - // @ts-ignore - this complains because we should never reach this point. If the model does not match the schema we will though. - throw new Error(`Unsupported layout type: ${dashboard.layout.kind}`); -} - -function createResponsiveGridItems( - items: ResponsiveGridLayoutItemKind[], - elements: Record -): ResponsiveGridItem[] { - return items.map((item) => { - const panel = elements[item.spec.element.name]; - if (!panel) { - throw new Error(`Panel with uid ${item.spec.element.name} not found in the dashboard elements`); - } - if (panel.kind !== 'Panel') { - throw new Error(`Unsupported element kind: ${panel.kind}`); - } - return new ResponsiveGridItem({ - key: getGridItemKeyForPanelId(panel.spec.id), - body: buildVizPanel(panel), - }); - }); -} - -function createSceneGridLayoutForItems(layout: GridLayoutKind, elements: Record): SceneGridItemLike[] { - const gridElements = layout.spec.items; - - return gridElements.map((element) => { - if (element.kind === 'GridLayoutItem') { - const panel = elements[element.spec.element.name]; - - if (!panel) { - throw new Error(`Panel with uid ${element.spec.element.name} not found in the dashboard elements`); - } - - if (panel.kind === 'Panel') { - return buildGridItem(element.spec, panel); - } else if (panel.kind === 'LibraryPanel') { - const libraryPanel = buildLibraryPanel(panel); - - return new DashboardGridItem({ - key: `grid-item-${panel.spec.id}`, - x: element.spec.x, - y: element.spec.y, - width: element.spec.width, - height: element.spec.height, - itemHeight: element.spec.height, - body: libraryPanel, - }); - } else { - throw new Error(`Unknown element kind: ${element.kind}`); - } - } else if (element.kind === 'GridLayoutRow') { - const children = element.spec.elements.map((gridElement) => { - const panel = elements[gridElement.spec.element.name]; - if (panel.kind === 'Panel') { - return buildGridItem(gridElement.spec, panel, element.spec.y + GRID_ROW_HEIGHT + gridElement.spec.y); - } else { - throw new Error(`Unknown element kind: ${gridElement.kind}`); - } - }); - let behaviors: SceneObject[] | undefined; - if (element.spec.repeat) { - behaviors = [new RowRepeaterBehavior({ variableName: element.spec.repeat.value })]; - } - return new SceneGridRow({ - y: element.spec.y, - isCollapsed: element.spec.collapsed, - title: element.spec.title, - $behaviors: behaviors, - actions: new RowActions({}), - children, - }); - } else { - // If this has been validated by the schema we should never reach this point, which is why TS is telling us this is an error. - //@ts-expect-error - throw new Error(`Unknown layout element kind: ${element.kind}`); - } - }); -} - -function buildLibraryPanel(panel: LibraryPanelKind): VizPanel { - const titleItems: SceneObject[] = []; - - if (config.featureToggles.angularDeprecationUI) { - titleItems.push(new AngularDeprecation()); - } - - titleItems.push( - new VizPanelLinks({ - rawLinks: [], - menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }), - }) - ); - - titleItems.push(new PanelNotices()); - - const vizPanelState: VizPanelState = { - key: getVizPanelKeyForPanelId(panel.spec.id), - titleItems, - $behaviors: [ - new LibraryPanelBehavior({ - uid: panel.spec.libraryPanel.uid, - name: panel.spec.libraryPanel.name, - }), - ], - extendPanelContext: setDashboardPanelContext, - pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID, - title: panel.spec.title, - options: {}, - fieldConfig: { - defaults: {}, - overrides: [], - }, - }; - - if (!config.publicDashboardAccessToken) { - vizPanelState.menu = new VizPanelMenu({ - $behaviors: [panelMenuBehavior], - }); - } - - return new VizPanel(vizPanelState); -} - -function buildVizPanel(panel: PanelKind): VizPanel { - const titleItems: SceneObject[] = []; - - if (config.featureToggles.angularDeprecationUI) { - titleItems.push(new AngularDeprecation()); - } - - titleItems.push( - new VizPanelLinks({ - rawLinks: panel.spec.links, - menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }), - }) - ); - - titleItems.push(new PanelNotices()); - - const queryOptions = panel.spec.data.spec.queryOptions; - const timeOverrideShown = (queryOptions.timeFrom || queryOptions.timeShift) && !queryOptions.hideTimeOverride; - - const vizPanelState: VizPanelState = { - key: getVizPanelKeyForPanelId(panel.spec.id), - title: panel.spec.title, - description: panel.spec.description, - pluginId: panel.spec.vizConfig.kind, - options: panel.spec.vizConfig.spec.options, - fieldConfig: transformMappingsToV1(panel.spec.vizConfig.spec.fieldConfig), - pluginVersion: panel.spec.vizConfig.spec.pluginVersion, - displayMode: panel.spec.transparent ? 'transparent' : 'default', - hoverHeader: !panel.spec.title && !timeOverrideShown, - hoverHeaderOffset: 0, - $data: createPanelDataProvider(panel), - titleItems, - $behaviors: [], - extendPanelContext: setDashboardPanelContext, - // _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), //FIXME: Angular Migration - }; - - if (!config.publicDashboardAccessToken) { - vizPanelState.menu = new VizPanelMenu({ - $behaviors: [panelMenuBehavior], - }); - } - - if (queryOptions.timeFrom || queryOptions.timeShift) { - vizPanelState.$timeRange = new PanelTimeRange({ - timeFrom: queryOptions.timeFrom, - timeShift: queryOptions.timeShift, - hideTimeOverride: queryOptions.hideTimeOverride, - }); - } - - return new VizPanel(vizPanelState); -} - function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined { if (!panel.spec.data?.spec.queries?.length) { return undefined; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index c87a4a0a687..39ff7241b11 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -9,6 +9,7 @@ import { GroupByVariable, IntervalVariable, QueryVariable, + SceneCSSGridLayout, SceneGridLayout, SceneGridRow, SceneRefreshPicker, @@ -24,6 +25,10 @@ import { VariableSort as VariableSortV1, } from '@grafana/schema/dist/esm/index.gen'; +import { + ResponsiveGridLayoutSpec, + RowsLayoutSpec, +} from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha0'; import { DashboardEditPane } from '../edit-pane/DashboardEditPane'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; @@ -33,6 +38,11 @@ import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; +import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem'; +import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager'; +import { RowItem } from '../scene/layout-rows/RowItem'; +import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager'; +import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2'; @@ -365,6 +375,158 @@ describe('transformSceneToSaveModelSchemaV2', () => { }); }); +function getMinimalSceneState(body: DashboardLayoutManager): Partial { + return { + id: 1, + title: 'Test Dashboard', + description: 'Test Description', + preload: true, + tags: ['tag1', 'tag2'], + uid: 'test-uid', + version: 1, + + controls: new DashboardControls({ + refreshPicker: new SceneRefreshPicker({ + refresh: '5s', + intervals: ['5s', '10s', '30s'], + autoEnabled: true, + autoMinInterval: '5s', + autoValue: '5s', + isOnCanvas: true, + primary: true, + withText: true, + minRefreshInterval: '5s', + }), + timePicker: new SceneTimePicker({ + isOnCanvas: true, + hidePicker: true, + }), + }), + + $timeRange: new SceneTimeRange({ + timeZone: 'UTC', + from: 'now-1h', + to: 'now', + weekStart: 'monday', + fiscalYearStartMonth: 1, + UNSAFE_nowDelay: '1m', + refreshOnActivate: { + afterMs: 10, + percent: 0.1, + }, + }), + + body, + }; +} + +describe('dynamic layouts', () => { + it('should transform scene with rows layout with default grids in rows to save model schema v2', () => { + const scene = setupDashboardScene( + getMinimalSceneState( + new RowsLayoutManager({ + rows: [ + new RowItem({ + layout: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [ + new DashboardGridItem({ + y: 0, + height: 10, + body: new VizPanel({}), + }), + ], + }), + }), + }), + ], + }) + ) + ); + + const result = transformSceneToSaveModelSchemaV2(scene); + expect(result.layout.kind).toBe('RowsLayout'); + const rowsLayout = result.layout.spec as RowsLayoutSpec; + expect(rowsLayout.rows.length).toBe(1); + expect(rowsLayout.rows[0].kind).toBe('RowsLayoutRow'); + expect(rowsLayout.rows[0].spec.layout.kind).toBe('GridLayout'); + }); + + it('should transform scene with rows layout with multiple rows with different grids to save model schema v2', () => { + const scene = setupDashboardScene( + getMinimalSceneState( + new RowsLayoutManager({ + rows: [ + new RowItem({ + layout: new ResponsiveGridLayoutManager({ + layout: new SceneCSSGridLayout({ + children: [ + new ResponsiveGridItem({ + body: new VizPanel({}), + }), + ], + }), + }), + }), + new RowItem({ + layout: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [ + new DashboardGridItem({ + y: 0, + height: 10, + body: new VizPanel({}), + }), + ], + }), + }), + }), + ], + }) + ) + ); + + const result = transformSceneToSaveModelSchemaV2(scene); + expect(result.layout.kind).toBe('RowsLayout'); + const rowsLayout = result.layout.spec as RowsLayoutSpec; + expect(rowsLayout.rows.length).toBe(2); + expect(rowsLayout.rows[0].kind).toBe('RowsLayoutRow'); + expect(rowsLayout.rows[0].spec.layout.kind).toBe('ResponsiveGridLayout'); + expect(rowsLayout.rows[0].spec.layout.spec.items[0].kind).toBe('ResponsiveGridLayoutItem'); + + expect(rowsLayout.rows[1].spec.layout.kind).toBe('GridLayout'); + expect(rowsLayout.rows[1].spec.layout.spec.items[0].kind).toBe('GridLayoutItem'); + }); + + it('should transform scene with responsive grid layout to schema v2', () => { + const scene = setupDashboardScene( + getMinimalSceneState( + new ResponsiveGridLayoutManager({ + layout: new SceneCSSGridLayout({ + autoRows: 'rowString', + templateColumns: 'colString', + children: [ + new ResponsiveGridItem({ + body: new VizPanel({}), + }), + new ResponsiveGridItem({ + body: new VizPanel({}), + }), + ], + }), + }) + ) + ); + const result = transformSceneToSaveModelSchemaV2(scene); + expect(result.layout.kind).toBe('ResponsiveGridLayout'); + const respGridLayout = result.layout.spec as ResponsiveGridLayoutSpec; + expect(respGridLayout.col).toBe('colString'); + expect(respGridLayout.row).toBe('rowString'); + expect(respGridLayout.items.length).toBe(2); + expect(respGridLayout.items[0].kind).toBe('ResponsiveGridLayoutItem'); + }); +}); + const annotationLayer1 = new DashboardAnnotationsDataLayer({ key: 'layer1', query: { diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 7a318e38b64..59b44050d81 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -7,7 +7,6 @@ import { dataLayers, SceneDataQuery, SceneDataTransformer, - SceneGridRow, SceneVariableSet, VizPanel, } from '@grafana/scenes'; @@ -24,7 +23,6 @@ import { DataTransformerConfig, PanelQuerySpec, DataQueryKind, - GridLayoutItemKind, QueryOptionsSpec, QueryVariableKind, TextVariableKind, @@ -38,27 +36,13 @@ import { DataLink, LibraryPanelKind, Element, - RepeatOptions, - GridLayoutRowKind, DashboardCursorSync, FieldConfig, FieldColor, - GridLayoutKind, - RowsLayoutKind, - ResponsiveGridLayoutKind, - ResponsiveGridLayoutItemKind, } from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha0'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { PanelTimeRange } from '../scene/PanelTimeRange'; -import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; -import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; -import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem'; -import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager'; -import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager'; -import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; -import { isClonedKey } from '../utils/clone'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getLibraryPanelBehavior, @@ -66,10 +50,9 @@ import { getQueryRunnerFor, getVizPanelKeyForPanelId, isLibraryPanel, - calculateGridItemDimensions, } from '../utils/utils'; -import { GRID_ROW_HEIGHT } from './const'; +import { getLayout } from './layoutSerializers/utils'; import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables'; import { colorIdEnumToColorIdV2, transformCursorSynctoEnum } from './transformToV2TypesUtils'; @@ -127,7 +110,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps // EOF annotations // layout - layout: getLayout(sceneDash.body, isSnapshot), + layout: getLayout(sceneDash.body), // EOF layout }; @@ -144,75 +127,6 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps } } -function getLayout( - layoutManager: DashboardLayoutManager, - isSnapshot?: boolean -): GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind { - if (layoutManager instanceof DefaultGridLayoutManager) { - return getGridLayout(layoutManager, isSnapshot); - } else if (layoutManager instanceof RowsLayoutManager) { - return { - kind: 'RowsLayout', - spec: { - rows: layoutManager.state.rows.map((row) => { - if (row.state.layout instanceof RowsLayoutManager) { - throw new Error('Nesting row layouts is not supported'); - } - let layout: GridLayoutKind | ResponsiveGridLayoutKind | undefined = undefined; - if (row.state.layout instanceof DefaultGridLayoutManager) { - layout = getGridLayout(row.state.layout, isSnapshot); - } else if (row.state.layout instanceof ResponsiveGridLayoutManager) { - layout = { - kind: 'ResponsiveGridLayout', - spec: { - items: getResponsiveGridLayoutItems(row.state.layout), - col: - row.state.layout.state.layout.state.templateColumns?.toString() ?? - ResponsiveGridLayoutManager.defaultCSS.templateColumns, - row: - row.state.layout.state.layout.state.autoRows?.toString() ?? - ResponsiveGridLayoutManager.defaultCSS.autoRows, - }, - }; - } - if (!layout) { - throw new Error('Unsupported layout type'); - } - return { - kind: 'RowsLayoutRow', - spec: { - title: row.state.title, - collapsed: row.state.isCollapsed ?? false, - layout: layout, - }, - }; - }), - }, - }; - } else if (layoutManager instanceof ResponsiveGridLayoutManager) { - return { - kind: 'ResponsiveGridLayout', - spec: { - items: getResponsiveGridLayoutItems(layoutManager), - col: - layoutManager.state.layout.state.templateColumns?.toString() ?? - ResponsiveGridLayoutManager.defaultCSS.templateColumns, - row: layoutManager.state.layout.state.autoRows?.toString() ?? ResponsiveGridLayoutManager.defaultCSS.autoRows, - }, - }; - } - throw new Error('Unsupported layout type'); -} - -function getGridLayout(layoutManager: DefaultGridLayoutManager, isSnapshot?: boolean): GridLayoutKind { - return { - kind: 'GridLayout', - spec: { - items: getGridLayoutItems(layoutManager, isSnapshot), - }, - }; -} - function getCursorSync(state: DashboardSceneState) { const cursorSync = state.$behaviors?.find((b): b is behaviors.CursorSync => b instanceof behaviors.CursorSync)?.state .sync; @@ -231,146 +145,6 @@ function getLiveNow(state: DashboardSceneState) { return Boolean(liveNow); } -function getGridLayoutItems( - body: DefaultGridLayoutManager, - isSnapshot?: boolean -): Array { - let elements: Array = []; - for (const child of body.state.grid.state.children) { - if (child instanceof DashboardGridItem) { - // TODO: handle panel repeater scenario - if (child.state.variableName) { - elements = elements.concat(repeaterToLayoutItems(child, isSnapshot)); - } else { - elements.push(gridItemToGridLayoutItemKind(child, isSnapshot)); - } - } else if (child instanceof SceneGridRow) { - if (isClonedKey(child.state.key!) && !isSnapshot) { - // Skip repeat rows - continue; - } - elements.push(gridRowToLayoutRowKind(child, isSnapshot)); - } - } - - return elements; -} - -function getResponsiveGridLayoutItems(body: ResponsiveGridLayoutManager): ResponsiveGridLayoutItemKind[] { - const items: ResponsiveGridLayoutItemKind[] = []; - - for (const child of body.state.layout.state.children) { - if (child instanceof ResponsiveGridItem) { - items.push({ - kind: 'ResponsiveGridLayoutItem', - spec: { - element: { - kind: 'ElementReference', - name: child.state?.body?.state.key ?? 'DefaultName', - }, - }, - }); - } - } - return items; -} - -export function gridItemToGridLayoutItemKind( - gridItem: DashboardGridItem, - isSnapshot = false, - yOverride?: number -): GridLayoutItemKind { - let elementGridItem: GridLayoutItemKind | undefined; - let x = 0, - y = 0, - width = 0, - height = 0; - - let gridItem_ = gridItem; - - if (!(gridItem_.state.body instanceof VizPanel)) { - throw new Error('DashboardGridItem body expected to be VizPanel'); - } - - // Get the grid position and size - height = (gridItem_.state.variableName ? gridItem_.state.itemHeight : gridItem_.state.height) ?? 0; - x = gridItem_.state.x ?? 0; - y = gridItem_.state.y ?? 0; - width = gridItem_.state.width ?? 0; - const repeatVar = gridItem_.state.variableName; - - // FIXME: which name should we use for the element reference, key or something else ? - const elementName = gridItem_.state.body.state.key ?? 'DefaultName'; - elementGridItem = { - kind: 'GridLayoutItem', - spec: { - x, - y: yOverride ?? y, - width: width, - height: height, - element: { - kind: 'ElementReference', - name: elementName, - }, - }, - }; - - if (repeatVar) { - const repeat: RepeatOptions = { - mode: 'variable', - value: repeatVar, - }; - - if (gridItem_.state.maxPerRow) { - repeat.maxPerRow = gridItem_.getMaxPerRow(); - } - - if (gridItem_.state.repeatDirection) { - repeat.direction = gridItem_.getRepeatDirection(); - } - - elementGridItem.spec.repeat = repeat; - } - - if (!elementGridItem) { - throw new Error('Unsupported grid item type'); - } - - return elementGridItem; -} - -function getRowRepeat(row: SceneGridRow): RepeatOptions | undefined { - if (row.state.$behaviors) { - for (const behavior of row.state.$behaviors) { - if (behavior instanceof RowRepeaterBehavior) { - return { value: behavior.state.variableName, mode: 'variable' }; - } - } - } - return undefined; -} - -function gridRowToLayoutRowKind(row: SceneGridRow, isSnapshot = false): GridLayoutRowKind { - const children = row.state.children.map((child) => { - if (!(child instanceof DashboardGridItem)) { - throw new Error('Unsupported row child type'); - } - const y = (child.state.y ?? 0) - (row.state.y ?? 0) - GRID_ROW_HEIGHT; - return gridItemToGridLayoutItemKind(child, isSnapshot, y); - }); - - return { - kind: 'GridLayoutRow', - spec: { - title: row.state.title, - y: row.state.y ?? 0, - collapsed: Boolean(row.state.isCollapsed), - elements: children, - repeat: getRowRepeat(row), - }, - }; -} - function getElements(state: DashboardSceneState) { const panels = state.body.getVizPanels() ?? []; @@ -577,60 +351,6 @@ function createElements(panels: Element[]): Record { }, {}); } -function repeaterToLayoutItems(repeater: DashboardGridItem, isSnapshot = false): GridLayoutItemKind[] { - if (!isSnapshot) { - return [gridItemToGridLayoutItemKind(repeater)]; - } else { - if (repeater.state.body instanceof VizPanel && isLibraryPanel(repeater.state.body)) { - // TODO: implement - // const { x = 0, y = 0, width: w = 0, height: h = 0 } = repeater.state; - // return [vizPanelToPanel(repeater.state.body, { x, y, w, h }, isSnapshot)]; - return []; - } - - if (repeater.state.repeatedPanels) { - const { h, w, columnCount } = calculateGridItemDimensions(repeater); - const panels = repeater.state.repeatedPanels!.map((panel, index) => { - let x = 0, - y = 0; - if (repeater.state.repeatDirection === 'v') { - x = repeater.state.x!; - y = index * h; - } else { - x = (index % columnCount) * w; - y = repeater.state.y! + Math.floor(index / columnCount) * h; - } - - const gridPos = { x, y, w, h }; - - const result: GridLayoutItemKind = { - kind: 'GridLayoutItem', - spec: { - x: gridPos.x, - y: gridPos.y, - width: gridPos.w, - height: gridPos.h, - repeat: { - mode: 'variable', - value: repeater.state.variableName!, - maxPerRow: repeater.getMaxPerRow(), - direction: repeater.state.repeatDirection, - }, - element: { - kind: 'ElementReference', - name: panel.state.key!, - }, - }, - }; - return result; - }); - - return panels; - } - return []; - } -} - function getVariables(oldDash: DashboardSceneState) { const variablesSet = oldDash.$variables; From e3cb73301e9e80e3711f85310599059bbb917162 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 11 Feb 2025 15:24:11 +0300 Subject: [PATCH 486/894] Authz/Schema: Remove unused stub cue definitions (#100387) --- kinds/accesspolicy/access_policy_kind.cue | 51 ------- kinds/role/role_kind.cue | 25 --- kinds/rolebinding/role_binding_kind.cue | 36 ----- packages/grafana-schema/src/index.gen.ts | 22 --- .../accesspolicy/x/accesspolicy_types.gen.ts | 60 -------- .../src/raw/role/x/role_types.gen.ts | 32 ---- .../rolebinding/x/rolebinding_types.gen.ts | 38 ----- pkg/kinds/accesspolicy/accesspolicy_gen.go | 43 ------ .../accesspolicy/accesspolicy_metadata_gen.go | 42 ----- .../accesspolicy/accesspolicy_spec_gen.go | 79 ---------- .../accesspolicy/accesspolicy_status_gen.go | 74 --------- pkg/kinds/accesspolicy/utils.go | 99 ------------ pkg/kinds/accesspolicy/utils_test.go | 68 --------- pkg/kinds/role/role_gen.go | 43 ------ pkg/kinds/role/role_metadata_gen.go | 42 ----- pkg/kinds/role/role_spec_gen.go | 30 ---- pkg/kinds/role/role_status_gen.go | 74 --------- pkg/kinds/rolebinding/rolebinding_gen.go | 43 ------ .../rolebinding/rolebinding_metadata_gen.go | 42 ----- pkg/kinds/rolebinding/rolebinding_spec_gen.go | 144 ------------------ .../rolebinding/rolebinding_status_gen.go | 74 --------- pkg/registry/schemas/core_kind.go | 27 ---- 22 files changed, 1188 deletions(-) delete mode 100644 kinds/accesspolicy/access_policy_kind.cue delete mode 100644 kinds/role/role_kind.cue delete mode 100644 kinds/rolebinding/role_binding_kind.cue delete mode 100644 packages/grafana-schema/src/raw/accesspolicy/x/accesspolicy_types.gen.ts delete mode 100644 packages/grafana-schema/src/raw/role/x/role_types.gen.ts delete mode 100644 packages/grafana-schema/src/raw/rolebinding/x/rolebinding_types.gen.ts delete mode 100644 pkg/kinds/accesspolicy/accesspolicy_gen.go delete mode 100644 pkg/kinds/accesspolicy/accesspolicy_metadata_gen.go delete mode 100644 pkg/kinds/accesspolicy/accesspolicy_spec_gen.go delete mode 100644 pkg/kinds/accesspolicy/accesspolicy_status_gen.go delete mode 100644 pkg/kinds/accesspolicy/utils.go delete mode 100644 pkg/kinds/accesspolicy/utils_test.go delete mode 100644 pkg/kinds/role/role_gen.go delete mode 100644 pkg/kinds/role/role_metadata_gen.go delete mode 100644 pkg/kinds/role/role_spec_gen.go delete mode 100644 pkg/kinds/role/role_status_gen.go delete mode 100644 pkg/kinds/rolebinding/rolebinding_gen.go delete mode 100644 pkg/kinds/rolebinding/rolebinding_metadata_gen.go delete mode 100644 pkg/kinds/rolebinding/rolebinding_spec_gen.go delete mode 100644 pkg/kinds/rolebinding/rolebinding_status_gen.go diff --git a/kinds/accesspolicy/access_policy_kind.cue b/kinds/accesspolicy/access_policy_kind.cue deleted file mode 100644 index ed9d85a7d82..00000000000 --- a/kinds/accesspolicy/access_policy_kind.cue +++ /dev/null @@ -1,51 +0,0 @@ -package kind - -name: "AccessPolicy" -maturity: "merged" -description: "Access rules for a scope+role. NOTE there is a unique constraint on role+scope" -pluralName: "AccessPolicies" -machineName: "accesspolicy" -pluralMachineName: "accesspolicies" - -lineage: schemas: [{ - version: [0, 0] - schema: { - spec: { - // The scope where these policies should apply - scope: #ResourceRef - - // The role that must apply this policy - role: #RoleRef - - // The set of rules to apply. Note that * is required to modify - // access policy rules, and that "none" will reject all actions - rules: [...#AccessRule] - } @cuetsy(kind="interface") - - #RoleRef: { - // Policies can apply to roles, teams, or users - // Applying policies to individual users is supported, but discouraged - kind: "Role" | "BuiltinRole" | "Team" | "User" - name: string - xname: string // temporary - } @cuetsy(kind="interface") - - #ResourceRef: { - kind: string // explicit resource or folder will cascade - name: string - } @cuetsy(kind="interface") - - #AccessRule: { - // The kind this rule applies to (dashboards, alert, etc) - kind: "*" | string - - // READ, WRITE, CREATE, DELETE, ... - // should move to k8s style verbs like: "get", "list", "watch", "create", "update", "patch", "delete" - verb: "*" | "none" | string - - // Specific sub-elements like "alert.rules" or "dashboard.permissions"???? - target?: string - } @cuetsy(kind="interface") - } -}, -] diff --git a/kinds/role/role_kind.cue b/kinds/role/role_kind.cue deleted file mode 100644 index 6e80d2abbde..00000000000 --- a/kinds/role/role_kind.cue +++ /dev/null @@ -1,25 +0,0 @@ -package kind - -name: "Role" -maturity: "merged" -description: "Roles represent a set of users+teams that should share similar access" - -lineage: schemas: [{ - version: [0, 0] - schema: { - spec: { - // The role identifier `managed:builtins:editor:permissions` - name: string - // Optional display - displayName?: string - // Name of the team. - groupName?: string - // Role description - description?: string - - // Do not show this role - hidden: bool | false - } @cuetsy(kind="interface") - } -}, -] diff --git a/kinds/rolebinding/role_binding_kind.cue b/kinds/rolebinding/role_binding_kind.cue deleted file mode 100644 index c76a633a721..00000000000 --- a/kinds/rolebinding/role_binding_kind.cue +++ /dev/null @@ -1,36 +0,0 @@ -package kind - -name: "RoleBinding" -maturity: "merged" -description: "Role bindings links a user|team to a configured role" - -lineage: schemas: [{ - version: [0, 0] - schema: { - spec: { - // The role we are discussing - role: #BuiltinRoleRef | #CustomRoleRef - - // The team or user that has the specified role - subject: #RoleBindingSubject - } @cuetsy(kind="interface") - - #CustomRoleRef: { - kind: "Role" - name: string - } @cuetsy(kind="interface") - - #BuiltinRoleRef: { - kind: "BuiltinRole" - name: "viewer" | "editor" | "admin" - } @cuetsy(kind="interface") - - #RoleBindingSubject: { - kind: "Team" | "User" - - // The team/user identifier name - name: string - } @cuetsy(kind="interface") - } -}, -] diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index d1d95f5c9ae..637094bd700 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -7,17 +7,6 @@ // // Run 'make gen-cue' from repository root to regenerate. -// Raw generated types from AccessPolicy kind. -export type { - AccessPolicy, - RoleRef, - ResourceRef, - AccessRule -} from './raw/accesspolicy/x/accesspolicy_types.gen'; - -// Raw generated enums and default consts from accesspolicy kind. -export { defaultAccessPolicy } from './raw/accesspolicy/x/accesspolicy_types.gen'; - // Raw generated types from Dashboard kind. export type { AnnotationTarget, @@ -135,14 +124,3 @@ export { defaultNavbarPreference } from './raw/preferences/x/preferences_types.g // Raw generated types from PublicDashboard kind. export type { PublicDashboard } from './raw/publicdashboard/x/publicdashboard_types.gen'; - -// Raw generated types from Role kind. -export type { Role } from './raw/role/x/role_types.gen'; - -// Raw generated types from RoleBinding kind. -export type { - RoleBinding, - CustomRoleRef, - BuiltinRoleRef, - RoleBindingSubject -} from './raw/rolebinding/x/rolebinding_types.gen'; diff --git a/packages/grafana-schema/src/raw/accesspolicy/x/accesspolicy_types.gen.ts b/packages/grafana-schema/src/raw/accesspolicy/x/accesspolicy_types.gen.ts deleted file mode 100644 index 7e18ea50777..00000000000 --- a/packages/grafana-schema/src/raw/accesspolicy/x/accesspolicy_types.gen.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// TSTypesJenny -// LatestMajorsOrXJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -export interface RoleRef { - /** - * Policies can apply to roles, teams, or users - * Applying policies to individual users is supported, but discouraged - */ - kind: ('Role' | 'BuiltinRole' | 'Team' | 'User'); - name: string; - xname: string; // temporary -} - -export interface ResourceRef { - kind: string; // explicit resource or folder will cascade - name: string; -} - -export interface AccessRule { - /** - * The kind this rule applies to (dashboards, alert, etc) - */ - kind: ('*' | string); - /** - * Specific sub-elements like "alert.rules" or "dashboard.permissions"???? - */ - target?: string; - /** - * READ, WRITE, CREATE, DELETE, ... - * should move to k8s style verbs like: "get", "list", "watch", "create", "update", "patch", "delete" - */ - verb: ('*' | 'none' | string); -} - -export interface AccessPolicy { - /** - * The role that must apply this policy - */ - role: RoleRef; - /** - * The set of rules to apply. Note that * is required to modify - * access policy rules, and that "none" will reject all actions - */ - rules: Array; - /** - * The scope where these policies should apply - */ - scope: ResourceRef; -} - -export const defaultAccessPolicy: Partial = { - rules: [], -}; diff --git a/packages/grafana-schema/src/raw/role/x/role_types.gen.ts b/packages/grafana-schema/src/raw/role/x/role_types.gen.ts deleted file mode 100644 index 88cadf639fe..00000000000 --- a/packages/grafana-schema/src/raw/role/x/role_types.gen.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// TSTypesJenny -// LatestMajorsOrXJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -export interface Role { - /** - * Role description - */ - description?: string; - /** - * Optional display - */ - displayName?: string; - /** - * Name of the team. - */ - groupName?: string; - /** - * Do not show this role - */ - hidden: (boolean | false); - /** - * The role identifier `managed:builtins:editor:permissions` - */ - name: string; -} diff --git a/packages/grafana-schema/src/raw/rolebinding/x/rolebinding_types.gen.ts b/packages/grafana-schema/src/raw/rolebinding/x/rolebinding_types.gen.ts deleted file mode 100644 index ea6d65fbf4e..00000000000 --- a/packages/grafana-schema/src/raw/rolebinding/x/rolebinding_types.gen.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// TSTypesJenny -// LatestMajorsOrXJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -export interface CustomRoleRef { - kind: 'Role'; - name: string; -} - -export interface BuiltinRoleRef { - kind: 'BuiltinRole'; - name: ('viewer' | 'editor' | 'admin'); -} - -export interface RoleBindingSubject { - kind: ('Team' | 'User'); - /** - * The team/user identifier name - */ - name: string; -} - -export interface RoleBinding { - /** - * The role we are discussing - */ - role: (BuiltinRoleRef | CustomRoleRef); - /** - * The team or user that has the specified role - */ - subject: RoleBindingSubject; -} diff --git a/pkg/kinds/accesspolicy/accesspolicy_gen.go b/pkg/kinds/accesspolicy/accesspolicy_gen.go deleted file mode 100644 index a528ab8ea36..00000000000 --- a/pkg/kinds/accesspolicy/accesspolicy_gen.go +++ /dev/null @@ -1,43 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package accesspolicy - -import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/kinds" -) - -// Resource is the kubernetes style representation of AccessPolicy. (TODO be better) -type K8sResource = kinds.GrafanaResource[Spec, Status] - -// NewResource creates a new instance of the resource with a given name (UID) -func NewK8sResource(name string, s *Spec) K8sResource { - return K8sResource{ - TypeMeta: v1.TypeMeta{ - Kind: "AccessPolicy", - APIVersion: "v0-0-alpha", - }, - ObjectMeta: v1.ObjectMeta{ - Name: name, - Annotations: make(map[string]string), - Labels: make(map[string]string), - }, - Spec: s, - } -} - -// Resource is the wire representation of AccessPolicy. -// It currently will soon be merged into the k8s flavor (TODO be better) -type Resource struct { - Metadata Metadata `json:"metadata"` - Spec Spec `json:"spec"` - Status Status `json:"status"` -} diff --git a/pkg/kinds/accesspolicy/accesspolicy_metadata_gen.go b/pkg/kinds/accesspolicy/accesspolicy_metadata_gen.go deleted file mode 100644 index 689f54c57d1..00000000000 --- a/pkg/kinds/accesspolicy/accesspolicy_metadata_gen.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package accesspolicy - -import ( - "time" -) - -// Metadata defines model for Metadata. -type Metadata struct { - CreatedBy string `json:"createdBy"` - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - - // extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - ExtraFields map[string]any `json:"extraFields"` - Finalizers []string `json:"finalizers"` - Labels map[string]string `json:"labels"` - ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` - UpdateTimestamp time.Time `json:"updateTimestamp"` - UpdatedBy string `json:"updatedBy"` -} - -// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -// It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -// As it is also intended to be generic enough to function with any API Server. -type KubeObjectMetadata struct { - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - Finalizers []string `json:"finalizers"` - Labels map[string]string `json:"labels"` - ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` -} diff --git a/pkg/kinds/accesspolicy/accesspolicy_spec_gen.go b/pkg/kinds/accesspolicy/accesspolicy_spec_gen.go deleted file mode 100644 index bb5d2de3235..00000000000 --- a/pkg/kinds/accesspolicy/accesspolicy_spec_gen.go +++ /dev/null @@ -1,79 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// GoResourceTypes -// -// Run 'make gen-cue' from repository root to regenerate. - -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package accesspolicy - -type ResourceRef struct { - // explicit resource or folder will cascade - Kind string `json:"kind"` - Name string `json:"name"` -} - -// NewResourceRef creates a new ResourceRef object. -func NewResourceRef() *ResourceRef { - return &ResourceRef{} -} - -type RoleRef struct { - // Policies can apply to roles, teams, or users - // Applying policies to individual users is supported, but discouraged - Kind RoleRefKind `json:"kind"` - Name string `json:"name"` - // temporary - Xname string `json:"xname"` -} - -// NewRoleRef creates a new RoleRef object. -func NewRoleRef() *RoleRef { - return &RoleRef{} -} - -type AccessRule struct { - // The kind this rule applies to (dashboards, alert, etc) - Kind string `json:"kind"` - // READ, WRITE, CREATE, DELETE, ... - // should move to k8s style verbs like: "get", "list", "watch", "create", "update", "patch", "delete" - Verb string `json:"verb"` - // Specific sub-elements like "alert.rules" or "dashboard.permissions"???? - Target *string `json:"target,omitempty"` -} - -// NewAccessRule creates a new AccessRule object. -func NewAccessRule() *AccessRule { - return &AccessRule{} -} - -type Spec struct { - // The scope where these policies should apply - Scope ResourceRef `json:"scope"` - // The role that must apply this policy - Role RoleRef `json:"role"` - // The set of rules to apply. Note that * is required to modify - // access policy rules, and that "none" will reject all actions - Rules []AccessRule `json:"rules"` -} - -// NewSpec creates a new Spec object. -func NewSpec() *Spec { - return &Spec{ - Scope: *NewResourceRef(), - Role: *NewRoleRef(), - } -} - -type RoleRefKind string - -const ( - RoleRefKindRole RoleRefKind = "Role" - RoleRefKindBuiltinRole RoleRefKind = "BuiltinRole" - RoleRefKindTeam RoleRefKind = "Team" - RoleRefKindUser RoleRefKind = "User" -) diff --git a/pkg/kinds/accesspolicy/accesspolicy_status_gen.go b/pkg/kinds/accesspolicy/accesspolicy_status_gen.go deleted file mode 100644 index 5101e417741..00000000000 --- a/pkg/kinds/accesspolicy/accesspolicy_status_gen.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package accesspolicy - -// Defines values for OperatorStateState. -const ( - OperatorStateStateFailed OperatorStateState = "failed" - OperatorStateStateInProgress OperatorStateState = "in_progress" - OperatorStateStateSuccess OperatorStateState = "success" -) - -// Defines values for StatusOperatorStateState. -const ( - StatusOperatorStateStateFailed StatusOperatorStateState = "failed" - StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" - StatusOperatorStateStateSuccess StatusOperatorStateState = "success" -) - -// OperatorState defines model for OperatorState. -type OperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]any `json:"details,omitempty"` - - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State OperatorStateState `json:"state"` -} - -// OperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. -type OperatorStateState string - -// Status defines model for Status. -type Status struct { - // additionalFields is reserved for future use - AdditionalFields map[string]any `json:"additionalFields,omitempty"` - - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"` -} - -// StatusOperatorState defines model for status.#OperatorState. -type StatusOperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]any `json:"details,omitempty"` - - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State StatusOperatorStateState `json:"state"` -} - -// StatusOperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. -type StatusOperatorStateState string diff --git a/pkg/kinds/accesspolicy/utils.go b/pkg/kinds/accesspolicy/utils.go deleted file mode 100644 index c043af90761..00000000000 --- a/pkg/kinds/accesspolicy/utils.go +++ /dev/null @@ -1,99 +0,0 @@ -package accesspolicy - -import ( - "sort" - - "github.com/grafana/grafana/pkg/util" -) - -const PermissionsTarget = "permissions" -const AllowAll = "*" -const AllowNone = "none" - -func ReduceRules(rules []AccessRule) []AccessRule { - type verbs struct { - Verb map[string][]string - Terminal string - } - - kinds := make(map[string]*verbs) - for _, rule := range rules { - if rule.Kind == "" || rule.Verb == "" { - continue // invalid - } - - // flip write permission to * - if rule.Target != nil && *rule.Target == PermissionsTarget { - if rule.Verb == "write" { - rule.Verb = AllowAll - } - } - kind, ok := kinds[rule.Kind] - if !ok { - kind = &verbs{ - Verb: make(map[string][]string), - } - kinds[rule.Kind] = kind - } - - terminal := rule.Verb == AllowAll || rule.Verb == AllowNone - if terminal { - if rule.Kind == AllowAll { - return []AccessRule{rule} - } - kind.Terminal = rule.Verb - } else if kind.Terminal == "" { - targets, ok := kind.Verb[rule.Verb] - if !ok { - targets = []string{} - } - if rule.Target != nil && !contains(targets, *rule.Target) { - targets = append(targets, *rule.Target) - sort.Strings(targets) - } - kind.Verb[rule.Verb] = targets - } - } - - results := make([]AccessRule, 0) - for _, kind := range getSortedKeys(kinds) { - verb := kinds[kind] - if verb.Terminal != "" { - results = append(results, AccessRule{Kind: kind, Verb: verb.Terminal}) - } else { - for _, v := range getSortedKeys(verb.Verb) { - targets := verb.Verb[v] - if len(targets) == 0 { - results = append(results, AccessRule{Kind: kind, Verb: v}) - } else { - for _, t := range targets { - results = append(results, AccessRule{ - Kind: kind, - Verb: v, - Target: util.Pointer(t), - }) - } - } - } - } - } - return results -} - -func getSortedKeys[T any](vals map[string]T) []string { - keys := make([]string, 0, len(vals)) - for k := range vals { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -func contains[T comparable](s []T, e T) bool { - for _, v := range s { - if v == e { - return true - } - } - return false -} diff --git a/pkg/kinds/accesspolicy/utils_test.go b/pkg/kinds/accesspolicy/utils_test.go deleted file mode 100644 index 314a8229346..00000000000 --- a/pkg/kinds/accesspolicy/utils_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package accesspolicy - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/grafana/grafana/pkg/util" - "github.com/stretchr/testify/require" -) - -func TestRuleReducer(t *testing.T) { - t.Run("Check write pointer becomes star", func(t *testing.T) { - rules := ReduceRules([]AccessRule{ - {Kind: "dashboard", Verb: "read"}, - {Kind: "dashboard", Verb: "write", Target: util.Pointer("permissions")}, - {Kind: "dashboard", Verb: "read"}, - }) - require.Len(t, rules, 1) - require.Equal(t, rules[0], AccessRule{Kind: "dashboard", Verb: "*"}) - }) - - t.Run("Check sort", func(t *testing.T) { - rules := ReduceRules([]AccessRule{ - {Kind: "x", Verb: "b"}, - {Kind: "x", Verb: "a"}, - {Kind: "x", Verb: "a"}, // ignore duplicates - {Kind: "x", Verb: "a"}, // ignore duplicates - {Kind: "x", Verb: "a"}, // ignore duplicates - {Kind: "x", Verb: "a"}, - {Kind: "z", Verb: "b"}, - {Kind: "AAA", Verb: ""}, // ignore - {Kind: "", Verb: "XXX"}, // ignore - {Kind: "z", Verb: "a"}, - {Kind: "y", Verb: "b"}, - {Kind: "y", Verb: "a"}, - }) - out, err := json.MarshalIndent(rules, "", " ") - fmt.Printf("%s", string(out)) - require.NoError(t, err) - require.JSONEq(t, `[ - { - "kind": "x", - "verb": "a" - }, - { - "kind": "x", - "verb": "b" - }, - { - "kind": "y", - "verb": "a" - }, - { - "kind": "y", - "verb": "b" - }, - { - "kind": "z", - "verb": "a" - }, - { - "kind": "z", - "verb": "b" - } - ]`, string(out)) - }) -} diff --git a/pkg/kinds/role/role_gen.go b/pkg/kinds/role/role_gen.go deleted file mode 100644 index c054e8c1773..00000000000 --- a/pkg/kinds/role/role_gen.go +++ /dev/null @@ -1,43 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package role - -import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/kinds" -) - -// Resource is the kubernetes style representation of Role. (TODO be better) -type K8sResource = kinds.GrafanaResource[Spec, Status] - -// NewResource creates a new instance of the resource with a given name (UID) -func NewK8sResource(name string, s *Spec) K8sResource { - return K8sResource{ - TypeMeta: v1.TypeMeta{ - Kind: "Role", - APIVersion: "v0-0-alpha", - }, - ObjectMeta: v1.ObjectMeta{ - Name: name, - Annotations: make(map[string]string), - Labels: make(map[string]string), - }, - Spec: s, - } -} - -// Resource is the wire representation of Role. -// It currently will soon be merged into the k8s flavor (TODO be better) -type Resource struct { - Metadata Metadata `json:"metadata"` - Spec Spec `json:"spec"` - Status Status `json:"status"` -} diff --git a/pkg/kinds/role/role_metadata_gen.go b/pkg/kinds/role/role_metadata_gen.go deleted file mode 100644 index 21bd45d3362..00000000000 --- a/pkg/kinds/role/role_metadata_gen.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package role - -import ( - "time" -) - -// Metadata defines model for Metadata. -type Metadata struct { - CreatedBy string `json:"createdBy"` - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - - // extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - ExtraFields map[string]any `json:"extraFields"` - Finalizers []string `json:"finalizers"` - Labels map[string]string `json:"labels"` - ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` - UpdateTimestamp time.Time `json:"updateTimestamp"` - UpdatedBy string `json:"updatedBy"` -} - -// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -// It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -// As it is also intended to be generic enough to function with any API Server. -type KubeObjectMetadata struct { - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - Finalizers []string `json:"finalizers"` - Labels map[string]string `json:"labels"` - ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` -} diff --git a/pkg/kinds/role/role_spec_gen.go b/pkg/kinds/role/role_spec_gen.go deleted file mode 100644 index c7a7123c983..00000000000 --- a/pkg/kinds/role/role_spec_gen.go +++ /dev/null @@ -1,30 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// GoResourceTypes -// -// Run 'make gen-cue' from repository root to regenerate. - -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package role - -type Spec struct { - // The role identifier `managed:builtins:editor:permissions` - Name string `json:"name"` - // Optional display - DisplayName *string `json:"displayName,omitempty"` - // Name of the team. - GroupName *string `json:"groupName,omitempty"` - // Role description - Description *string `json:"description,omitempty"` - // Do not show this role - Hidden bool `json:"hidden"` -} - -// NewSpec creates a new Spec object. -func NewSpec() *Spec { - return &Spec{} -} diff --git a/pkg/kinds/role/role_status_gen.go b/pkg/kinds/role/role_status_gen.go deleted file mode 100644 index ff9f44bdc5e..00000000000 --- a/pkg/kinds/role/role_status_gen.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package role - -// Defines values for OperatorStateState. -const ( - OperatorStateStateFailed OperatorStateState = "failed" - OperatorStateStateInProgress OperatorStateState = "in_progress" - OperatorStateStateSuccess OperatorStateState = "success" -) - -// Defines values for StatusOperatorStateState. -const ( - StatusOperatorStateStateFailed StatusOperatorStateState = "failed" - StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" - StatusOperatorStateStateSuccess StatusOperatorStateState = "success" -) - -// OperatorState defines model for OperatorState. -type OperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]any `json:"details,omitempty"` - - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State OperatorStateState `json:"state"` -} - -// OperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. -type OperatorStateState string - -// Status defines model for Status. -type Status struct { - // additionalFields is reserved for future use - AdditionalFields map[string]any `json:"additionalFields,omitempty"` - - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"` -} - -// StatusOperatorState defines model for status.#OperatorState. -type StatusOperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]any `json:"details,omitempty"` - - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State StatusOperatorStateState `json:"state"` -} - -// StatusOperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. -type StatusOperatorStateState string diff --git a/pkg/kinds/rolebinding/rolebinding_gen.go b/pkg/kinds/rolebinding/rolebinding_gen.go deleted file mode 100644 index 216bd3a9525..00000000000 --- a/pkg/kinds/rolebinding/rolebinding_gen.go +++ /dev/null @@ -1,43 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package rolebinding - -import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/kinds" -) - -// Resource is the kubernetes style representation of RoleBinding. (TODO be better) -type K8sResource = kinds.GrafanaResource[Spec, Status] - -// NewResource creates a new instance of the resource with a given name (UID) -func NewK8sResource(name string, s *Spec) K8sResource { - return K8sResource{ - TypeMeta: v1.TypeMeta{ - Kind: "RoleBinding", - APIVersion: "v0-0-alpha", - }, - ObjectMeta: v1.ObjectMeta{ - Name: name, - Annotations: make(map[string]string), - Labels: make(map[string]string), - }, - Spec: s, - } -} - -// Resource is the wire representation of RoleBinding. -// It currently will soon be merged into the k8s flavor (TODO be better) -type Resource struct { - Metadata Metadata `json:"metadata"` - Spec Spec `json:"spec"` - Status Status `json:"status"` -} diff --git a/pkg/kinds/rolebinding/rolebinding_metadata_gen.go b/pkg/kinds/rolebinding/rolebinding_metadata_gen.go deleted file mode 100644 index 2c2f4b28343..00000000000 --- a/pkg/kinds/rolebinding/rolebinding_metadata_gen.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package rolebinding - -import ( - "time" -) - -// Metadata defines model for Metadata. -type Metadata struct { - CreatedBy string `json:"createdBy"` - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - - // extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - ExtraFields map[string]any `json:"extraFields"` - Finalizers []string `json:"finalizers"` - Labels map[string]string `json:"labels"` - ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` - UpdateTimestamp time.Time `json:"updateTimestamp"` - UpdatedBy string `json:"updatedBy"` -} - -// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -// It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -// As it is also intended to be generic enough to function with any API Server. -type KubeObjectMetadata struct { - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - Finalizers []string `json:"finalizers"` - Labels map[string]string `json:"labels"` - ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` -} diff --git a/pkg/kinds/rolebinding/rolebinding_spec_gen.go b/pkg/kinds/rolebinding/rolebinding_spec_gen.go deleted file mode 100644 index 3d3149ef39d..00000000000 --- a/pkg/kinds/rolebinding/rolebinding_spec_gen.go +++ /dev/null @@ -1,144 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// GoResourceTypes -// -// Run 'make gen-cue' from repository root to regenerate. - -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package rolebinding - -import ( - json "encoding/json" - errors "errors" - fmt "fmt" -) - -type BuiltinRoleRef struct { - Kind string `json:"kind"` - Name BuiltinRoleRefName `json:"name"` -} - -// NewBuiltinRoleRef creates a new BuiltinRoleRef object. -func NewBuiltinRoleRef() *BuiltinRoleRef { - return &BuiltinRoleRef{ - Kind: "BuiltinRole", - } -} - -type CustomRoleRef struct { - Kind string `json:"kind"` - Name string `json:"name"` -} - -// NewCustomRoleRef creates a new CustomRoleRef object. -func NewCustomRoleRef() *CustomRoleRef { - return &CustomRoleRef{ - Kind: "Role", - } -} - -type RoleBindingSubject struct { - Kind RoleBindingSubjectKind `json:"kind"` - // The team/user identifier name - Name string `json:"name"` -} - -// NewRoleBindingSubject creates a new RoleBindingSubject object. -func NewRoleBindingSubject() *RoleBindingSubject { - return &RoleBindingSubject{} -} - -type Spec struct { - // The role we are discussing - Role BuiltinRoleRefOrCustomRoleRef `json:"role"` - // The team or user that has the specified role - Subject RoleBindingSubject `json:"subject"` -} - -// NewSpec creates a new Spec object. -func NewSpec() *Spec { - return &Spec{ - Role: *NewBuiltinRoleRefOrCustomRoleRef(), - Subject: *NewRoleBindingSubject(), - } -} - -type BuiltinRoleRefName string - -const ( - BuiltinRoleRefNameViewer BuiltinRoleRefName = "viewer" - BuiltinRoleRefNameEditor BuiltinRoleRefName = "editor" - BuiltinRoleRefNameAdmin BuiltinRoleRefName = "admin" -) - -type RoleBindingSubjectKind string - -const ( - RoleBindingSubjectKindTeam RoleBindingSubjectKind = "Team" - RoleBindingSubjectKindUser RoleBindingSubjectKind = "User" -) - -type BuiltinRoleRefOrCustomRoleRef struct { - BuiltinRoleRef *BuiltinRoleRef `json:"BuiltinRoleRef,omitempty"` - CustomRoleRef *CustomRoleRef `json:"CustomRoleRef,omitempty"` -} - -// NewBuiltinRoleRefOrCustomRoleRef creates a new BuiltinRoleRefOrCustomRoleRef object. -func NewBuiltinRoleRefOrCustomRoleRef() *BuiltinRoleRefOrCustomRoleRef { - return &BuiltinRoleRefOrCustomRoleRef{} -} - -// MarshalJSON implements a custom JSON marshalling logic to encode `BuiltinRoleRefOrCustomRoleRef` as JSON. -func (resource BuiltinRoleRefOrCustomRoleRef) MarshalJSON() ([]byte, error) { - if resource.BuiltinRoleRef != nil { - return json.Marshal(resource.BuiltinRoleRef) - } - if resource.CustomRoleRef != nil { - return json.Marshal(resource.CustomRoleRef) - } - - return nil, fmt.Errorf("no value for disjunction of refs") -} - -// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `BuiltinRoleRefOrCustomRoleRef` from JSON. -func (resource *BuiltinRoleRefOrCustomRoleRef) UnmarshalJSON(raw []byte) error { - if raw == nil { - return nil - } - - // FIXME: this is wasteful, we need to find a more efficient way to unmarshal this. - parsedAsMap := make(map[string]any) - if err := json.Unmarshal(raw, &parsedAsMap); err != nil { - return err - } - - discriminator, found := parsedAsMap["kind"] - if !found { - return errors.New("discriminator field 'kind' not found in payload") - } - - switch discriminator { - case "BuiltinRole": - var builtinRoleRef BuiltinRoleRef - if err := json.Unmarshal(raw, &builtinRoleRef); err != nil { - return err - } - - resource.BuiltinRoleRef = &builtinRoleRef - return nil - case "Role": - var customRoleRef CustomRoleRef - if err := json.Unmarshal(raw, &customRoleRef); err != nil { - return err - } - - resource.CustomRoleRef = &customRoleRef - return nil - } - - return fmt.Errorf("could not unmarshal resource with `kind = %v`", discriminator) -} diff --git a/pkg/kinds/rolebinding/rolebinding_status_gen.go b/pkg/kinds/rolebinding/rolebinding_status_gen.go deleted file mode 100644 index 1b4552df63d..00000000000 --- a/pkg/kinds/rolebinding/rolebinding_status_gen.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package rolebinding - -// Defines values for OperatorStateState. -const ( - OperatorStateStateFailed OperatorStateState = "failed" - OperatorStateStateInProgress OperatorStateState = "in_progress" - OperatorStateStateSuccess OperatorStateState = "success" -) - -// Defines values for StatusOperatorStateState. -const ( - StatusOperatorStateStateFailed StatusOperatorStateState = "failed" - StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" - StatusOperatorStateStateSuccess StatusOperatorStateState = "success" -) - -// OperatorState defines model for OperatorState. -type OperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]any `json:"details,omitempty"` - - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State OperatorStateState `json:"state"` -} - -// OperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. -type OperatorStateState string - -// Status defines model for Status. -type Status struct { - // additionalFields is reserved for future use - AdditionalFields map[string]any `json:"additionalFields,omitempty"` - - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"` -} - -// StatusOperatorState defines model for status.#OperatorState. -type StatusOperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]any `json:"details,omitempty"` - - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State StatusOperatorStateState `json:"state"` -} - -// StatusOperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. -type StatusOperatorStateState string diff --git a/pkg/registry/schemas/core_kind.go b/pkg/registry/schemas/core_kind.go index 1827acefff8..a262cc3f6f2 100644 --- a/pkg/registry/schemas/core_kind.go +++ b/pkg/registry/schemas/core_kind.go @@ -30,15 +30,6 @@ func GetCoreKinds() ([]CoreKind, error) { _, caller, _, _ := runtime.Caller(0) root := filepath.Join(caller, "../../../..") - accesspolicyCue, err := loadCueFile(ctx, filepath.Join(root, "./kinds/accesspolicy/access_policy_kind.cue")) - if err != nil { - return nil, err - } - kinds = append(kinds, CoreKind{ - Name: "accesspolicy", - CueFile: accesspolicyCue, - }) - dashboardCue, err := loadCueFile(ctx, filepath.Join(root, "./kinds/dashboard/dashboard_kind.cue")) if err != nil { return nil, err @@ -75,24 +66,6 @@ func GetCoreKinds() ([]CoreKind, error) { CueFile: publicdashboardCue, }) - roleCue, err := loadCueFile(ctx, filepath.Join(root, "./kinds/role/role_kind.cue")) - if err != nil { - return nil, err - } - kinds = append(kinds, CoreKind{ - Name: "role", - CueFile: roleCue, - }) - - rolebindingCue, err := loadCueFile(ctx, filepath.Join(root, "./kinds/rolebinding/role_binding_kind.cue")) - if err != nil { - return nil, err - } - kinds = append(kinds, CoreKind{ - Name: "rolebinding", - CueFile: rolebindingCue, - }) - return kinds, nil } From 5118e82e8c23b4a5088bb9d4ae68ab55a22a07de Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 11 Feb 2025 14:09:39 +0100 Subject: [PATCH 487/894] Zanzana: Run reconciliation in its own service (#100361) * Zanzana: Start reconciliation in its own service * cleanup * update go workspaces * refactor * remove unused code * move func definition --- go.work.sum | 1 + pkg/api/folder_bench_test.go | 3 +- .../backgroundsvcs/background_services.go | 6 +- pkg/server/wire.go | 3 + pkg/services/accesscontrol/accesscontrol.go | 1 - pkg/services/accesscontrol/acimpl/service.go | 20 +--- .../accesscontrol/acimpl/service_test.go | 1 - .../accesscontrol/dualwrite/reconciler.go | 41 ++++---- .../ossaccesscontrol/testutil/testutil.go | 3 +- .../extsvcaccounts/service_test.go | 2 +- pkg/storage/unified/resource/go.sum | 93 ------------------- 11 files changed, 36 insertions(+), 138 deletions(-) diff --git a/go.work.sum b/go.work.sum index d49e9e2b217..1be6908c9d8 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1647,6 +1647,7 @@ github.com/ionos-cloud/sdk-go/v6 v6.1.11/go.mod h1:EzEgRIDxBELvfoa/uBN0kOQaqovLj github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= +github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk= github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo= diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index c94de153484..d6645c1b945 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -27,7 +27,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" - "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" @@ -465,7 +464,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog nil, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) acSvc := acimpl.ProvideOSSService( sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(), - features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db, permreg.ProvidePermissionRegistry(), nil, folderServiceWithFlagOn, + features, tracing.InitializeTracerForTest(), sc.db, permreg.ProvidePermissionRegistry(), nil, folderServiceWithFlagOn, ) folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( cfg, features, routing.NewRouteRegister(), sc.db, ac, license, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets) diff --git a/pkg/registry/backgroundsvcs/background_services.go b/pkg/registry/backgroundsvcs/background_services.go index 8435b86fede..4c6bb02f89d 100644 --- a/pkg/registry/backgroundsvcs/background_services.go +++ b/pkg/registry/backgroundsvcs/background_services.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/registry" apiregistry "github.com/grafana/grafana/pkg/registry/apis" appregistry "github.com/grafana/grafana/pkg/registry/apps" - "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite" "github.com/grafana/grafana/pkg/services/anonymous/anonimpl" grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/auth" @@ -66,7 +66,7 @@ func ProvideBackgroundServiceRegistry( ssoSettings *ssosettingsimpl.Service, pluginExternal *pluginexternal.Service, pluginInstaller *plugininstaller.Service, - accessControl accesscontrol.Service, + zanzanaReconciler *dualwrite.ZanzanaReconciler, appRegistry *appregistry.Service, // Need to make sure these are initialized, is there a better place to put them? _ dashboardsnapshots.Service, @@ -111,7 +111,7 @@ func ProvideBackgroundServiceRegistry( ssoSettings, pluginExternal, pluginInstaller, - accessControl, + zanzanaReconciler, appRegistry, ) } diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 9f60a03f40f..f4e3f92e656 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -8,6 +8,7 @@ package server import ( "github.com/google/wire" + "github.com/grafana/grafana/pkg/storage/unified/resource" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" @@ -40,6 +41,7 @@ import ( appregistry "github.com/grafana/grafana/pkg/registry/apps" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" @@ -365,6 +367,7 @@ var wireBasicSet = wire.NewSet( wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, + dualwrite.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index a763de15613..7225b2545ae 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -32,7 +32,6 @@ type AccessControl interface { } type Service interface { - registry.BackgroundService registry.ProvidesUsageStats // GetRoleByName returns a role by name GetRoleByName(ctx context.Context, orgID int64, roleName string) (*RoleDTO, error) diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index d8cccb73c29..26f3e5cd70c 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -12,6 +12,7 @@ import ( "go.opentelemetry.io/otel/attribute" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" @@ -25,11 +26,9 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/api" "github.com/grafana/grafana/pkg/services/accesscontrol/database" - "github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite" "github.com/grafana/grafana/pkg/services/accesscontrol/migrator" "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" "github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils" - "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -54,7 +53,7 @@ var OSSRolesPrefixes = []string{accesscontrol.ManagedRolePrefix, accesscontrol.E func ProvideService( cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService, accessControl accesscontrol.AccessControl, userService user.Service, actionResolver accesscontrol.ActionResolver, - features featuremgmt.FeatureToggles, tracer tracing.Tracer, zclient zanzana.Client, permRegistry permreg.PermissionRegistry, + features featuremgmt.FeatureToggles, tracer tracing.Tracer, permRegistry permreg.PermissionRegistry, lock *serverlock.ServerLockService, folderService folder.Service, ) (*Service, error) { service := ProvideOSSService( @@ -64,7 +63,6 @@ func ProvideService( cache, features, tracer, - zclient, db, permRegistry, lock, @@ -90,8 +88,8 @@ func ProvideService( func ProvideOSSService( cfg *setting.Cfg, store accesscontrol.Store, actionResolver accesscontrol.ActionResolver, cache *localcache.CacheService, features featuremgmt.FeatureToggles, tracer tracing.Tracer, - zclient zanzana.Client, db db.DB, permRegistry permreg.PermissionRegistry, - lock *serverlock.ServerLockService, folderService folder.Service, + db db.DB, permRegistry permreg.PermissionRegistry, lock *serverlock.ServerLockService, + folderService folder.Service, ) *Service { s := &Service{ actionResolver: actionResolver, @@ -101,7 +99,6 @@ func ProvideOSSService( log: log.New("accesscontrol.service"), roles: accesscontrol.BuildBasicRoleDefinitions(), store: store, - reconciler: dualwrite.NewZanzanaReconciler(cfg, zclient, db, lock, folderService), permRegistry: permRegistry, } @@ -118,18 +115,9 @@ type Service struct { registrations accesscontrol.RegistrationList roles map[string]*accesscontrol.RoleDTO store accesscontrol.Store - reconciler *dualwrite.ZanzanaReconciler permRegistry permreg.PermissionRegistry } -// Run implements accesscontrol.Service. -func (s *Service) Run(ctx context.Context) error { - if s.features.IsEnabledGlobally(featuremgmt.FlagZanzana) { - return s.reconciler.Reconcile(ctx) - } - return nil -} - func (s *Service) GetUsageStats(_ context.Context) map[string]any { return map[string]any{ "stats.oss.accesscontrol.enabled.count": 1, diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go index 9b1d04ecd96..daaffdea54f 100644 --- a/pkg/services/accesscontrol/acimpl/service_test.go +++ b/pkg/services/accesscontrol/acimpl/service_test.go @@ -71,7 +71,6 @@ func TestUsageMetrics(t *testing.T) { featuremgmt.WithFeatures(), tracing.InitializeTracerForTest(), nil, - nil, permreg.ProvidePermissionRegistry(), nil, nil, diff --git a/pkg/services/accesscontrol/dualwrite/reconciler.go b/pkg/services/accesscontrol/dualwrite/reconciler.go index 70fe010fb3a..f6b34b81177 100644 --- a/pkg/services/accesscontrol/dualwrite/reconciler.go +++ b/pkg/services/accesscontrol/dualwrite/reconciler.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" @@ -25,12 +26,12 @@ var tracer = otel.Tracer("github.com/grafana/grafana/pkg/accesscontrol/migrator" // We should rewrite the migration after we have "migrated" all possible actions // into our schema. type ZanzanaReconciler struct { - cfg *setting.Cfg - log log.Logger - - store db.DB - client zanzana.Client - lock *serverlock.ServerLockService + cfg *setting.Cfg + log log.Logger + features featuremgmt.FeatureToggles + store db.DB + client zanzana.Client + lock *serverlock.ServerLockService // reconcilers are migrations that tries to reconcile the state of grafana db to zanzana store. // These are run periodically to try to maintain a consistent state. reconcilers []resourceReconciler @@ -38,13 +39,14 @@ type ZanzanaReconciler struct { globalReconcilers []resourceReconciler } -func NewZanzanaReconciler(cfg *setting.Cfg, client zanzana.Client, store db.DB, lock *serverlock.ServerLockService, folderService folder.Service) *ZanzanaReconciler { +func ProvideZanzanaReconciler(cfg *setting.Cfg, features featuremgmt.FeatureToggles, client zanzana.Client, store db.DB, lock *serverlock.ServerLockService, folderService folder.Service) *ZanzanaReconciler { zanzanaReconciler := &ZanzanaReconciler{ - cfg: cfg, - log: log.New("zanzana.reconciler"), - client: client, - lock: lock, - store: store, + cfg: cfg, + log: log.New("zanzana.reconciler"), + features: features, + client: client, + lock: lock, + store: store, reconcilers: []resourceReconciler{ newResourceReconciler( "team memberships", @@ -119,6 +121,14 @@ func NewZanzanaReconciler(cfg *setting.Cfg, client zanzana.Client, store db.DB, return zanzanaReconciler } +// Run implements registry.BackgroundService +func (r *ZanzanaReconciler) Run(ctx context.Context) error { + if r.features.IsEnabledGlobally(featuremgmt.FlagZanzana) { + return r.Reconcile(ctx) + } + return nil +} + // Reconcile schedules as job that will run and reconcile resources between // legacy access control and zanzana. func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error { @@ -137,13 +147,6 @@ func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error { } } -// ReconcileSync runs reconciliation and returns. Useful for tests to perform -// reconciliation in a synchronous way. -func (r *ZanzanaReconciler) ReconcileSync(ctx context.Context) error { - r.reconcile(ctx) - return nil -} - func (r *ZanzanaReconciler) reconcile(ctx context.Context) { runGlobal := func(ctx context.Context) { for _, reconciler := range r.globalReconcilers { diff --git a/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go b/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go index 9379bfb7abe..6bd3f39e6d5 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" - "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder/folderimpl" @@ -52,7 +51,7 @@ func ProvideFolderPermissions( acSvc := acimpl.ProvideOSSService( cfg, acdb.ProvideService(sqlStore), actionSets, localcache.ProvideService(), - features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sqlStore, permreg.ProvidePermissionRegistry(), + features, tracing.InitializeTracerForTest(), sqlStore, permreg.ProvidePermissionRegistry(), nil, fService, ) diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go index 71e45e37376..f0f2fe1d7f7 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go @@ -54,7 +54,7 @@ func setupTestEnv(t *testing.T) *TestEnv { enabled: true, acSvc: acimpl.ProvideOSSService( cfg, env.AcStore, &resourcepermissions.FakeActionSetSvc{}, - localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest(), nil, nil, + localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest(), nil, permreg.ProvidePermissionRegistry(), nil, nil, ), defaultOrgID: autoAssignOrgID, diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 8d74f2a39db..3bda999b30b 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -1,5 +1,3 @@ -cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0= -cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= @@ -61,8 +59,6 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+ github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= -github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= @@ -71,8 +67,6 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f h1:HR5nRmUQgXrwqZOwZ2DAc/aCi3Bu3xENpspW935vxu0= github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f/go.mod h1:f3HiCrHjHBdcm6E83vGaXh1KomZMA2P6aeo3hKx/wg0= -github.com/Yiling-J/theine-go v0.6.0 h1:jv7V/tcD6ijL0T4kfbJDKP81TCZBkoriNTPSqwivWuY= -github.com/Yiling-J/theine-go v0.6.0/go.mod h1:mdch1vjgGWd7s3rWKvY+MF5InRLfRv/CWVI9RVNQ8wY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -81,8 +75,6 @@ github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vS github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= -github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 h1:hXVi7QKuCQ0E8Yujfu9b0f0RnzZ72efpWvPnZgnJPrE= github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30/go.mod h1:RNuWDIiGjq5nndL2PyQrndUy9nMLwheA3uWaAV7fe4U= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -220,8 +212,6 @@ github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTE github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa h1:kyoPzxViSXAyqfO0Mab7Qo1UogFIrxZKKyBU6kBOl+E= github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= -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/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 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= @@ -230,15 +220,11 @@ 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/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= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -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/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= @@ -363,8 +349,6 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40= -github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -437,8 +421,6 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKt github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/sqlds/v4 v4.1.3 h1:+Hy5Yz+tSbD5N3yuLM0VKTsWlVaCzM1S1m1QEBZL7fE= github.com/grafana/sqlds/v4 v4.1.3/go.mod h1:Lx8IR939lIrCBpCKthv7AXs7E7bmNWPgt0gene/idT8= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 h1:kQ0NI7W1B3HwiN5gAYtY+XFItDPbLBwYRxAqbFTyDes= @@ -476,8 +458,6 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= @@ -488,16 +468,7 @@ github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= @@ -555,10 +526,6 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLgx3MUnGwJqpE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8= github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -567,8 +534,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= @@ -596,8 +561,6 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= -github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= -github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= @@ -639,10 +602,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= -github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk= -github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= @@ -651,8 +610,6 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -660,14 +617,6 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/openfga/api/proto v0.0.0-20250107154247-c22e6db5c4f5 h1:z9jaRoo+NIN1AB0ogjtrjx1316TTuq6IbqpEg3UJycA= -github.com/openfga/api/proto v0.0.0-20250107154247-c22e6db5c4f5/go.mod h1:m74TNgnAAIJ03gfHcx+xaRWnr+IbQy3y/AVNwwCFrC0= -github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c h1:1y84C0V4NRfPtRi4MqQ7+gnFtYgeBKPIeIAPLdVJ7j4= -github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c/go.mod h1:12RMe/HuRNyOzS33RQa53jwdcxE2znr8ycXMlVbgQN4= -github.com/openfga/openfga v1.8.4 h1:OqyRpuxMCxcS7irTFYFkhAIYzmAnczNwxUqjnuZOQyo= -github.com/openfga/openfga v1.8.4/go.mod h1:9Ax9VMMySV2JMsCT8MTePeYt4OrTnPAy1XUV1y9RyuU= -github.com/opentracing-contrib/go-stdlib v1.0.0 h1:TBS7YuVotp8myLon4Pv7BtCBzOTo1DeZCld0Z63mW2w= -github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= @@ -676,8 +625,6 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -692,8 +639,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/pressly/goose/v3 v3.24.0 h1:sFbNms7Bd++2VMq6HSgDHDLWa7kHz1qXzPb3ZIU72VU= -github.com/pressly/goose/v3 v3.24.0/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug= github.com/prometheus/alertmanager v0.27.0 h1:V6nTa2J5V4s8TG4C4HtrBP/WNSebCCTYGGv4qecA/+I= github.com/prometheus/alertmanager v0.27.0/go.mod h1:8Ia/R3urPmbzJ8OsdvmZvIprDwvwmYCmUbwBL+jlPOE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -727,8 +672,6 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -739,15 +682,9 @@ github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= -github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= @@ -765,20 +702,12 @@ github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PX github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -798,8 +727,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= @@ -884,12 +811,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= gocloud.dev v0.40.0 h1:f8LgP+4WDqOG/RXoUcyLpeIAGOcAbZrZbDQCUee10ng= gocloud.dev v0.40.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1151,20 +1072,6 @@ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= -modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= -modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= -modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= -modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8= -modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= From 43d7d0024796d5a4dcaca24f70dbd5245dac6ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Tue, 11 Feb 2025 14:13:42 +0100 Subject: [PATCH 488/894] Combobox: fix check for existing options when creating a custom value (#100123) --- .../src/components/Combobox/Combobox.test.tsx | 14 +++++++++++++- .../src/components/Combobox/Combobox.tsx | 6 +++--- .../src/components/Combobox/useOptions.ts | 2 ++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx index e99a2568faa..6127c007e56 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx @@ -243,7 +243,19 @@ describe('Combobox', () => { await userEvent.keyboard('{Enter}'); expect(screen.getByDisplayValue('Use custom value')).toBeInTheDocument(); - expect(onChangeHandler).toHaveBeenCalledWith(expect.objectContaining({ value: 'Use custom value' })); + expect(onChangeHandler).toHaveBeenCalledWith(expect.objectContaining({ description: 'Use custom value' })); + }); + + it('should not allow creating a custom value when it is an existing value', async () => { + const onChangeHandler = jest.fn(); + render(); + const input = screen.getByRole('combobox'); + await userEvent.type(input, '4'); + await userEvent.keyboard('{Enter}'); + expect(screen.queryByDisplayValue('Use custom value')).not.toBeInTheDocument(); + expect(screen.getByDisplayValue('Option 4')).toBeInTheDocument(); + expect(onChangeHandler).toHaveBeenCalledWith(expect.objectContaining({ value: '4' })); + expect(onChangeHandler).not.toHaveBeenCalledWith(expect.objectContaining({ description: 'Use custom value' })); }); it('should provide custom string when all options are numbers', async () => { diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index 4c2f6a97b63..471bb22fe5d 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -129,9 +129,9 @@ export const Combobox = (props: ComboboxProps) => let itemsToSet = items; logOptions(itemsToSet.length, RECOMMENDED_ITEMS_AMOUNT, id, ariaLabelledBy); if (inputValue && createCustomValue) { - const optionMatchingInput = items.find( - (opt) => opt.label === 'Custom value: ' + inputValue || opt.value === inputValue - ); + //Since the label of a normal option does not have to match its value and a custom option has the same value and label, + //we just focus on the value to check if the option already exists + const optionMatchingInput = items.find((opt) => opt.value === inputValue); if (!optionMatchingInput) { const customValueOption = { diff --git a/packages/grafana-ui/src/components/Combobox/useOptions.ts b/packages/grafana-ui/src/components/Combobox/useOptions.ts index a66e60a2747..b76339cf54e 100644 --- a/packages/grafana-ui/src/components/Combobox/useOptions.ts +++ b/packages/grafana-ui/src/components/Combobox/useOptions.ts @@ -62,6 +62,8 @@ export function useOptions(rawOptions: AsyncOptions>) => { let currentOptions: Array> = opts; if (createCustomValue && userTypedSearch) { + //Since the label of a normal option does not have to match its value and a custom option has the same value and label, + //we just focus on the value to check if the option already exists const customValueExists = opts.some((opt) => opt.value === userTypedSearch); if (!customValueExists) { currentOptions = [ From bbe21bb1d2d48931b06eb0b4d9a3e747c3dc4f44 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:20:47 +0000 Subject: [PATCH 489/894] Bump Scenes to 5.42.0 (#100205) * update scenes and weekStart prop type * update test * adjust weekStart type in schemaV2 * clean up weekStart error message * clean up weekStart in test data --- .betterer.results | 8 +++++-- package.json | 4 ++-- .../dashboard/v2alpha0/dashboard.schema.cue | 2 +- .../schema/dashboard/v2alpha0/types.gen.ts | 3 +-- .../DateTimePickers/WeekStartPicker.tsx | 4 ++-- .../DashboardScenePageStateManager.test.ts | 1 - .../DashboardSceneSerializer.test.ts | 2 -- .../transformSceneToSaveModelSchemaV2.ts | 8 +++++-- .../settings/GeneralSettingsEditView.tsx | 3 ++- ...DashboardModelCompatibilityWrapper.test.ts | 4 ++-- .../dashboard/api/ResponseTransformers.ts | 4 +++- .../DashboardSettings/TimePickerSettings.tsx | 6 ++--- yarn.lock | 22 +++++++++---------- 13 files changed, 39 insertions(+), 32 deletions(-) diff --git a/.betterer.results b/.betterer.results index bb9b2860e3b..9e1de13c1be 100644 --- a/.betterer.results +++ b/.betterer.results @@ -632,6 +632,9 @@ exports[`better eslint`] = { [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/DateTimePickers/WeekStartPicker.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "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"] ], @@ -3655,9 +3658,10 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"], [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"] + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"] ], "public/app/features/dashboard/api/v0.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/package.json b/package.json index c3d81624ad9..5770dbd27e4 100644 --- a/package.json +++ b/package.json @@ -276,8 +276,8 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "5.41.2", - "@grafana/scenes-react": "5.41.2", + "@grafana/scenes": "5.42.0", + "@grafana/scenes-react": "5.42.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 9286c13c003..4b983ddf7ae 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -467,7 +467,7 @@ TimeSettingsSpec: { // Whether timepicker is visible or not. hideTimepicker: bool // v1: timepicker.hidden // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". - weekStart: string + weekStart?: "saturday" | "monday" | "sunday" // The month that the fiscal year starts on. 0 = January, 11 = December fiscalYearStartMonth: int // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index 70262bbfdd5..83c733c6f60 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -644,7 +644,7 @@ export interface TimeSettingsSpec { // v1: timepicker.hidden hideTimepicker: boolean; // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". - weekStart: string; + weekStart?: "saturday" | "monday" | "sunday"; // The month that the fiscal year starts on. 0 = January, 11 = December fiscalYearStartMonth: number; // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. @@ -681,7 +681,6 @@ export const defaultTimeSettingsSpec = (): TimeSettingsSpec => ({ "30d", ], hideTimepicker: false, - weekStart: "", fiscalYearStartMonth: 0, }); diff --git a/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx index bd047f8be99..aebcc7075d2 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx @@ -7,7 +7,7 @@ import { Combobox } from '../Combobox/Combobox'; import { ComboboxOption } from '../Combobox/types'; export interface Props { - onChange: (weekStart: string) => void; + onChange: (weekStart: WeekStart) => void; value: string; width?: number; autoFocus?: boolean; @@ -57,7 +57,7 @@ export const WeekStartPicker = (props: Props) => { const onChangeWeekStart = useCallback( (selectable: ComboboxOption | null) => { if (selectable && selectable.value !== undefined) { - onChange(selectable.value); + onChange(selectable.value as WeekStart); } }, [onChange] diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index d885e17fdda..2e46767d485 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -663,7 +663,6 @@ const customHomeDashboardV2Spec = { autoRefreshIntervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], quickRanges: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'], hideTimepicker: false, - weekStart: '', fiscalYearStartMonth: 0, }, variables: [], diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index 27956a2bbd7..8f09578ae54 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -586,7 +586,6 @@ describe('DashboardSceneSerializer', () => { autoRefreshIntervals: [], quickRanges: [], hideTimepicker: false, - weekStart: '', fiscalYearStartMonth: 0, timezone: '', }, @@ -650,7 +649,6 @@ describe('DashboardSceneSerializer', () => { quickRanges: [], timezone: 'browser', to: 'now', - weekStart: '', }); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 59b44050d81..bd8ed9f3fac 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -508,8 +508,12 @@ function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec { if (!('hideTimepicker' in dash.timeSettings) || typeof dash.timeSettings.hideTimepicker !== 'boolean') { throw new Error('HideTimepicker is not a boolean'); } - if (!('weekStart' in dash.timeSettings) || typeof dash.timeSettings.weekStart !== 'string') { - throw new Error('WeekStart is not a string'); + if ( + 'weekStart' in dash.timeSettings && + typeof dash.timeSettings.weekStart === 'string' && + !['saturday', 'sunday', 'monday'].includes(dash.timeSettings.weekStart) + ) { + throw new Error('WeekStart should be one of "saturday", "sunday" or "monday"'); } if (!('fiscalYearStartMonth' in dash.timeSettings) || typeof dash.timeSettings.fiscalYearStartMonth !== 'number') { throw new Error('FiscalYearStartMonth is not a number'); diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx index 8c98ce445b0..ef7ae0489e6 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx @@ -15,6 +15,7 @@ import { Switch, TagsInput, TextArea, + WeekStart, } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { FolderPicker } from 'app/core/components/Select/FolderPicker'; @@ -122,7 +123,7 @@ export class GeneralSettingsEditView }); }; - public onWeekStartChange = (value: string) => { + public onWeekStartChange = (value: WeekStart) => { this.getTimeRange().setState({ weekStart: value }); }; diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts index a4aa7dc8426..e8ce98ce08a 100644 --- a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts +++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts @@ -26,7 +26,7 @@ describe('DashboardModelCompatibilityWrapper', () => { expect(wrapper.links).toEqual([NEW_LINK]); expect(wrapper.time.from).toBe('now-6h'); expect(wrapper.timezone).toBe('America/New_York'); - expect(wrapper.weekStart).toBe('friday'); + expect(wrapper.weekStart).toBe('saturday'); expect(wrapper.timepicker.refresh_intervals![0]).toEqual('5s'); expect(wrapper.timepicker.hidden).toEqual(true); expect(wrapper.panels).toHaveLength(5); @@ -141,7 +141,7 @@ function setup() { }, }, $timeRange: new SceneTimeRange({ - weekStart: 'friday', + weekStart: 'saturday', timeZone: 'America/New_York', }), $data: new DashboardDataLayerSet({ diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index af0b15ff2e5..b4841435f3a 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -41,6 +41,7 @@ import { GridLayoutItemKind, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { DashboardLink, DataTransformerConfig } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen'; +import { WeekStart } from '@grafana/ui'; import { AnnoKeyCreatedBy, AnnoKeyDashboardGnetId, @@ -160,7 +161,8 @@ export function ensureV2Response( fiscalYearStartMonth: dashboard.fiscalYearStartMonth || timeSettingsDefaults.fiscalYearStartMonth, hideTimepicker: dashboard.timepicker?.hidden || timeSettingsDefaults.hideTimepicker, quickRanges: dashboard.timepicker?.time_options || timeSettingsDefaults.quickRanges, - weekStart: dashboard.weekStart || timeSettingsDefaults.weekStart, + // casting WeekStart here to avoid editing old schema + weekStart: (dashboard.weekStart as WeekStart) || timeSettingsDefaults.weekStart, nowDelay: dashboard.timepicker?.nowDelay || timeSettingsDefaults.nowDelay, }, links: dashboard.links || [], diff --git a/public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx index d7d35e21ab2..d88e3deaa64 100644 --- a/public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx @@ -4,13 +4,13 @@ import * as React from 'react'; import { rangeUtil, TimeZone } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { CollapsableSection, Field, Input, Switch, TimeZonePicker, WeekStartPicker } from '@grafana/ui'; +import { CollapsableSection, Field, Input, Switch, TimeZonePicker, WeekStart, WeekStartPicker } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { AutoRefreshIntervals } from './AutoRefreshIntervals'; interface Props { - onWeekStartChange: (weekStart: string) => void; + onWeekStartChange: (weekStart: WeekStart) => void; onTimeZoneChange: (timeZone: TimeZone) => void; onRefreshIntervalChange: (interval: string[]) => void; onNowDelayChange: (nowDelay: string) => void; @@ -62,7 +62,7 @@ export class TimePickerSettings extends PureComponent { this.props.onTimeZoneChange(timeZone); }; - onWeekStartChange = (weekStart: string) => { + onWeekStartChange = (weekStart: WeekStart) => { this.props.onWeekStartChange(weekStart); }; diff --git a/yarn.lock b/yarn.lock index 4f4260e4e94..ac12d0c5190 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3831,11 +3831,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:5.41.2": - version: 5.41.2 - resolution: "@grafana/scenes-react@npm:5.41.2" +"@grafana/scenes-react@npm:5.42.0": + version: 5.42.0 + resolution: "@grafana/scenes-react@npm:5.42.0" dependencies: - "@grafana/scenes": "npm:5.41.2" + "@grafana/scenes": "npm:5.42.0" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3846,13 +3846,13 @@ __metadata: "@grafana/ui": ^11.0.0 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/43684bb4d169666b9edb44f5804eb533fcba531b6d161837306def3611bfa606ea2fb3ad2a9aa374ec7d6757d7560df55217593927c0874254d3a0cae8a215f0 + checksum: 10/c94db6d57b02be5f960e44dc0c4be46e5e4708ede34f2f4e30934134543db88c8b553aceb89d9b3ae4dbf5d1eab1f48e534eb9a31d5c99b8908f715e45cb6dcf languageName: node linkType: hard -"@grafana/scenes@npm:5.41.2": - version: 5.41.2 - resolution: "@grafana/scenes@npm:5.41.2" +"@grafana/scenes@npm:5.42.0": + version: 5.42.0 + resolution: "@grafana/scenes@npm:5.42.0" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3869,7 +3869,7 @@ __metadata: "@grafana/ui": ">=10.4" react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/68d6f722e7298123271785f0c38d47f9b1c000ad51bc1f4bfdd90b6ad83d17e69a18261263f4e9cbda6c7062a72514cd91a22e7189b3a0429f28b96f7f928dc1 + checksum: 10/3232a499b839a45c8eed924441e4423d1b1d9f6b4ea6725437d357c1afea93d9ab300c46319b1ddab76b0da1197905b22c3abe8095d78c3320b1befb4731bd24 languageName: node linkType: hard @@ -18166,8 +18166,8 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:5.41.2" - "@grafana/scenes-react": "npm:5.41.2" + "@grafana/scenes": "npm:5.42.0" + "@grafana/scenes-react": "npm:5.42.0" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0" From 7234a17d1d1cd6412033b24ca665a8b3d0754352 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 11 Feb 2025 14:25:30 +0100 Subject: [PATCH 490/894] Zanzana: Use authzService audience (#100417) --- pkg/services/authz/zanzana.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index ea3a95d7603..1327689f1b1 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -18,6 +18,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" claims "github.com/grafana/authlib/types" "github.com/grafana/dskit/services" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -29,8 +30,6 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -const zanzanaAudience = "zanzana" - // ProvideZanzana used to register ZanzanaClient. // It will also start an embedded ZanzanaSever if mode is set to "embedded". func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureToggles) (zanzana.Client, error) { @@ -179,7 +178,7 @@ func (z *Zanzana) start(ctx context.Context) error { authenticator := authnlib.NewAccessTokenAuthenticator( authnlib.NewAccessTokenVerifier( authnlib.VerifierConfig{ - AllowedAudiences: []string{zanzanaAudience}, + AllowedAudiences: []string{authzServiceAudience}, }, authnlib.NewKeyRetriever(authnlib.KeyRetrieverConfig{ SigningKeysURL: z.cfg.ZanzanaServer.SigningKeysURL, @@ -255,7 +254,7 @@ type tokenAuth struct { func (t *tokenAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) { token, err := t.tokenClient.Exchange(ctx, authnlib.TokenExchangeRequest{ Namespace: t.namespace, - Audiences: []string{zanzanaAudience}, + Audiences: []string{authzServiceAudience}, }) if err != nil { return nil, err From 8c0e087ce2c7a475cb94f9518a501619fa9f1d1e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:04:18 +0200 Subject: [PATCH 491/894] Update dependency esbuild to v0.25.0 [SECURITY] (#100426) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-icons/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-schema/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 342 ++++++++++++++++---- 10 files changed, 286 insertions(+), 74 deletions(-) diff --git a/package.json b/package.json index 5770dbd27e4..08a515c120c 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ "cypress": "13.10.0", "cypress-file-upload": "5.0.8", "cypress-recurse": "^1.35.3", - "esbuild": "0.24.2", + "esbuild": "0.25.0", "esbuild-loader": "4.2.2", "esbuild-plugin-browserslist": "^0.15.0", "eslint": "9.19.0", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 2f118f5c4bf..e5c550ba2b6 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -70,7 +70,7 @@ "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/tinycolor2": "1.4.6", - "esbuild": "0.24.2", + "esbuild": "0.25.0", "react": "18.3.1", "react-dom": "18.3.1", "rimraf": "6.0.1", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 261f8d89a18..4d40c9d558c 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -42,7 +42,7 @@ "@rollup/plugin-node-resolve": "16.0.0", "@types/node": "22.12.0", "@types/semver": "7.5.8", - "esbuild": "0.24.2", + "esbuild": "0.25.0", "rimraf": "6.0.1", "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.1", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index b45e8137f03..9f6ff3d3d25 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -73,7 +73,7 @@ "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", - "esbuild": "0.24.2", + "esbuild": "0.25.0", "jest": "^29.6.4", "jest-canvas-mock": "2.5.2", "rollup": "^4.22.4", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index f5f41fdae68..d75fea57ca5 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -48,7 +48,7 @@ "@types/node": "22.12.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", - "esbuild": "0.24.2", + "esbuild": "0.25.0", "prettier": "3.4.2", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 27577e85c5b..dd26bced034 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -106,7 +106,7 @@ "@typescript-eslint/parser": "8.22.0", "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", - "esbuild": "0.24.2", + "esbuild": "0.25.0", "eslint": "9.19.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 9a83e3cde67..4bb413f2495 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -62,7 +62,7 @@ "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/systemjs": "6.15.1", - "esbuild": "0.24.2", + "esbuild": "0.25.0", "lodash": "4.17.21", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index ad5f1622b94..9975c9548fc 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -38,7 +38,7 @@ "devDependencies": { "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "16.0.0", - "esbuild": "0.24.2", + "esbuild": "0.25.0", "glob": "^11.0.0", "rimraf": "6.0.1", "rollup": "^4.22.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index b693a8fb5ac..bcbb20a3757 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -163,7 +163,7 @@ "core-js": "3.40.0", "css-loader": "7.1.2", "csstype": "3.1.3", - "esbuild": "0.24.2", + "esbuild": "0.25.0", "expose-loader": "5.0.0", "fs-extra": "^11.2.0", "mock-raf": "1.0.1", diff --git a/yarn.lock b/yarn.lock index ac12d0c5190..e13c19a5289 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2081,6 +2081,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/aix-ppc64@npm:0.25.0" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm64@npm:0.21.5" @@ -2095,6 +2102,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/android-arm64@npm:0.25.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm@npm:0.21.5" @@ -2109,6 +2123,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/android-arm@npm:0.25.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-x64@npm:0.21.5" @@ -2123,6 +2144,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/android-x64@npm:0.25.0" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-arm64@npm:0.21.5" @@ -2137,6 +2165,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/darwin-arm64@npm:0.25.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-x64@npm:0.21.5" @@ -2151,6 +2186,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/darwin-x64@npm:0.25.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-arm64@npm:0.21.5" @@ -2165,6 +2207,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/freebsd-arm64@npm:0.25.0" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-x64@npm:0.21.5" @@ -2179,6 +2228,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/freebsd-x64@npm:0.25.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm64@npm:0.21.5" @@ -2193,6 +2249,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/linux-arm64@npm:0.25.0" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm@npm:0.21.5" @@ -2207,6 +2270,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/linux-arm@npm:0.25.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ia32@npm:0.21.5" @@ -2221,6 +2291,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/linux-ia32@npm:0.25.0" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-loong64@npm:0.21.5" @@ -2235,6 +2312,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/linux-loong64@npm:0.25.0" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-mips64el@npm:0.21.5" @@ -2249,6 +2333,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/linux-mips64el@npm:0.25.0" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ppc64@npm:0.21.5" @@ -2263,6 +2354,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/linux-ppc64@npm:0.25.0" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-riscv64@npm:0.21.5" @@ -2277,6 +2375,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/linux-riscv64@npm:0.25.0" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-s390x@npm:0.21.5" @@ -2291,6 +2396,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/linux-s390x@npm:0.25.0" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-x64@npm:0.21.5" @@ -2305,6 +2417,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/linux-x64@npm:0.25.0" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/netbsd-arm64@npm:0.24.2" @@ -2312,6 +2431,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-arm64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/netbsd-arm64@npm:0.25.0" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/netbsd-x64@npm:0.21.5" @@ -2326,6 +2452,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/netbsd-x64@npm:0.25.0" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/openbsd-arm64@npm:0.24.2" @@ -2333,6 +2466,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-arm64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/openbsd-arm64@npm:0.25.0" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/openbsd-x64@npm:0.21.5" @@ -2347,6 +2487,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/openbsd-x64@npm:0.25.0" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/sunos-x64@npm:0.21.5" @@ -2361,6 +2508,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/sunos-x64@npm:0.25.0" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-arm64@npm:0.21.5" @@ -2375,6 +2529,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/win32-arm64@npm:0.25.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-ia32@npm:0.21.5" @@ -2389,6 +2550,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/win32-ia32@npm:0.25.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-x64@npm:0.21.5" @@ -2403,6 +2571,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.25.0": + version: 0.25.0 + resolution: "@esbuild/win32-x64@npm:0.25.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -3214,7 +3389,7 @@ __metadata: d3-interpolate: "npm:3.0.1" date-fns: "npm:4.1.0" dompurify: "npm:3.2.4" - esbuild: "npm:0.24.2" + esbuild: "npm:0.25.0" eventemitter3: "npm:5.0.1" fast_array_intersect: "npm:1.1.0" history: "npm:4.10.1" @@ -3265,7 +3440,7 @@ __metadata: "@rollup/plugin-node-resolve": "npm:16.0.0" "@types/node": "npm:22.12.0" "@types/semver": "npm:7.5.8" - esbuild: "npm:0.24.2" + esbuild: "npm:0.25.0" rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" rollup-plugin-dts: "npm:^6.1.1" @@ -3311,27 +3486,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/faro-core@npm:^1.12.3": - version: 1.12.3 - resolution: "@grafana/faro-core@npm:1.12.3" - dependencies: - "@opentelemetry/api": "npm:^1.9.0" - "@opentelemetry/otlp-transformer": "npm:^0.57.1" - checksum: 10/616592a8e65c72be763f27baa3a44c228fc61aeff9cd6c895dac7b4adc24a836accc4766d574a3b5156896b3068c8d7df51c1d715679f3f1a1feef90dcaf82e3 - languageName: node - linkType: hard - -"@grafana/faro-core@npm:^1.13.1": - version: 1.13.1 - resolution: "@grafana/faro-core@npm:1.13.1" - dependencies: - "@opentelemetry/api": "npm:^1.9.0" - "@opentelemetry/otlp-transformer": "npm:^0.57.1" - checksum: 10/ce3747c476bd2b0f2b07c808c584953de75bf970fc17fbeb27258decf86919a753472cc464a4dccd4e0091003232feec6d3ba07f2e1b7bc38b487d9199abd18f - languageName: node - linkType: hard - -"@grafana/faro-core@npm:^1.13.2": +"@grafana/faro-core@npm:^1.13.1, @grafana/faro-core@npm:^1.13.2": version: 1.13.2 resolution: "@grafana/faro-core@npm:1.13.2" dependencies: @@ -3341,7 +3496,7 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:^1.13.2": +"@grafana/faro-web-sdk@npm:^1.13.2, @grafana/faro-web-sdk@npm:^1.3.6": version: 1.13.2 resolution: "@grafana/faro-web-sdk@npm:1.13.2" dependencies: @@ -3352,17 +3507,6 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:^1.3.6": - version: 1.12.3 - resolution: "@grafana/faro-web-sdk@npm:1.12.3" - dependencies: - "@grafana/faro-core": "npm:^1.12.3" - ua-parser-js: "npm:^1.0.32" - web-vitals: "npm:^4.0.1" - checksum: 10/433b118ff5b8bc231e209902c20b8a645efe6efd9ecc4ef57947111fa076842becc9760b5bcc8957c1f04cd20c801241ba9cca0a6d4637b8aa2b034d7a7c1888 - languageName: node - linkType: hard - "@grafana/faro-web-tracing@npm:^1.13.2": version: 1.13.2 resolution: "@grafana/faro-web-tracing@npm:1.13.2" @@ -3409,7 +3553,7 @@ __metadata: "@types/tinycolor2": "npm:1.4.6" babel-jest: "npm:29.7.0" d3: "npm:^7.8.5" - esbuild: "npm:0.24.2" + esbuild: "npm:0.25.0" jest: "npm:^29.6.4" jest-canvas-mock: "npm:2.5.2" lodash: "npm:4.17.21" @@ -3676,7 +3820,7 @@ __metadata: d3: "npm:7.9.0" date-fns: "npm:4.1.0" debounce-promise: "npm:3.1.2" - esbuild: "npm:0.24.2" + esbuild: "npm:0.25.0" eslint: "npm:9.19.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" @@ -3755,7 +3899,7 @@ __metadata: "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/systemjs": "npm:6.15.1" - esbuild: "npm:0.24.2" + esbuild: "npm:0.25.0" history: "npm:4.10.1" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -3814,7 +3958,7 @@ __metadata: "@types/node": "npm:22.12.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" - esbuild: "npm:0.24.2" + esbuild: "npm:0.25.0" prettier: "npm:3.4.2" react: "npm:18.3.1" react-dom: "npm:18.3.1" @@ -3888,7 +4032,7 @@ __metadata: dependencies: "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:16.0.0" - esbuild: "npm:0.24.2" + esbuild: "npm:0.25.0" glob: "npm:^11.0.0" rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" @@ -4106,7 +4250,7 @@ __metadata: d3: "npm:7.9.0" date-fns: "npm:4.1.0" downshift: "npm:^9.0.6" - esbuild: "npm:0.24.2" + esbuild: "npm:0.25.0" expose-loader: "npm:5.0.0" fs-extra: "npm:^11.2.0" hoist-non-react-statics: "npm:3.3.2" @@ -14961,14 +15105,7 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:10": - version: 10.4.3 - resolution: "decimal.js@npm:10.4.3" - checksum: 10/de663a7bc4d368e3877db95fcd5c87b965569b58d16cdc4258c063d231ca7118748738df17cd638f7e9dd0be8e34cec08d7234b20f1f2a756a52fc5a38b188d0 - languageName: node - linkType: hard - -"decimal.js@npm:^10.4.2": +"decimal.js@npm:10, decimal.js@npm:^10.4.2": version: 10.5.0 resolution: "decimal.js@npm:10.5.0" checksum: 10/714d49cf2f2207b268221795ede330e51452b7c451a0c02a770837d2d4faed47d603a729c2aa1d952eb6c4102d999e91c9b952c1aa016db3c5cba9fc8bf4cda2 @@ -16013,7 +16150,93 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:0.24.2, esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0, esbuild@npm:^0.24.0": +"esbuild@npm:0.25.0": + version: 0.25.0 + resolution: "esbuild@npm:0.25.0" + dependencies: + "@esbuild/aix-ppc64": "npm:0.25.0" + "@esbuild/android-arm": "npm:0.25.0" + "@esbuild/android-arm64": "npm:0.25.0" + "@esbuild/android-x64": "npm:0.25.0" + "@esbuild/darwin-arm64": "npm:0.25.0" + "@esbuild/darwin-x64": "npm:0.25.0" + "@esbuild/freebsd-arm64": "npm:0.25.0" + "@esbuild/freebsd-x64": "npm:0.25.0" + "@esbuild/linux-arm": "npm:0.25.0" + "@esbuild/linux-arm64": "npm:0.25.0" + "@esbuild/linux-ia32": "npm:0.25.0" + "@esbuild/linux-loong64": "npm:0.25.0" + "@esbuild/linux-mips64el": "npm:0.25.0" + "@esbuild/linux-ppc64": "npm:0.25.0" + "@esbuild/linux-riscv64": "npm:0.25.0" + "@esbuild/linux-s390x": "npm:0.25.0" + "@esbuild/linux-x64": "npm:0.25.0" + "@esbuild/netbsd-arm64": "npm:0.25.0" + "@esbuild/netbsd-x64": "npm:0.25.0" + "@esbuild/openbsd-arm64": "npm:0.25.0" + "@esbuild/openbsd-x64": "npm:0.25.0" + "@esbuild/sunos-x64": "npm:0.25.0" + "@esbuild/win32-arm64": "npm:0.25.0" + "@esbuild/win32-ia32": "npm:0.25.0" + "@esbuild/win32-x64": "npm:0.25.0" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10/451daf6a442df29ec5d528587caa4ce783d41ff4acb93252da5a852b8d36c22e9f84d17f6721d4fbef9a1ba9855bc9fe1f167dd732c11665fe53032f2b89f114 + languageName: node + linkType: hard + +"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0, esbuild@npm:^0.24.0": version: 0.24.2 resolution: "esbuild@npm:0.24.2" dependencies: @@ -17381,18 +17604,7 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0": - version: 4.0.0 - resolution: "form-data@npm:4.0.0" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - mime-types: "npm:^2.1.12" - checksum: 10/7264aa760a8cf09482816d8300f1b6e2423de1b02bba612a136857413fdc96d7178298ced106817655facc6b89036c6e12ae31c9eb5bdc16aabf502ae8a5d805 - languageName: node - linkType: hard - -"form-data@npm:~4.0.0": +"form-data@npm:^4.0.0, form-data@npm:~4.0.0": version: 4.0.1 resolution: "form-data@npm:4.0.1" dependencies: @@ -18307,7 +18519,7 @@ __metadata: date-fns: "npm:4.1.0" debounce-promise: "npm:3.1.2" diff: "npm:^7.0.0" - esbuild: "npm:0.24.2" + esbuild: "npm:0.25.0" esbuild-loader: "npm:4.2.2" esbuild-plugin-browserslist: "npm:^0.15.0" eslint: "npm:9.19.0" @@ -25519,7 +25731,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.13.0, qs@npm:^6.11.2, qs@npm:^6.4.0": +"qs@npm:6.13.0": version: 6.13.0 resolution: "qs@npm:6.13.0" dependencies: @@ -25528,7 +25740,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.13.1": +"qs@npm:6.13.1, qs@npm:^6.11.2, qs@npm:^6.4.0": version: 6.13.1 resolution: "qs@npm:6.13.1" dependencies: From 14c8eb23735a017870ed4fd77ac786296cc2f013 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:27:29 +0200 Subject: [PATCH 492/894] Update dependency esbuild-loader to v4.3.0 (#100423) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 255 ++------------------------------------------------- 2 files changed, 8 insertions(+), 249 deletions(-) diff --git a/package.json b/package.json index 08a515c120c..444588222af 100644 --- a/package.json +++ b/package.json @@ -172,7 +172,7 @@ "cypress-file-upload": "5.0.8", "cypress-recurse": "^1.35.3", "esbuild": "0.25.0", - "esbuild-loader": "4.2.2", + "esbuild-loader": "4.3.0", "esbuild-plugin-browserslist": "^0.15.0", "eslint": "9.19.0", "eslint-config-prettier": "9.1.0", diff --git a/yarn.lock b/yarn.lock index e13c19a5289..ce4e0193997 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2067,13 +2067,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/aix-ppc64@npm:0.21.5" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/aix-ppc64@npm:0.24.2" @@ -2088,13 +2081,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/android-arm64@npm:0.21.5" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/android-arm64@npm:0.24.2" @@ -2109,13 +2095,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/android-arm@npm:0.21.5" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/android-arm@npm:0.24.2" @@ -2130,13 +2109,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/android-x64@npm:0.21.5" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/android-x64@npm:0.24.2" @@ -2151,13 +2123,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/darwin-arm64@npm:0.21.5" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/darwin-arm64@npm:0.24.2" @@ -2172,13 +2137,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/darwin-x64@npm:0.21.5" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/darwin-x64@npm:0.24.2" @@ -2193,13 +2151,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/freebsd-arm64@npm:0.21.5" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/freebsd-arm64@npm:0.24.2" @@ -2214,13 +2165,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/freebsd-x64@npm:0.21.5" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/freebsd-x64@npm:0.24.2" @@ -2235,13 +2179,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-arm64@npm:0.21.5" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-arm64@npm:0.24.2" @@ -2256,13 +2193,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-arm@npm:0.21.5" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-arm@npm:0.24.2" @@ -2277,13 +2207,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-ia32@npm:0.21.5" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-ia32@npm:0.24.2" @@ -2298,13 +2221,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-loong64@npm:0.21.5" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-loong64@npm:0.24.2" @@ -2319,13 +2235,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-mips64el@npm:0.21.5" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-mips64el@npm:0.24.2" @@ -2340,13 +2249,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-ppc64@npm:0.21.5" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-ppc64@npm:0.24.2" @@ -2361,13 +2263,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-riscv64@npm:0.21.5" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-riscv64@npm:0.24.2" @@ -2382,13 +2277,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-s390x@npm:0.21.5" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-s390x@npm:0.24.2" @@ -2403,13 +2291,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-x64@npm:0.21.5" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-x64@npm:0.24.2" @@ -2438,13 +2319,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/netbsd-x64@npm:0.21.5" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/netbsd-x64@npm:0.24.2" @@ -2473,13 +2347,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/openbsd-x64@npm:0.21.5" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/openbsd-x64@npm:0.24.2" @@ -2494,13 +2361,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/sunos-x64@npm:0.21.5" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/sunos-x64@npm:0.24.2" @@ -2515,13 +2375,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/win32-arm64@npm:0.21.5" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/win32-arm64@npm:0.24.2" @@ -2536,13 +2389,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/win32-ia32@npm:0.21.5" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/win32-ia32@npm:0.24.2" @@ -2557,13 +2403,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/win32-x64@npm:0.21.5" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/win32-x64@npm:0.24.2" @@ -16112,17 +15951,17 @@ __metadata: languageName: node linkType: hard -"esbuild-loader@npm:4.2.2": - version: 4.2.2 - resolution: "esbuild-loader@npm:4.2.2" +"esbuild-loader@npm:4.3.0": + version: 4.3.0 + resolution: "esbuild-loader@npm:4.3.0" dependencies: - esbuild: "npm:^0.21.0" + esbuild: "npm:^0.25.0" get-tsconfig: "npm:^4.7.0" loader-utils: "npm:^2.0.4" webpack-sources: "npm:^1.4.3" peerDependencies: webpack: ^4.40.0 || ^5.0.0 - checksum: 10/235d06c60e26827333c3c66df6e2f13fef31e1b84b8310ba36ebdb6aaa31abdf7628a940326e4923824cdb501c8ce6a6e9129c4e465daaeab112f0b5db3423f4 + checksum: 10/451bf9b344419870b6a10a9d80c1e07ccb4e2871867c8e4dbdf7b491171f0de31571933a8a96627034b360627a034e6f43edd004b1f3e3fe291100ded5985fa6 languageName: node linkType: hard @@ -16150,7 +15989,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:0.25.0": +"esbuild@npm:0.25.0, esbuild@npm:^0.25.0": version: 0.25.0 resolution: "esbuild@npm:0.25.0" dependencies: @@ -16322,86 +16161,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.21.0": - version: 0.21.5 - resolution: "esbuild@npm:0.21.5" - dependencies: - "@esbuild/aix-ppc64": "npm:0.21.5" - "@esbuild/android-arm": "npm:0.21.5" - "@esbuild/android-arm64": "npm:0.21.5" - "@esbuild/android-x64": "npm:0.21.5" - "@esbuild/darwin-arm64": "npm:0.21.5" - "@esbuild/darwin-x64": "npm:0.21.5" - "@esbuild/freebsd-arm64": "npm:0.21.5" - "@esbuild/freebsd-x64": "npm:0.21.5" - "@esbuild/linux-arm": "npm:0.21.5" - "@esbuild/linux-arm64": "npm:0.21.5" - "@esbuild/linux-ia32": "npm:0.21.5" - "@esbuild/linux-loong64": "npm:0.21.5" - "@esbuild/linux-mips64el": "npm:0.21.5" - "@esbuild/linux-ppc64": "npm:0.21.5" - "@esbuild/linux-riscv64": "npm:0.21.5" - "@esbuild/linux-s390x": "npm:0.21.5" - "@esbuild/linux-x64": "npm:0.21.5" - "@esbuild/netbsd-x64": "npm:0.21.5" - "@esbuild/openbsd-x64": "npm:0.21.5" - "@esbuild/sunos-x64": "npm:0.21.5" - "@esbuild/win32-arm64": "npm:0.21.5" - "@esbuild/win32-ia32": "npm:0.21.5" - "@esbuild/win32-x64": "npm:0.21.5" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10/d2ff2ca84d30cce8e871517374d6c2290835380dc7cd413b2d49189ed170d45e407be14de2cb4794cf76f75cf89955c4714726ebd3de7444b3046f5cab23ab6b - languageName: node - linkType: hard - "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -18520,7 +18279,7 @@ __metadata: debounce-promise: "npm:3.1.2" diff: "npm:^7.0.0" esbuild: "npm:0.25.0" - esbuild-loader: "npm:4.2.2" + esbuild-loader: "npm:4.3.0" esbuild-plugin-browserslist: "npm:^0.15.0" eslint: "npm:9.19.0" eslint-config-prettier: "npm:9.1.0" From 8a9f6416d2806a74d9b510c6ee59d5065c9076a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Tue, 11 Feb 2025 15:39:39 +0100 Subject: [PATCH 493/894] fix(unified-storage): error on failed primary deletes in mode2 (#100427) --- pkg/apiserver/rest/dualwriter_mode2.go | 33 ++++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/pkg/apiserver/rest/dualwriter_mode2.go b/pkg/apiserver/rest/dualwriter_mode2.go index fd6aeaf7e23..62b3526d504 100644 --- a/pkg/apiserver/rest/dualwriter_mode2.go +++ b/pkg/apiserver/rest/dualwriter_mode2.go @@ -207,28 +207,25 @@ func (d *DualWriterMode2) Delete(ctx context.Context, name string, deleteValidat log := d.Log.WithValues("name", name, "method", method) ctx = klog.NewContext(ctx, log) + // We should delete from Unified storage first so we can retry if legacy fails. + startStorage := time.Now() + deletedS, _, err := d.Storage.Delete(ctx, name, deleteValidation, options) + d.recordStorageDuration(err != nil, mode2Str, d.resource, method, startStorage) + if err != nil { + if !apierrors.IsNotFound(err) { + log.WithValues("objectList", deletedS).Error(err, "could not delete from unified storage") + return nil, false, err + } + } + startLegacy := time.Now() deletedLS, async, err := d.Legacy.Delete(ctx, name, deleteValidation, options) - + d.recordLegacyDuration(err != nil, mode2Str, d.resource, method, startLegacy) + // Deleting from legacy should always work in mode two, as legacy is still the primary database and + // needs to have all the data. if err != nil { - if !apierrors.IsNotFound(err) { - log.WithValues("objectList", deletedLS).Error(err, "could not delete from legacy store") - d.recordLegacyDuration(true, mode2Str, d.resource, method, startLegacy) - return deletedLS, async, err - } + return nil, false, err } - d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy) - - startStorage := time.Now() - deletedS, async, err := d.Storage.Delete(ctx, name, deleteValidation, options) - if err != nil { - if !apierrors.IsNotFound(err) { - log.WithValues("objectList", deletedS).Error(err, "could not delete from duplicate storage") - d.recordStorageDuration(true, mode2Str, d.resource, method, startStorage) - } - return deletedS, async, err - } - d.recordStorageDuration(false, mode2Str, d.resource, method, startStorage) go func() { areEqual := Compare(deletedS, deletedLS) From 79bd3ffd8c610206cd2be47836051ae2995708ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:44:41 +0000 Subject: [PATCH 494/894] Update dependency rollup-plugin-esbuild to v6.2.0 (#100421) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-icons/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-schema/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 94 ++++++++++++++------- 9 files changed, 73 insertions(+), 37 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index e5c550ba2b6..61124640e72 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -76,7 +76,7 @@ "rimraf": "6.0.1", "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.1", - "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0", "typescript": "5.7.3" }, diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 4d40c9d558c..739e36cfec8 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -46,7 +46,7 @@ "rimraf": "6.0.1", "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.1", - "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0" }, "dependencies": { diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 9f6ff3d3d25..3eb48f5eaef 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -78,7 +78,7 @@ "jest-canvas-mock": "2.5.2", "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.1", - "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0", "ts-jest": "29.2.5", "ts-node": "10.9.2", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index d75fea57ca5..dec83cf36f8 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -55,7 +55,7 @@ "rimraf": "6.0.1", "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.1", - "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "8.0.0", "ts-node": "10.9.2", "typescript": "5.7.3" diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index dd26bced034..72c92a1c944 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -128,7 +128,7 @@ "react-select-event": "5.5.1", "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.1", - "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0", "sass": "1.83.4", "sass-loader": "16.0.4", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 4bb413f2495..33b457d4340 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -69,7 +69,7 @@ "rimraf": "6.0.1", "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.1", - "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0", "rollup-plugin-sourcemaps": "0.6.3", "typescript": "5.7.3" diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 9975c9548fc..a4939ade8a1 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -43,7 +43,7 @@ "rimraf": "6.0.1", "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.1", - "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0", "typescript": "5.7.3" }, diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index bcbb20a3757..cee844a1b5d 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -175,7 +175,7 @@ "rollup": "^4.22.4", "rollup-plugin-copy": "3.5.0", "rollup-plugin-dts": "^6.1.1", - "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0", "rollup-plugin-svg-import": "3.0.0", "sass-loader": "16.0.4", diff --git a/yarn.lock b/yarn.lock index ce4e0193997..cb8a2e153d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3245,7 +3245,7 @@ __metadata: rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" rollup-plugin-dts: "npm:^6.1.1" - rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" rxjs: "npm:7.8.1" string-hash: "npm:^1.1.3" @@ -3283,7 +3283,7 @@ __metadata: rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" rollup-plugin-dts: "npm:^6.1.1" - rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" semver: "npm:7.7.0" tslib: "npm:2.8.1" @@ -3401,7 +3401,7 @@ __metadata: react-virtualized-auto-sizer: "npm:1.0.25" rollup: "npm:^4.22.4" rollup-plugin-dts: "npm:^6.1.1" - rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" tinycolor2: "npm:1.6.0" ts-jest: "npm:29.2.5" @@ -3695,7 +3695,7 @@ __metadata: react-window: "npm:1.8.11" rollup: "npm:^4.22.4" rollup-plugin-dts: "npm:^6.1.1" - rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" rxjs: "npm:7.8.1" sass: "npm:1.83.4" @@ -3747,7 +3747,7 @@ __metadata: rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" rollup-plugin-dts: "npm:^6.1.1" - rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" rollup-plugin-sourcemaps: "npm:0.6.3" rxjs: "npm:7.8.1" @@ -3804,7 +3804,7 @@ __metadata: rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" rollup-plugin-dts: "npm:^6.1.1" - rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:8.0.0" ts-node: "npm:10.9.2" typescript: "npm:5.7.3" @@ -3876,7 +3876,7 @@ __metadata: rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" rollup-plugin-dts: "npm:^6.1.1" - rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" tslib: "npm:2.8.1" typescript: "npm:5.7.3" @@ -4134,7 +4134,7 @@ __metadata: rollup: "npm:^4.22.4" rollup-plugin-copy: "npm:3.5.0" rollup-plugin-dts: "npm:^6.1.1" - rollup-plugin-esbuild: "npm:6.1.1" + rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" rollup-plugin-svg-import: "npm:3.0.0" rxjs: "npm:7.8.1" @@ -7150,7 +7150,7 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.0.5, @rollup/pluginutils@npm:^5.1.0": +"@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.1.0": version: 5.1.2 resolution: "@rollup/pluginutils@npm:5.1.2" dependencies: @@ -14906,15 +14906,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": - version: 4.3.7 - resolution: "debug@npm:4.3.7" +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0": + version: 4.4.0 + resolution: "debug@npm:4.4.0" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a + checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 languageName: node linkType: hard @@ -14927,6 +14927,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a + languageName: node + linkType: hard + "decamelize-keys@npm:^1.1.0": version: 1.1.1 resolution: "decamelize-keys@npm:1.1.1" @@ -15877,10 +15889,10 @@ __metadata: languageName: node linkType: hard -"es-module-lexer@npm:^1.2.1, es-module-lexer@npm:^1.3.1, es-module-lexer@npm:^1.5.0, es-module-lexer@npm:^1.5.3": - version: 1.5.4 - resolution: "es-module-lexer@npm:1.5.4" - checksum: 10/f29c7c97a58eb17640dcbd71bd6ef754ad4f58f95c3073894573d29dae2cad43ecd2060d97ed5b866dfb7804d5590fb7de1d2c5339a5fceae8bd60b580387fc5 +"es-module-lexer@npm:^1.2.1, es-module-lexer@npm:^1.5.0, es-module-lexer@npm:^1.5.3, es-module-lexer@npm:^1.6.0": + version: 1.6.0 + resolution: "es-module-lexer@npm:1.6.0" + checksum: 10/807ee7020cc46a9c970c78cad1f2f3fc139877e5ebad7f66dbfbb124d451189ba1c48c1c632bd5f8ce1b8af2caef3fca340ba044a410fa890d17b080a59024bb languageName: node linkType: hard @@ -17744,12 +17756,12 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.7.0, get-tsconfig@npm:^4.7.2": - version: 4.8.1 - resolution: "get-tsconfig@npm:4.8.1" +"get-tsconfig@npm:^4.10.0, get-tsconfig@npm:^4.7.0": + version: 4.10.0 + resolution: "get-tsconfig@npm:4.10.0" dependencies: resolve-pkg-maps: "npm:^1.0.0" - checksum: 10/3fb5a8ad57b9633eaea085d81661e9e5c9f78b35d8f8689eaf8b8b45a2a3ebf3b3422266d4d7df765e308cc1e6231648d114803ab3d018332e29916f2c1de036 + checksum: 10/5259b5c99a1957114337d9d0603b4a305ec9e29fa6cac7d2fbf634ba6754a0cc88bfd281a02416ce64e604b637d3cb239185381a79a5842b17fb55c097b38c4b languageName: node linkType: hard @@ -24514,6 +24526,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^2.0.2": + version: 2.0.2 + resolution: "pathe@npm:2.0.2" + checksum: 10/027dd246720ec6d3b5567e2b0201f1a815b6a69f2912a4dcafed59620afc729af15b4aff4bc780504c88d11dfb081c051e37327b928a093e714c3e09bf35aff3 + languageName: node + linkType: hard + "pbf@npm:3.2.1": version: 3.2.1 resolution: "pbf@npm:3.2.1" @@ -24578,6 +24597,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.2": + version: 4.0.2 + resolution: "picomatch@npm:4.0.2" + checksum: 10/ce617b8da36797d09c0baacb96ca8a44460452c89362d7cb8f70ca46b4158ba8bc3606912de7c818eb4a939f7f9015cef3c766ec8a0c6bfc725fdc078e39c717 + languageName: node + linkType: hard + "pify@npm:5.0.0": version: 5.0.0 resolution: "pify@npm:5.0.0" @@ -27838,18 +27864,18 @@ __metadata: languageName: node linkType: hard -"rollup-plugin-esbuild@npm:6.1.1": - version: 6.1.1 - resolution: "rollup-plugin-esbuild@npm:6.1.1" +"rollup-plugin-esbuild@npm:6.2.0": + version: 6.2.0 + resolution: "rollup-plugin-esbuild@npm:6.2.0" dependencies: - "@rollup/pluginutils": "npm:^5.0.5" - debug: "npm:^4.3.4" - es-module-lexer: "npm:^1.3.1" - get-tsconfig: "npm:^4.7.2" + debug: "npm:^4.4.0" + es-module-lexer: "npm:^1.6.0" + get-tsconfig: "npm:^4.10.0" + unplugin-utils: "npm:^0.2.3" peerDependencies: esbuild: ">=0.18.0" rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 - checksum: 10/bba2d1dfb92a193823ac9dd1cdd44a8fd8cd9f25868e9a22ca077e1b7445feb4eaaf6df051148e367fc902d7d59c9f50efab49086c24c367972f05c86f3a656d + checksum: 10/305563dfa72a843fd7db7b885583f22f62d4d584cbce7e8d80f138692ce67eb34e45dc1227b3420662f22fa75fa603ca09b64e7d1670919275a8577da653fada languageName: node linkType: hard @@ -31100,6 +31126,16 @@ __metadata: languageName: node linkType: hard +"unplugin-utils@npm:^0.2.3": + version: 0.2.3 + resolution: "unplugin-utils@npm:0.2.3" + dependencies: + pathe: "npm:^2.0.2" + picomatch: "npm:^4.0.2" + checksum: 10/95a18001b4a3aa92e58259162ee70be74f49780bff7aca0272035b206143772d07f3b117922ec9d7caaed8bd24578afcf7baa4330943796ea9f011dcc96af1d9 + languageName: node + linkType: hard + "unplugin@npm:^1.3.1": version: 1.5.0 resolution: "unplugin@npm:1.5.0" From 4cac3158c76bb3f36ca01dadfa1ac3e7df7615d3 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Tue, 11 Feb 2025 09:46:02 -0500 Subject: [PATCH 495/894] Alerting: Fix alert rule copy to include metadata (#100212) * copy metadata * add tests for copy and generator * extract copy rule to a production method and update usages * fix tests --- pkg/services/ngalert/models/alert_rule.go | 75 ++++++++++++++++ .../ngalert/models/alert_rule_test.go | 66 +++++++++++++- pkg/services/ngalert/models/testing.go | 88 ++++--------------- .../ngalert/schedule/schedule_unit_test.go | 2 + pkg/services/ngalert/store/alert_rule.go | 4 +- pkg/services/ngalert/store/deltas.go | 2 +- pkg/services/ngalert/store/deltas_test.go | 4 +- 7 files changed, 166 insertions(+), 75 deletions(-) diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 26f7d84eee1..e11c5eb1add 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -680,6 +680,81 @@ func (alertRule *AlertRule) Type() RuleType { return RuleTypeAlerting } +// Copy creates and returns a deep copy of the AlertRule instance, duplicating all fields and nested data structures. +func (alertRule *AlertRule) Copy() *AlertRule { + if alertRule == nil { + return nil + } + result := AlertRule{ + ID: alertRule.ID, + OrgID: alertRule.OrgID, + Title: alertRule.Title, + Condition: alertRule.Condition, + Updated: alertRule.Updated, + UpdatedBy: alertRule.UpdatedBy, + IntervalSeconds: alertRule.IntervalSeconds, + Version: alertRule.Version, + UID: alertRule.UID, + NamespaceUID: alertRule.NamespaceUID, + RuleGroup: alertRule.RuleGroup, + RuleGroupIndex: alertRule.RuleGroupIndex, + NoDataState: alertRule.NoDataState, + ExecErrState: alertRule.ExecErrState, + For: alertRule.For, + Record: alertRule.Record, + IsPaused: alertRule.IsPaused, + Metadata: alertRule.Metadata, + } + + if alertRule.DashboardUID != nil { + dash := *alertRule.DashboardUID + result.DashboardUID = &dash + } + if alertRule.PanelID != nil { + p := *alertRule.PanelID + result.PanelID = &p + } + + for _, d := range alertRule.Data { + q := AlertQuery{ + RefID: d.RefID, + QueryType: d.QueryType, + RelativeTimeRange: d.RelativeTimeRange, + DatasourceUID: d.DatasourceUID, + } + q.Model = make([]byte, 0, cap(d.Model)) + q.Model = append(q.Model, d.Model...) + result.Data = append(result.Data, q) + } + + if alertRule.Annotations != nil { + result.Annotations = make(map[string]string, len(alertRule.Annotations)) + for s, s2 := range alertRule.Annotations { + result.Annotations[s] = s2 + } + } + + if alertRule.Labels != nil { + result.Labels = make(map[string]string, len(alertRule.Labels)) + for s, s2 := range alertRule.Labels { + result.Labels[s] = s2 + } + } + + if alertRule.Record != nil { + result.Record = &Record{ + From: alertRule.Record.From, + Metric: alertRule.Record.Metric, + } + } + + for _, s := range alertRule.NotificationSettings { + result.NotificationSettings = append(result.NotificationSettings, CopyNotificationSettings(s)) + } + + return &result +} + func ClearRecordingRuleIgnoredFields(rule *AlertRule) { rule.NoDataState = "" rule.ExecErrState = "" diff --git a/pkg/services/ngalert/models/alert_rule_test.go b/pkg/services/ngalert/models/alert_rule_test.go index 7bb1b44a39b..6594732c04b 100644 --- a/pkg/services/ngalert/models/alert_rule_test.go +++ b/pkg/services/ngalert/models/alert_rule_test.go @@ -15,6 +15,7 @@ import ( "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/util" @@ -407,7 +408,7 @@ func TestDiff(t *testing.T) { rule1 := RuleGen.GenerateRef() rule2 := RuleGen.GenerateRef() - diffs := rule1.Diff(rule2, "Data", "Annotations", "Labels", "NotificationSettings") // these fields will be tested separately + diffs := rule1.Diff(rule2, "Data", "Annotations", "Labels", "NotificationSettings", "Metadata") // these fields will be tested separately difCnt := 0 if rule1.ID != rule2.ID { @@ -839,6 +840,24 @@ func TestDiff(t *testing.T) { }) } }) + + t.Run("should detect changes in Metadata", func(t *testing.T) { + rule1 := RuleGen.With(RuleGen.WithMetadata(AlertRuleMetadata{EditorSettings: EditorSettings{ + SimplifiedQueryAndExpressionsSection: false, + SimplifiedNotificationsSection: false, + }})).GenerateRef() + + rule2 := CopyRule(rule1, RuleGen.WithMetadata(AlertRuleMetadata{EditorSettings: EditorSettings{ + SimplifiedQueryAndExpressionsSection: true, + SimplifiedNotificationsSection: true, + }})) + + diff := rule1.Diff(rule2) + assert.ElementsMatch(t, []string{ + "Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection", + "Metadata.EditorSettings.SimplifiedNotificationsSection", + }, diff.Paths()) + }) } func TestSortByGroupIndex(t *testing.T) { @@ -919,3 +938,48 @@ func TestAlertRuleGetKeyWithGroup(t *testing.T) { require.Equal(t, expected, rule.GetKeyWithGroup()) }) } + +func TestAlertRuleCopy(t *testing.T) { + for i := 0; i < 100; i++ { + rule := RuleGen.GenerateRef() + copied := rule.Copy() + require.Empty(t, rule.Diff(copied)) + } +} + +// This test makes sure the default generator +func TestGeneratorFillsAllFields(t *testing.T) { + ignoredFields := map[string]struct{}{ + "ID": {}, + "IsPaused": {}, + "Record": {}, + } + + tpe := reflect.TypeOf(AlertRule{}) + fields := make(map[string]struct{}, tpe.NumField()) + for i := 0; i < tpe.NumField(); i++ { + if _, ok := ignoredFields[tpe.Field(i).Name]; ok { + continue + } + fields[tpe.Field(i).Name] = struct{}{} + } + + for i := 0; i < 1000; i++ { + rule := RuleGen.Generate() + v := reflect.ValueOf(rule) + + for j := 0; j < tpe.NumField(); j++ { + field := tpe.Field(j) + value := v.Field(j) + if !value.IsValid() || value.Kind() == reflect.Ptr && value.IsNil() || value.IsZero() { + continue + } + delete(fields, field.Name) + if len(fields) == 0 { + return + } + } + } + + require.FailNow(t, "AlertRule generator does not populate fields", "skipped fields: %v", maps.Keys(fields)) +} diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 8feca77ec27..04869e53750 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -123,6 +123,7 @@ func (g *AlertRuleGenerator) Generate() AlertRule { Annotations: annotations, Labels: labels, NotificationSettings: ns, + Metadata: GenerateMetadata(), } for _, mutator := range g.mutators { @@ -569,6 +570,12 @@ func (a *AlertRuleMutators) WithVersion(version int64) AlertRuleMutator { } } +func (a *AlertRuleMutators) WithMetadata(meta AlertRuleMetadata) AlertRuleMutator { + return func(r *AlertRule) { + r.Metadata = meta + } +} + func (g *AlertRuleGenerator) GenerateLabels(min, max int, prefix string) data.Labels { count := max if min > max { @@ -643,79 +650,13 @@ func GenerateGroupKey(orgID int64) AlertRuleGroupKey { // CopyRule creates a deep copy of AlertRule func CopyRule(r *AlertRule, mutators ...AlertRuleMutator) *AlertRule { - result := AlertRule{ - ID: r.ID, - OrgID: r.OrgID, - Title: r.Title, - Condition: r.Condition, - Updated: r.Updated, - UpdatedBy: r.UpdatedBy, - IntervalSeconds: r.IntervalSeconds, - Version: r.Version, - UID: r.UID, - NamespaceUID: r.NamespaceUID, - RuleGroup: r.RuleGroup, - RuleGroupIndex: r.RuleGroupIndex, - NoDataState: r.NoDataState, - ExecErrState: r.ExecErrState, - For: r.For, - Record: r.Record, - IsPaused: r.IsPaused, - } - - if r.DashboardUID != nil { - dash := *r.DashboardUID - result.DashboardUID = &dash - } - if r.PanelID != nil { - p := *r.PanelID - result.PanelID = &p - } - - for _, d := range r.Data { - q := AlertQuery{ - RefID: d.RefID, - QueryType: d.QueryType, - RelativeTimeRange: d.RelativeTimeRange, - DatasourceUID: d.DatasourceUID, - } - q.Model = make([]byte, 0, cap(d.Model)) - q.Model = append(q.Model, d.Model...) - result.Data = append(result.Data, q) - } - - if r.Annotations != nil { - result.Annotations = make(map[string]string, len(r.Annotations)) - for s, s2 := range r.Annotations { - result.Annotations[s] = s2 - } - } - - if r.Labels != nil { - result.Labels = make(map[string]string, len(r.Labels)) - for s, s2 := range r.Labels { - result.Labels[s] = s2 - } - } - - if r.Record != nil { - result.Record = &Record{ - From: r.Record.From, - Metric: r.Record.Metric, - } - } - - for _, s := range r.NotificationSettings { - result.NotificationSettings = append(result.NotificationSettings, CopyNotificationSettings(s)) - } - + result := r.Copy() if len(mutators) > 0 { for _, mutator := range mutators { - mutator(&result) + mutator(result) } } - - return &result + return result } func CreateClassicConditionExpression(refID string, inputRefID string, reducer string, operation string, threshold int) AlertQuery { @@ -862,6 +803,15 @@ func CreateHysteresisExpression(t *testing.T, refID string, inputRefID string, t return q } +func GenerateMetadata() AlertRuleMetadata { + return AlertRuleMetadata{ + EditorSettings: EditorSettings{ + SimplifiedQueryAndExpressionsSection: rand.Int()%2 == 0, + SimplifiedNotificationsSection: rand.Int()%2 == 0, + }, + } +} + type AlertInstanceMutator func(*AlertInstance) // AlertInstanceGen provides a factory function that generates a random AlertInstance. diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 575f19cda27..390c966786f 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -784,6 +784,7 @@ func TestSchedule_updateRulesMetrics(t *testing.T) { alertRuleWithAdvancedSettings := models.RuleGen.With( models.RuleGen.WithOrgID(firstOrgID), + models.RuleGen.WithEditorSettingsSimplifiedNotificationsSection(false), models.RuleGen.WithEditorSettingsSimplifiedQueryAndExpressionsSection(false), ).GenerateRef() @@ -818,6 +819,7 @@ func TestSchedule_updateRulesMetrics(t *testing.T) { alertRule2 := models.RuleGen.With( models.RuleGen.WithOrgID(secondOrgID), + models.RuleGen.WithEditorSettingsSimplifiedNotificationsSection(false), models.RuleGen.WithEditorSettingsSimplifiedQueryAndExpressionsSection(true), ).GenerateRef() diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 17f3f294214..4a50990c5c1 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -984,7 +984,7 @@ func (st DBstore) RenameReceiverInNotificationSettings(ctx context.Context, orgI continue } - r := ngmodels.CopyRule(rule) + r := rule.Copy() for idx := range r.NotificationSettings { if r.NotificationSettings[idx].Receiver == oldReceiver { r.NotificationSettings[idx].Receiver = newReceiver @@ -1059,7 +1059,7 @@ func (st DBstore) RenameTimeIntervalInNotificationSettings( continue } - r := ngmodels.CopyRule(rule) + r := rule.Copy() for idx := range r.NotificationSettings { for mtIdx := range r.NotificationSettings[idx].MuteTimeIntervals { if r.NotificationSettings[idx].MuteTimeIntervals[mtIdx] == oldTimeInterval { diff --git a/pkg/services/ngalert/store/deltas.go b/pkg/services/ngalert/store/deltas.go index e08bc3c5cfc..a7b00d27adf 100644 --- a/pkg/services/ngalert/store/deltas.go +++ b/pkg/services/ngalert/store/deltas.go @@ -175,7 +175,7 @@ func UpdateCalculatedRuleFields(ch *GroupDelta) *GroupDelta { } if groupKey != ch.GroupKey { if rule.RuleGroupIndex != idx { - upd.New = models.CopyRule(rule) + upd.New = rule.Copy() upd.New.RuleGroupIndex = idx upd.Diff = rule.Diff(upd.New, AlertRuleFieldsToIgnoreInDiff[:]...) } diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index 904c0da5164..fde5a8e252f 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -81,7 +81,7 @@ func TestCalculateChanges(t *testing.T) { submittedMap := groupByUID(t, rules) submitted := make([]*models.AlertRuleWithOptionals, 0, len(rules)) for _, rule := range rules { - submitted = append(submitted, &models.AlertRuleWithOptionals{AlertRule: *rule}) + submitted = append(submitted, &models.AlertRuleWithOptionals{AlertRule: *rule, HasMetadata: true}) } fakeStore := fakes.NewRuleStore(t) @@ -216,7 +216,7 @@ func TestCalculateChanges(t *testing.T) { submittedMap := groupByUID(t, rules) submitted := make([]*models.AlertRuleWithOptionals, 0, len(rules)) for _, rule := range rules { - submitted = append(submitted, &models.AlertRuleWithOptionals{AlertRule: *rule}) + submitted = append(submitted, &models.AlertRuleWithOptionals{AlertRule: *rule, HasMetadata: true}) } changes, err := CalculateChanges(context.Background(), fakeStore, groupKey, submitted) From 6da6660def28367b9e8b7632c610f1ce7bd534a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:46:16 +0200 Subject: [PATCH 496/894] Update dependency esbuild-plugin-browserslist to ^0.16.0 (#100420) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 444588222af..189fc00835e 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,7 @@ "cypress-recurse": "^1.35.3", "esbuild": "0.25.0", "esbuild-loader": "4.3.0", - "esbuild-plugin-browserslist": "^0.15.0", + "esbuild-plugin-browserslist": "^0.16.0", "eslint": "9.19.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", diff --git a/yarn.lock b/yarn.lock index cb8a2e153d0..d5061c52875 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15977,16 +15977,16 @@ __metadata: languageName: node linkType: hard -"esbuild-plugin-browserslist@npm:^0.15.0": - version: 0.15.0 - resolution: "esbuild-plugin-browserslist@npm:0.15.0" +"esbuild-plugin-browserslist@npm:^0.16.0": + version: 0.16.0 + resolution: "esbuild-plugin-browserslist@npm:0.16.0" dependencies: - debug: "npm:^4.3.7" - zod: "npm:^3.23.8" + debug: "npm:^4.4.0" + zod: "npm:^3.24.1" peerDependencies: browserslist: ^4.21.8 - esbuild: ~0.24.0 - checksum: 10/b1afe26f5c013a37664ff6ca2f8190d50e36f719afca8aba2c0b18f0fd0f34de3334db1435154a70dbfeb5e91acf732cc20296bb3fd3cbdfe68ae37dadcffe32 + esbuild: ~0.25.0 + checksum: 10/9f3986901521627270a74500a77a3ddf3599016729de6ebe04e675230188a0e5dd3f5c6a5a1ab781a0b9cb54952df0edb066a3a5737166ef8e8d9db4a2eb44ab languageName: node linkType: hard @@ -18292,7 +18292,7 @@ __metadata: diff: "npm:^7.0.0" esbuild: "npm:0.25.0" esbuild-loader: "npm:4.3.0" - esbuild-plugin-browserslist: "npm:^0.15.0" + esbuild-plugin-browserslist: "npm:^0.16.0" eslint: "npm:9.19.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" @@ -32505,10 +32505,10 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.23.8": - version: 3.23.8 - resolution: "zod@npm:3.23.8" - checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1 +"zod@npm:^3.23.8, zod@npm:^3.24.1": + version: 3.24.1 + resolution: "zod@npm:3.24.1" + checksum: 10/54e25956495dec22acb9399c168c6ba657ff279801a7fcd0530c414d867f1dcca279335e160af9b138dd70c332e17d548be4bc4d2f7eaf627dead50d914fec27 languageName: node linkType: hard From 28f21e0a0deb1a7e9abd49c062e1df7e7c671bed Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Tue, 11 Feb 2025 09:46:26 -0500 Subject: [PATCH 497/894] Alerting: Do not record rule version if no difference (#100364) --- pkg/services/ngalert/store/alert_rule.go | 8 ++++- pkg/services/ngalert/store/alert_rule_test.go | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 4a50990c5c1..77d432ddc78 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -329,7 +329,13 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, user *ngmodels.UserUID, v := alertRuleToAlertRuleVersion(converted) v.Version++ v.ParentVersion = r.Existing.Version - ruleVersions = append(ruleVersions, v) + + // check if there is diff between existing and new, and if no, skip saving version. + existingConverted, err := alertRuleFromModelsAlertRule(*r.Existing) + if err != nil || !alertRuleToAlertRuleVersion(existingConverted).EqualSpec(v) { + ruleVersions = append(ruleVersions, v) + } + keys = append(keys, ngmodels.AlertRuleKey{OrgID: r.New.OrgID, UID: r.New.UID}) } if len(ruleVersions) > 0 { diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 0eba646ab46..e5d5e77f483 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -73,6 +73,16 @@ func TestIntegrationUpdateAlertRules(t *testing.T) { require.NoError(t, err) require.Equal(t, rule.Version+1, dbrule.Version) + + t.Run("should create version record", func(t *testing.T) { + var count int64 + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + count, err = sess.Table(alertRuleVersion{}).Where("rule_uid = ?", rule.UID).Count() + return err + }) + require.NoError(t, err) + require.EqualValues(t, 1, count) // only the current version, insert did not create version. + }) }) t.Run("updating record field should increase version", func(t *testing.T) { @@ -191,6 +201,32 @@ func TestIntegrationUpdateAlertRules(t *testing.T) { assert.Nil(t, dbrule.UpdatedBy) }) }) + + t.Run("should save noop update", func(t *testing.T) { + rule := createRule(t, store, gen) + newRule := models.CopyRule(rule) + err := store.UpdateAlertRules(context.Background(), &usr, []models.UpdateRule{{ + Existing: rule, + New: *newRule, + }, + }) + require.NoError(t, err) + + newRule, err = store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{UID: rule.UID}) + require.NoError(t, err) + + assert.Equal(t, rule.Version+1, newRule.Version) + + t.Run("should not create version record", func(t *testing.T) { + var count int64 + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + count, err = sess.Table(alertRuleVersion{}).Where("rule_uid = ?", rule.UID).Count() + return err + }) + require.NoError(t, err) + require.EqualValues(t, 1, count) // only the current version + }) + }) } func TestIntegrationUpdateAlertRulesWithUniqueConstraintViolation(t *testing.T) { From f7588376df32ca268bbb60ebf269c39fc1889033 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Tue, 11 Feb 2025 10:15:54 -0500 Subject: [PATCH 498/894] SQL Expressions: (Chore) Update code for latest plugin-sdk data pkg (#100425) Use new NilAt and SetRefId methods --- pkg/expr/sql/db_test.go | 9 ++--- pkg/expr/sql/frame_db_conv.go | 62 ----------------------------------- pkg/expr/sql/frame_table.go | 2 +- 3 files changed, 3 insertions(+), 70 deletions(-) diff --git a/pkg/expr/sql/db_test.go b/pkg/expr/sql/db_test.go index e648407e41a..5d7d0332da7 100644 --- a/pkg/expr/sql/db_test.go +++ b/pkg/expr/sql/db_test.go @@ -45,11 +45,11 @@ func TestQueryFrames(t *testing.T) { name: "query all rows from single input frame", query: `SELECT * FROM inputFrameRefId LIMIT 1;`, input_frames: []*data.Frame{ - setRefID(data.NewFrame( + data.NewFrame( "", //nolint:misspell data.NewField("OSS Projects with Typos", nil, []string{"Garfana", "Pormetheus"}), - ), "inputFrameRefId"), + ).SetRefID("inputFrameRefId"), }, expected: data.NewFrame( "sqlExpressionRefId", @@ -174,8 +174,3 @@ func TestQueryFramesDateTimeSelect(t *testing.T) { func p[T any](v T) *T { return &v } - -func setRefID(f *data.Frame, refID string) *data.Frame { - f.RefID = refID - return f -} diff --git a/pkg/expr/sql/frame_db_conv.go b/pkg/expr/sql/frame_db_conv.go index ce0edfc2341..81e59fd9f32 100644 --- a/pkg/expr/sql/frame_db_conv.go +++ b/pkg/expr/sql/frame_db_conv.go @@ -410,65 +410,3 @@ func fieldValFromRowVal(fieldType data.FieldType, val interface{}) (interface{}, return nil, fmt.Errorf("unsupported field type %s for val %v", fieldType, val) } } - -// Is the field nilAt the index. Can panic if out of range. -// TODO: Maybe this should be a method on data.Field? -func nilAt(field data.Field, at int) bool { - if !field.Nullable() { - return false - } - - switch field.Type() { - case data.FieldTypeNullableInt8: - v := field.At(at).(*int8) - return v == nil - - case data.FieldTypeNullableUint8: - v := field.At(at).(*uint8) - return v == nil - - case data.FieldTypeNullableInt16: - v := field.At(at).(*int16) - return v == nil - - case data.FieldTypeNullableUint16: - v := field.At(at).(*uint16) - return v == nil - - case data.FieldTypeNullableInt32: - v := field.At(at).(*int32) - return v == nil - - case data.FieldTypeNullableUint32: - v := field.At(at).(*uint32) - return v == nil - - case data.FieldTypeNullableInt64: - v := field.At(at).(*int64) - return v == nil - - case data.FieldTypeNullableUint64: - v := field.At(at).(*uint64) - return v == nil - - case data.FieldTypeNullableFloat64: - v := field.At(at).(*float64) - return v == nil - - case data.FieldTypeNullableString: - v := field.At(at).(*string) - return v == nil - - case data.FieldTypeNullableTime: - v := field.At(at).(*time.Time) - return v == nil - - case data.FieldTypeNullableBool: - v := field.At(at).(*bool) - return v == nil - - default: - // Either it's not a nullable type or it's unsupported - return false - } -} diff --git a/pkg/expr/sql/frame_table.go b/pkg/expr/sql/frame_table.go index a511a3606fa..7ddf7f1dd39 100644 --- a/pkg/expr/sql/frame_table.go +++ b/pkg/expr/sql/frame_table.go @@ -85,7 +85,7 @@ func (ri *rowIter) Next(_ *mysql.Context) (mysql.Row, error) { // the value from each column at the current row index. row := make(mysql.Row, len(ri.ft.Frame.Fields)) for colIndex, field := range ri.ft.Frame.Fields { - if nilAt(*field, ri.row) { + if field.NilAt(ri.row) { continue } row[colIndex], _ = field.ConcreteAt(ri.row) From d1b4162a334ae8342f8878e2e36b34cbde35f6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Tue, 11 Feb 2025 16:19:43 +0100 Subject: [PATCH 499/894] refactor(unified-storage): measure also failed queries (#100430) --- pkg/apiserver/rest/dualwriter_mode3.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pkg/apiserver/rest/dualwriter_mode3.go b/pkg/apiserver/rest/dualwriter_mode3.go index 8b68c748769..ecf0e21b80d 100644 --- a/pkg/apiserver/rest/dualwriter_mode3.go +++ b/pkg/apiserver/rest/dualwriter_mode3.go @@ -176,28 +176,22 @@ func (d *DualWriterMode3) Delete(ctx context.Context, name string, deleteValidat // we want to delete from legacy first, otherwise if the delete from unistore was successful, // but legacy failed, the user would get a failure, but not be able to retry the delete // as they would not be able to see the object in unistore anymore. - startLegacy := time.Now() objFromLegacy, asyncLegacy, err := d.Legacy.Delete(ctx, name, deleteValidation, options) + d.recordLegacyDuration(err != nil && !apierrors.IsNotFound(err), mode3Str, d.resource, method, startLegacy) if err != nil { if !apierrors.IsNotFound(err) { log.WithValues("object", objFromLegacy).Error(err, "could not delete from legacy store") - d.recordLegacyDuration(true, mode3Str, d.resource, method, startLegacy) return objFromLegacy, asyncLegacy, err } } - d.recordLegacyDuration(false, mode3Str, d.resource, method, startLegacy) startStorage := time.Now() objFromStorage, asyncStorage, err := d.Storage.Delete(ctx, name, deleteValidation, options) + d.recordStorageDuration(err != nil && !apierrors.IsNotFound(err), mode3Str, d.resource, method, startStorage) if err != nil { - if !apierrors.IsNotFound(err) { - log.WithValues("object", objFromStorage).Error(err, "could not delete from storage") - d.recordStorageDuration(true, mode3Str, d.resource, method, startStorage) - } - return objFromStorage, asyncStorage, err + return nil, false, err } - d.recordStorageDuration(false, mode3Str, d.resource, method, startStorage) areEqual := Compare(objFromStorage, objFromLegacy) d.recordOutcome(mode3Str, name, areEqual, method) From a8b98ded665a81c70e14ce8706a3be3a691f67bd Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Tue, 11 Feb 2025 09:41:46 -0600 Subject: [PATCH 500/894] CI: Add release branches to patch automation (#100442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CI: Add release branches to patch automation * Update .github/workflows/create-security-patch-from-security-mirror.yml Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com> --------- Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com> --- .github/workflows/create-security-patch-from-security-mirror.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/create-security-patch-from-security-mirror.yml b/.github/workflows/create-security-patch-from-security-mirror.yml index e187149b8b4..f85239241ad 100644 --- a/.github/workflows/create-security-patch-from-security-mirror.yml +++ b/.github/workflows/create-security-patch-from-security-mirror.yml @@ -11,6 +11,7 @@ on: branches: - "main" - "v*.*.*" + - "release-*.*.*" # This is run before the pull request has been merged, so we'll run against the src branch jobs: From c15c9f8af614c5942e6eb02aa08c0e50a85a27cc Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Tue, 11 Feb 2025 16:55:54 +0100 Subject: [PATCH 501/894] Prometheus: Fix cursor jump in prometheus code editor (#100273) * set save view state prop * remove custom onChange function --- .../monaco-query-field/MonacoQueryField.tsx | 28 ++++--------------- .../MonacoQueryFieldLazy.tsx | 2 -- .../MonacoQueryFieldProps.ts | 2 -- .../MonacoQueryFieldWrapper.tsx | 10 +------ 4 files changed, 6 insertions(+), 36 deletions(-) diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx index a27fa4943fc..97e49df416b 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx +++ b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx @@ -1,7 +1,6 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryField.tsx import { css } from '@emotion/css'; import { parser } from '@prometheus-io/lezer-promql'; -import { debounce } from 'lodash'; import { promLanguageDefinition } from 'monaco-promql'; import { useEffect, useRef } from 'react'; import { useLatest } from 'react-use'; @@ -106,13 +105,12 @@ const MonacoQueryField = (props: Props) => { // we need only one instance of `overrideServices` during the lifetime of the react component const overrideServicesRef = useRef(getOverrideServices()); const containerRef = useRef(null); - const { languageProvider, history, onBlur, onRunQuery, initialValue, placeholder, onChange, datasource } = props; + const { languageProvider, history, onBlur, onRunQuery, initialValue, placeholder, datasource } = props; const lpRef = useLatest(languageProvider); const historyRef = useLatest(history); const onRunQueryRef = useLatest(onRunQuery); const onBlurRef = useLatest(onBlur); - const onChangeRef = useLatest(onChange); const autocompleteDisposeFun = useRef<(() => void) | null>(null); @@ -134,6 +132,8 @@ const MonacoQueryField = (props: Props) => { ref={containerRef} > { editor.onDidContentSizeChange(updateElementHeight); updateElementHeight(); - // Whenever the editor changes, lets save the last value so the next query for this editor will be up-to-date. - // This change is being introduced to fix a bug where you can submit a query via shift+enter: - // If you clicked into another field and haven't un-blurred the active field, - // then the query that is run will be stale, as the reference is only updated - // with the value of the last blurred input. - // This can run quite slowly, so we're debouncing this which should accomplish two things - // 1. Should prevent this function from blocking the current call stack by pushing into the web API callback queue - // 2. Should prevent a bunch of duplicates of this function being called as the user is typing - const updateCurrentEditorValue = debounce(() => { - const editorValue = editor.getValue(); - onChangeRef.current(editorValue); - }, lpRef.current.datasource.getDebounceTimeInMilliseconds()); - - editor.getModel()?.onDidChangeContent(() => { - updateCurrentEditorValue(); - }); - // handle: shift + enter // FIXME: maybe move this functionality into CodeEditor? editor.addCommand( @@ -235,9 +218,8 @@ const MonacoQueryField = (props: Props) => { command: null, }); - /* Something in this configuration of monaco doesn't bubble up [mod]+K, which the - command palette uses. Pass the event out of monaco manually - */ + // Something in this configuration of monaco doesn't bubble up [mod]+K, + // which the command palette uses. Pass the event out of monaco manually editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, function () { global.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })); }); diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldLazy.tsx b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldLazy.tsx index 53aad0b3047..53869e3cf58 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldLazy.tsx +++ b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldLazy.tsx @@ -4,8 +4,6 @@ import { Suspense } from 'react'; import MonacoQueryField from './MonacoQueryField'; import { Props } from './MonacoQueryFieldProps'; -// const Field = React.lazy(() => import('./MonacoQueryField')); - export const MonacoQueryFieldLazy = (props: Props) => { return ( diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts index a0d21761f2c..4ca3fd894ed 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts @@ -16,7 +16,5 @@ export type Props = { placeholder: string; onRunQuery: (value: string) => void; onBlur: (value: string) => void; - // onChange will never initiate a query, it just denotes that a query value has been changed - onChange: (value: string) => void; datasource: PrometheusDatasource; }; diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldWrapper.tsx b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldWrapper.tsx index c95e8341b92..b46234a3b6b 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldWrapper.tsx +++ b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldWrapper.tsx @@ -23,13 +23,5 @@ export const MonacoQueryFieldWrapper = (props: Props) => { onChange(value); }; - /** - * Handles changes without running any queries - * @param value - */ - const handleChange = (value: string) => { - onChange(value); - }; - - return ; + return ; }; From 53ae85ca57c94f3ada456bf2c905c57757a4ff77 Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:20:47 -0600 Subject: [PATCH 502/894] docs: updates to support prometheus data source learning journey (#100363) * updates to support prometheus data source learning journey * makes prettier --- .../configure-prometheus-data-source.md | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/sources/datasources/prometheus/configure-prometheus-data-source.md b/docs/sources/datasources/prometheus/configure-prometheus-data-source.md index 077b6aeff04..06cedd3f081 100644 --- a/docs/sources/datasources/prometheus/configure-prometheus-data-source.md +++ b/docs/sources/datasources/prometheus/configure-prometheus-data-source.md @@ -48,6 +48,8 @@ Grafana ships with built-in support for Prometheus. If you are new to Prometheus ## Configure the data source +{{< shared id="add-prom-data-source" >}} + To add the Prometheus data source, complete the following steps: 1. Click **Connections** in the left-side menu. @@ -55,6 +57,9 @@ To add the Prometheus data source, complete the following steps: 1. Enter `Prometheus` in the search bar. 1. Select **Prometheus**. 1. Click **Add new data source** in the upper right. +1. Enter a name for the data source. + +{{< /shared >}} You will be taken to the **Settings** tab where you will set up your Prometheus configuration. @@ -70,7 +75,8 @@ The first option to configure is the name of your connection: ### Connection section -- **Prometheus server URL** - The URL of your Prometheus server. If your Prometheus server is local, use `http://localhost:9090`. If it is on a server within a network, this is the URL with port where you are running Prometheus. Example: `http://prometheus.example.orgname:9090`. +- **Prometheus server URL** - The URL of your Prometheus server. {{< shared id="prom-data-source-url" >}} + If your Prometheus server is local, use `http://localhost:9090`. If it's on a server within a network, this is the URL with the port where you are running Prometheus. Example: `http://prometheus.example.orgname:9090`. {{< admonition type="note" >}} @@ -80,6 +86,8 @@ You should use the IP address of the Prometheus container, or the hostname if yo {{< /admonition >}} +{{< /shared >}} + ### Authentication section There are several authentication methods you can choose in the Authentication section. @@ -171,3 +179,15 @@ Support for exemplars is available only for the Prometheus data source. If this - **Label name** - The name of the field in the `labels` object used to obtain the traceID property. - **Remove exemplar link** - Click to remove existing links. + +### Troubleshooting + +Refer to the following troubleshooting information, as required. + +#### Data doesn't appear in Explore metrics + +If metric data doesn't appear in Explore after you've successfully tested a connection to a Prometheus data source, ensure that you've selected the correct data source in the **Data source** drop-down menu. + +The following image shows the **Data source** field in Explore metrics. + +![Image that shows Prometheus metrics in Explore](/media/docs/grafana/data-sources/prometheus/troubleshoot-connection-1.png) From 6d374a3d7f786a74ba262438c4333d58df0fe9ff Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 11 Feb 2025 19:41:34 +0200 Subject: [PATCH 503/894] Configuration tracker: Update copy in IRM and point to new IRM slack integration (#100440) Update copy + point to new IRM slack --- public/app/features/gops/configuration-tracker/irmHooks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/gops/configuration-tracker/irmHooks.ts b/public/app/features/gops/configuration-tracker/irmHooks.ts index 76c738e80f3..082d96278ab 100644 --- a/public/app/features/gops/configuration-tracker/irmHooks.ts +++ b/public/app/features/gops/configuration-tracker/irmHooks.ts @@ -235,7 +235,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { }, { title: 'Respond', - description: 'Configure OnCall and Incident', + description: getIsIrmPluginPresent() ? 'Configure IRM' : 'Configure OnCall and Incident', steps: getIsIrmPluginPresent() ? [ { @@ -261,7 +261,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/grate.slack`, + url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/apps/grate.irm.slack`, }, label: 'Connect', urlLinkOnDone: { From b6ea06f25981de33c9cb5064c1b3cdb1865e637b Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 11 Feb 2025 11:42:30 -0600 Subject: [PATCH 504/894] Docs: Updating manual installation instructions (#98834) * Docs: adding additional installation steps to Grafana on openSUSE docs * Docs: Adding systemd service info for grafana manual install * Finalizing first edit with tested steps * spacing adjustment and adding steps to RHEL/Fedora * Adding a note based on Marins feedback * A slight adjustment based on feedback from Marin and adding steps to Debian installation * adjusting some wording * adjusting naming conventions for Debian instructions * changing "open-source" to "open source" * vale and review edits * deleting erroneous character in shortcode * updating some shortcodes --------- Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --- .../installation/debian/index.md | 88 +++++++++++++++++-- .../installation/redhat-rhel-fedora/index.md | 72 +++++++++++++-- .../installation/suse-opensuse/index.md | 68 +++++++++++++- 3 files changed, 213 insertions(+), 15 deletions(-) diff --git a/docs/sources/setup-grafana/installation/debian/index.md b/docs/sources/setup-grafana/installation/debian/index.md index 1aeb3203e4b..09965c27678 100644 --- a/docs/sources/setup-grafana/installation/debian/index.md +++ b/docs/sources/setup-grafana/installation/debian/index.md @@ -18,9 +18,9 @@ This topic explains how to install Grafana dependencies, install Grafana on Linu There are multiple ways to install Grafana: using the Grafana Labs APT repository, by downloading a `.deb` package, or by downloading a binary `.tar.gz` file. Choose only one of the methods below that best suits your needs. -{{% admonition type="note" %}} +{{< admonition type="note" >}} If you install via the `.deb` package or `.tar.gz` file, then you must manually update Grafana for each new version. -{{% /admonition %}} +{{< /admonition >}} The following video demonstrates how to install Grafana on Debian and Ubuntu as outlined in this document: @@ -37,9 +37,9 @@ If you install from the APT repository, Grafana automatically updates when you r | Grafana OSS | grafana | `https://apt.grafana.com stable main` | | Grafana OSS (Beta) | grafana | `https://apt.grafana.com beta main` | -{{% admonition type="note" %}} +{{< admonition type="note" >}} Grafana Enterprise is the recommended and default edition. It is available for free and includes all the features of the OSS edition. You can also upgrade to the [full Enterprise feature set](/products/enterprise/?utm_source=grafana-install-page), which has support for [Enterprise plugins](/grafana/plugins/?enterprise=1&utcm_source=grafana-install-page). -{{% /admonition %}} +{{< /admonition >}} Complete the following steps to install Grafana from the APT repository: @@ -89,11 +89,11 @@ Complete the following steps to install Grafana from the APT repository: sudo apt-get install grafana-enterprise ``` -## Install Grafana using a deb package or as a standalone binary +## Install Grafana using a deb package -If you choose not to install Grafana using APT, you can download and install Grafana using the deb package or as a standalone binary. +If you install Grafana manually using the deb package, then you must manually update Grafana for each new version. -Complete the following steps to install Grafana using DEB or the standalone binaries: +Complete the following steps to install Grafana using a deb package: 1. Navigate to the [Grafana download page](/grafana/download). 1. Select the Grafana version you want to install. @@ -105,6 +105,80 @@ Complete the following steps to install Grafana using DEB or the standalone bina 1. Depending on which system you are running, click the **Linux** or **ARM** tab on the [download page](/grafana/download). 1. Copy and paste the code from the [download page](/grafana/download) into your command line and run. +## Install Grafana as a standalone binary + +Complete the following steps to install Grafana using the standalone binaries: + +1. Navigate to the [Grafana download page](/grafana/download). +1. Select the Grafana version you want to install. + - The most recent Grafana version is selected by default. + - The **Version** field displays only tagged releases. If you want to install a nightly build, click **Nightly Builds** and then select a version. +1. Select an **Edition**. + - **Enterprise:** This is the recommended version. It is functionally identical to the open source version but includes features you can unlock with a license if you so choose. + - **Open Source:** This version is functionally identical to the Enterprise version, but you will need to download the Enterprise version if you want Enterprise features. +1. Depending on which system you are running, click the **Linux** or **ARM** tab on the [download page](/grafana/download). +1. Copy and paste the code from the [download page](/grafana/download) page into your command line and run. +1. Create a user account for Grafana on your system: + + ```shell + sudo useradd -r -s /bin/false grafana + ``` + +1. Move the unpacked binary to `/usr/local/grafana`: + + ```shell + sudo mv /usr/local/grafana + ``` + +1. Change the owner of `/usr/local/grafana` to Grafana users: + + ```shell + sudo chown -R grafana:users /usr/local/grafana + ``` + +1. Create a Grafana server systemd unit file: + + ```shell + sudo touch /etc/systemd/system/grafana-server.service + ``` + +1. Add the following to the unit file in a text editor of your choice: + + ```ini + [Unit] + Description=Grafana Server + After=network.target + + [Service] + Type=simple + User=grafana + Group=users + ExecStart=/usr/local/grafana/bin/grafana server --config=/usr/local/grafana/conf/grafana.ini --homepath=/usr/local/grafana + Restart=on-failure + + [Install] + WantedBy=multi-user.target + ``` + +1. Use the binary to manually start the Grafana server: + + ```shell + /usr/local/grafana/bin/grafana-server --homepath /usr/local/grafana + ``` + + {{< admonition type="note" >}} + Manually invoking the binary in this step automatically creates the `/usr/local/grafana/data` directory, which needs to be created and configured before the installation can be considered complete. + {{< /admonition >}} + +1. Press `CTRL+C` to stop the Grafana server. +1. Change the owner of `/usr/local/grafana` to Grafana users again to apply the ownership to the newly created `/usr/local/grafana/data` directory: + + ```shell + sudo chown -R grafana:users /usr/local/grafana + ``` + +1. [Configure the Grafana server to start at boot time using systemd](https://grafana.com/docs/grafana/latest/setup-grafana/start-restart-grafana/#configure-the-grafana-server-to-start-at-boot-using-systemd). + ## Uninstall on Debian or Ubuntu Complete any of the following steps to uninstall Grafana. diff --git a/docs/sources/setup-grafana/installation/redhat-rhel-fedora/index.md b/docs/sources/setup-grafana/installation/redhat-rhel-fedora/index.md index 2a24eadc392..4a23ffb832c 100644 --- a/docs/sources/setup-grafana/installation/redhat-rhel-fedora/index.md +++ b/docs/sources/setup-grafana/installation/redhat-rhel-fedora/index.md @@ -32,15 +32,15 @@ If you install from the RPM repository, then Grafana is automatically updated ev | Grafana OSS | grafana | `https://rpm.grafana.com` | | Grafana OSS (Beta) | grafana | `https://rpm-beta.grafana.com` | -{{% admonition type="note" %}} +{{< admonition type="note" >}} Grafana Enterprise is the recommended and default edition. It is available for free and includes all the features of the OSS edition. You can also upgrade to the [full Enterprise feature set](/products/enterprise/?utm_source=grafana-install-page), which has support for [Enterprise plugins](/grafana/plugins/?enterprise=1&utcm_source=grafana-install-page). -{{% /admonition %}} +{{< /admonition >}} To install Grafana from the RPM repository, complete the following steps: -{{% admonition type="note" %}} +{{< admonition type="note" >}} If you wish to install beta versions of Grafana, substitute the repository URL for the beta URL listed above. -{{% /admonition %}} +{{< /admonition >}} 1. Import the GPG key: @@ -96,6 +96,8 @@ If you install Grafana manually using YUM or RPM, then you must manually update ## Install Grafana as a standalone binary +If you install Grafana manually using the standalone binaries, then you must manually update Grafana for each new version. + Complete the following steps to install Grafana using the standalone binaries: 1. Navigate to the [Grafana download page](/grafana/download). @@ -103,10 +105,70 @@ Complete the following steps to install Grafana using the standalone binaries: - The most recent Grafana version is selected by default. - The **Version** field displays only tagged releases. If you want to install a nightly build, click **Nightly Builds** and then select a version. 1. Select an **Edition**. - - **Enterprise:** This is the recommended version. It is functionally identical to the open-source version but includes features you can unlock with a license if you so choose. + - **Enterprise:** This is the recommended version. It is functionally identical to the open source version but includes features you can unlock with a license if you so choose. - **Open Source:** This version is functionally identical to the Enterprise version, but you will need to download the Enterprise version if you want Enterprise features. 1. Depending on which system you are running, click the **Linux** or **ARM** tab on the [download page](/grafana/download). 1. Copy and paste the code from the [download page](/grafana/download) page into your command line and run. +1. Create a user account for Grafana on your system: + + ```shell + sudo useradd -r -s /bin/false grafana + ``` + +1. Move the unpacked binary to `/usr/local/grafana`: + + ```shell + sudo mv /usr/local/grafana + ``` + +1. Change the owner of `/usr/local/grafana` to Grafana users: + + ```shell + sudo chown -R grafana:users /usr/local/grafana + ``` + +1. Create a Grafana server systemd unit file: + + ```shell + sudo touch /etc/systemd/system/grafana-server.service + ``` + +1. Add the following to the unit file in a text editor of your choice: + + ```ini + [Unit] + Description=Grafana Server + After=network.target + + [Service] + Type=simple + User=grafana + Group=users + ExecStart=/usr/local/grafana/bin/grafana server --config=/usr/local/grafana/conf/grafana.ini --homepath=/usr/local/grafana + Restart=on-failure + + [Install] + WantedBy=multi-user.target + ``` + +1. Use the binary to manually start the Grafana server: + + ```shell + /usr/local/grafana/bin/grafana-server --homepath /usr/local/grafana + ``` + + {{< admonition type="note" >}} + Manually invoking the binary in this step automatically creates the `/usr/local/grafana/data` directory, which needs to be created and configured before the installation can be considered complete. + {{< /admonition >}} + +1. Press `CTRL+C` to stop the Grafana server. +1. Change the owner of `/usr/local/grafana` to Grafana users again to apply the ownership to the newly created `/usr/local/grafana/data` directory: + + ```shell + sudo chown -R grafana:users /usr/local/grafana + ``` + +1. [Configure the Grafana server to start at boot time using systemd]({{< relref "../../start-restart-grafana#configure-the-grafana-server-to-start-at-boot-using-systemd" >}}). ## Uninstall on RHEL or Fedora diff --git a/docs/sources/setup-grafana/installation/suse-opensuse/index.md b/docs/sources/setup-grafana/installation/suse-opensuse/index.md index 48b505ba52d..09f76c8dd5e 100644 --- a/docs/sources/setup-grafana/installation/suse-opensuse/index.md +++ b/docs/sources/setup-grafana/installation/suse-opensuse/index.md @@ -30,9 +30,9 @@ If you install from the RPM repository, then Grafana is automatically updated ev | Grafana Enterprise | grafana-enterprise | `https://rpm.grafana.com` | | Grafana OSS | grafana | `https://rpm.grafana.com` | -{{% admonition type="note" %}} +{{< admonition type="note" >}} Grafana Enterprise is the recommended and default edition. It is available for free and includes all the features of the OSS edition. You can also upgrade to the [full Enterprise feature set](/products/enterprise/?utm_source=grafana-install-page), which has support for [Enterprise plugins](/grafana/plugins/?enterprise=1&utcm_source=grafana-install-page). -{{% /admonition %}} +{{< /admonition >}} To install Grafana using the RPM repository, complete the following steps: @@ -84,6 +84,8 @@ If you install Grafana manually using RPM, then you must manually update Grafana ## Install Grafana as a standalone binary +If you install Grafana manually using the standalone binaries, then you must manually update Grafana for each new version. + Complete the following steps to install Grafana using the standalone binaries: 1. Navigate to the [Grafana download page](/grafana/download). @@ -91,10 +93,70 @@ Complete the following steps to install Grafana using the standalone binaries: - The most recent Grafana version is selected by default. - The **Version** field displays only tagged releases. If you want to install a nightly build, click **Nightly Builds** and then select a version. 1. Select an **Edition**. - - **Enterprise:** This is the recommended version. It is functionally identical to the open-source version but includes features you can unlock with a license if you so choose. + - **Enterprise:** This is the recommended version. It is functionally identical to the open source version but includes features you can unlock with a license if you so choose. - **Open Source:** This version is functionally identical to the Enterprise version, but you will need to download the Enterprise version if you want Enterprise features. 1. Depending on which system you are running, click the **Linux** or **ARM** tab on the [download page](/grafana/download). 1. Copy and paste the code from the [download page](/grafana/download) into your command line and run. +1. Create a user account for Grafana on your system: + + ```shell + sudo useradd -r -s /bin/false grafana + ``` + +1. Move the unpacked binary to `/usr/local/grafana`: + + ```shell + sudo mv /usr/local/grafana + ``` + +1. Change the owner of `/usr/local/grafana` to Grafana users: + + ```shell + sudo chown -R grafana:users /usr/local/grafana + ``` + +1. Create a Grafana server systemd unit file: + + ```shell + sudo touch /etc/systemd/system/grafana-server.service + ``` + +1. Add the following to the unit file in a text editor of your choice: + + ```ini + [Unit] + Description=Grafana Server + After=network.target + + [Service] + Type=simple + User=grafana + Group=users + ExecStart=/usr/local/grafana/bin/grafana server --config=/usr/local/grafana/conf/grafana.ini --homepath=/usr/local/grafana + Restart=on-failure + + [Install] + WantedBy=multi-user.target + ``` + +1. Use the binary to manually start the Grafana server: + + ```shell + /usr/local/grafana/bin/grafana-server --homepath /usr/local/grafana + ``` + + {{< admonition type="note" >}} + Manually invoking the binary in this step automatically creates the `/usr/local/grafana/data` directory, which needs to be created and configured before the installation can be considered complete. + {{< /admonition >}} + +1. Press `CTRL+C` to stop the Grafana server. +1. Change the owner of `/usr/local/grafana` to Grafana users again to apply the ownership to the newly created `/usr/local/grafana/data` directory: + + ```shell + sudo chown -R grafana:users /usr/local/grafana + ``` + +1. [Configure the Grafana server to start at boot time using systemd](https://grafana.com/docs/grafana/latest/setup-grafana/start-restart-grafana/#configure-the-grafana-server-to-start-at-boot-using-systemd). ## Uninstall on SUSE or openSUSE From a5355fd66c96aac330b298186fd91a5d44648327 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 11 Feb 2025 20:57:46 +0300 Subject: [PATCH 505/894] Storage: Add command line tool to migrate legacy dashboards (and folders) to unified storage (#99199) --- go.mod | 5 + go.sum | 1 + pkg/cmd/grafana-cli/commands/commands.go | 5 + .../commands/datamigrations/stubs.go | 70 + .../datamigrations/to_unified_storage.go | 207 +++ .../datamigrations/to_unified_storage_test.go | 29 + pkg/registry/apis/dashboard/legacy/client.go | 7 +- pkg/registry/apis/dashboard/legacy/migrate.go | 411 +++++ pkg/registry/apis/dashboard/legacy/types.go | 1 + .../folderimpl/folder_unifiedstorage_test.go | 3 + pkg/storage/unified/apistore/go.mod | 6 + pkg/storage/unified/apistore/go.sum | 4 + pkg/storage/unified/parquet/README.md | 6 + pkg/storage/unified/parquet/client.go | 78 + pkg/storage/unified/parquet/reader.go | 264 +++ pkg/storage/unified/parquet/reader_test.go | 125 ++ pkg/storage/unified/parquet/writer.go | 209 +++ pkg/storage/unified/resource/batch.go | 297 +++ pkg/storage/unified/resource/client.go | 25 +- pkg/storage/unified/resource/keys.go | 27 +- pkg/storage/unified/resource/keys_test.go | 9 +- pkg/storage/unified/resource/resource.pb.go | 1617 +++++++++++------ pkg/storage/unified/resource/resource.proto | 68 +- .../unified/resource/resource_grpc.pb.go | 129 ++ pkg/storage/unified/resource/search.go | 13 +- pkg/storage/unified/resource/server.go | 11 +- pkg/storage/unified/sql/backend.go | 12 +- pkg/storage/unified/sql/batch.go | 338 ++++ pkg/storage/unified/sql/batch_test.go | 24 + .../sql/data/resource_insert_from_history.sql | 52 + pkg/storage/unified/sql/queries.go | 13 + pkg/storage/unified/sql/queries_test.go | 13 + pkg/storage/unified/sql/service.go | 2 + ...l--resource_insert_from_history-update.sql | 34 + ...s--resource_insert_from_history-update.sql | 34 + ...e--resource_insert_from_history-update.sql | 34 + 36 files changed, 3569 insertions(+), 614 deletions(-) create mode 100644 pkg/cmd/grafana-cli/commands/datamigrations/stubs.go create mode 100644 pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go create mode 100644 pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage_test.go create mode 100644 pkg/registry/apis/dashboard/legacy/migrate.go create mode 100644 pkg/storage/unified/parquet/README.md create mode 100644 pkg/storage/unified/parquet/client.go create mode 100644 pkg/storage/unified/parquet/reader.go create mode 100644 pkg/storage/unified/parquet/reader_test.go create mode 100644 pkg/storage/unified/parquet/writer.go create mode 100644 pkg/storage/unified/resource/batch.go create mode 100644 pkg/storage/unified/sql/batch.go create mode 100644 pkg/storage/unified/sql/batch_test.go create mode 100644 pkg/storage/unified/sql/data/resource_insert_from_history.sql create mode 100755 pkg/storage/unified/sql/testdata/mysql--resource_insert_from_history-update.sql create mode 100755 pkg/storage/unified/sql/testdata/postgres--resource_insert_from_history-update.sql create mode 100755 pkg/storage/unified/sql/testdata/sqlite--resource_insert_from_history-update.sql diff --git a/go.mod b/go.mod index e8ac930a4ca..59cc35cf90e 100644 --- a/go.mod +++ b/go.mod @@ -241,6 +241,7 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect github.com/FZambia/eagle v0.1.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 github.com/Masterminds/squirrel v1.5.4 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect @@ -250,6 +251,7 @@ require ( github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/apache/thrift v0.21.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect @@ -397,6 +399,7 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/klauspost/asmfmt v1.3.2 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -416,6 +419,8 @@ require ( github.com/mdlayher/vsock v1.2.1 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/miekg/dns v1.1.62 // indirect + github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect + github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect diff --git a/go.sum b/go.sum index b4820083bad..7ce560e11a0 100644 --- a/go.sum +++ b/go.sum @@ -708,6 +708,7 @@ github.com/FZambia/eagle v0.1.0 h1:9gyX6x+xjoIfglgyPTcYm7dvY7FJ93us1QY5De4CyXA= github.com/FZambia/eagle v0.1.0/go.mod h1:YjGSPVkQTNcVLfzEUQJNgW9ScPR0K4u/Ky0yeFa4oDA= 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= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index a144f4491fd..e977deb84eb 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -144,6 +144,11 @@ var adminCommands = []*cli.Command{ Usage: "Migrates passwords from unsecured fields to secure_json_data field. Return ok unless there is an error. Safe to execute multiple times.", Action: runDbCommand(datamigrations.EncryptDatasourcePasswords), }, + { + Name: "to-unified-storage", + Usage: "Migrates classic SQL data into unified storage", + Action: runDbCommand(datamigrations.ToUnifiedStorage), + }, }, }, { diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/stubs.go b/pkg/cmd/grafana-cli/commands/datamigrations/stubs.go new file mode 100644 index 00000000000..22dc15d6fc7 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/datamigrations/stubs.go @@ -0,0 +1,70 @@ +package datamigrations + +import ( + "context" + "path/filepath" + + "github.com/grafana/grafana/pkg/services/provisioning" + "github.com/grafana/grafana/pkg/services/provisioning/dashboards" +) + +var ( + _ provisioning.ProvisioningService = (*stubProvisioning)(nil) +) + +func newStubProvisioning(path string) (provisioning.ProvisioningService, error) { + cfgs, err := dashboards.ReadDashboardConfig(filepath.Join(path, "dashboards")) + if err != nil { + return nil, err + } + stub := &stubProvisioning{ + path: make(map[string]string), + } + for _, cfg := range cfgs { + stub.path[cfg.Name] = cfg.Options["path"].(string) + } + return &stubProvisioning{}, nil +} + +type stubProvisioning struct { + path map[string]string // name > options.path +} + +// GetAllowUIUpdatesFromConfig implements provisioning.ProvisioningService. +func (s *stubProvisioning) GetAllowUIUpdatesFromConfig(name string) bool { + return false +} + +func (s *stubProvisioning) GetDashboardProvisionerResolvedPath(name string) string { + return s.path[name] +} + +// ProvisionAlerting implements provisioning.ProvisioningService. +func (s *stubProvisioning) ProvisionAlerting(ctx context.Context) error { + panic("unimplemented") +} + +// ProvisionDashboards implements provisioning.ProvisioningService. +func (s *stubProvisioning) ProvisionDashboards(ctx context.Context) error { + panic("unimplemented") +} + +// ProvisionDatasources implements provisioning.ProvisioningService. +func (s *stubProvisioning) ProvisionDatasources(ctx context.Context) error { + panic("unimplemented") +} + +// ProvisionPlugins implements provisioning.ProvisioningService. +func (s *stubProvisioning) ProvisionPlugins(ctx context.Context) error { + panic("unimplemented") +} + +// Run implements provisioning.ProvisioningService. +func (s *stubProvisioning) Run(ctx context.Context) error { + panic("unimplemented") +} + +// RunInitProvisioners implements provisioning.ProvisioningService. +func (s *stubProvisioning) RunInitProvisioners(ctx context.Context) error { + panic("unimplemented") +} diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go new file mode 100644 index 00000000000..6b6673a9742 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go @@ -0,0 +1,207 @@ +package datamigrations + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" + "k8s.io/apimachinery/pkg/runtime/schema" + + dashboard "github.com/grafana/grafana/pkg/apis/dashboard" + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified" + "github.com/grafana/grafana/pkg/storage/unified/parquet" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +// ToUnifiedStorage converts dashboards+folders into unified storage +func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) error { + namespace := "default" // TODO... from command line + ns, err := authlib.ParseNamespace(namespace) + if err != nil { + return err + } + ctx := identity.WithServiceIdentityContext(context.Background(), ns.OrgID) + start := time.Now() + last := time.Now() + + opts := legacy.MigrateOptions{ + Namespace: namespace, + Resources: []schema.GroupResource{ + {Group: folders.GROUP, Resource: folders.RESOURCE}, + {Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE}, + {Group: dashboard.GROUP, Resource: dashboard.LIBRARY_PANEL_RESOURCE}, + }, + LargeObjects: nil, // TODO... from config + Progress: func(count int, msg string) { + if count < 1 || time.Since(last) > time.Second { + fmt.Printf("[%4d] %s\n", count, msg) + last = time.Now() + } + }, + } + + provisioning, err := newStubProvisioning(cfg.ProvisioningPath) + if err != nil { + return err + } + + migrator := legacy.NewDashboardAccess( + legacysql.NewDatabaseProvider(sqlStore), + authlib.OrgNamespaceFormatter, + nil, provisioning, false, + ) + + yes, err := promptYesNo(fmt.Sprintf("Count legacy resources for namespace: %s?", opts.Namespace)) + if err != nil { + return err + } + if yes { + opts.OnlyCount = true + rsp, err := migrator.Migrate(ctx, opts) + if err != nil { + return err + } + + fmt.Printf("Counting DONE: %s\n", time.Since(start)) + if rsp != nil { + jj, _ := json.MarshalIndent(rsp, "", " ") + fmt.Printf("%s\n", string(jj)) + } + } + + opts.OnlyCount = false + opts.WithHistory, err = promptYesNo("Include history in exports?") + if err != nil { + return err + } + + yes, err = promptYesNo("Export legacy resources to parquet file?") + if err != nil { + return err + } + if yes { + file, err := os.CreateTemp(cfg.DataPath, "grafana-export-*.parquet") + if err != nil { + return err + } + start = time.Now() + last = time.Now() + opts.Store, err = newParquetClient(file) + if err != nil { + return err + } + rsp, err := migrator.Migrate(ctx, opts) + if err != nil { + return err + } + fmt.Printf("Parquet export DONE: %s\n", time.Since(start)) + if rsp != nil { + jj, _ := json.MarshalIndent(rsp, "", " ") + fmt.Printf("%s\n", string(jj)) + } + fmt.Printf("File: %s\n", file.Name()) + } + + yes, err = promptYesNo("Export legacy resources to unified storage?") + if err != nil { + return err + } + if yes { + client, err := newUnifiedClient(cfg, sqlStore) + if err != nil { + return err + } + + // Check the stats (eventually compare) + req := &resource.ResourceStatsRequest{ + Namespace: opts.Namespace, + } + for _, r := range opts.Resources { + req.Kinds = append(req.Kinds, fmt.Sprintf("%s/%s", r.Group, r.Resource)) + } + + stats, err := client.GetStats(ctx, req) + if err != nil { + return err + } + + if stats != nil { + fmt.Printf("Existing resources in unified storage:\n") + jj, _ := json.MarshalIndent(stats, "", " ") + fmt.Printf("%s\n", string(jj)) + } + + yes, err = promptYesNo("Would you like to continue? (existing resources will be replaced)") + if err != nil { + return err + } + if yes { + start = time.Now() + last = time.Now() + opts.Store = client + opts.BlobStore = client + rsp, err := migrator.Migrate(ctx, opts) + if err != nil { + return err + } + fmt.Printf("Unified storage export: %s\n", time.Since(start)) + if rsp != nil { + jj, _ := json.MarshalIndent(rsp, "", " ") + fmt.Printf("%s\n", string(jj)) + } + } + } + return nil +} + +func promptYesNo(prompt string) (bool, error) { + line := "" + for { + fmt.Printf("%s (Y/N) >", prompt) + _, err := fmt.Scanln(&line) + if err != nil && err.Error() != "unexpected newline" { + return false, err + } + switch strings.ToLower(line) { + case "y", "yes": + return true, nil + case "n", "no": + return false, nil + } + } +} + +func newUnifiedClient(cfg *setting.Cfg, sqlStore db.DB) (resource.ResourceClient, error) { + return unified.ProvideUnifiedStorageClient(cfg, + featuremgmt.WithFeatures(), // none?? + sqlStore, + tracing.NewNoopTracerService(), + prometheus.NewPedanticRegistry(), + authlib.FixedAccessClient(true), // always true! + nil, // document supplier + ) +} + +func newParquetClient(file *os.File) (resource.BatchStoreClient, error) { + writer, err := parquet.NewParquetWriter(file) + if err != nil { + return nil, err + } + client := parquet.NewBatchResourceWriterClient(writer) + return client, nil +} diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage_test.go b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage_test.go new file mode 100644 index 00000000000..1fdbfc7b026 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage_test.go @@ -0,0 +1,29 @@ +package datamigrations + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/db" +) + +func TestUnifiedStorageCommand(t *testing.T) { + // setup datasources with password, basic_auth and none + store := db.InitTestDB(t) + err := store.WithDbSession(context.Background(), func(sess *db.Session) error { + unistoreMigrationTest(t, sess, store) + return nil + }) + require.NoError(t, err) +} + +func unistoreMigrationTest(t *testing.T, session *db.Session, sqlstore db.DB) { + // empty stats + + t.Run("get stats", func(t *testing.T) { + fmt.Printf("TODO... add folders and check that they migrate\n") + }) +} diff --git a/pkg/registry/apis/dashboard/legacy/client.go b/pkg/registry/apis/dashboard/legacy/client.go index eee92598a15..77dc173b69f 100644 --- a/pkg/registry/apis/dashboard/legacy/client.go +++ b/pkg/registry/apis/dashboard/legacy/client.go @@ -87,5 +87,10 @@ func (d *directResourceClient) Update(ctx context.Context, in *resource.UpdateRe // Watch implements ResourceClient. func (d *directResourceClient) Watch(ctx context.Context, in *resource.WatchRequest, opts ...grpc.CallOption) (resource.ResourceStore_WatchClient, error) { - return nil, fmt.Errorf("watch not yet supported with direct resource client") + return nil, fmt.Errorf("watch not supported with direct resource client") +} + +// BatchProcess implements resource.ResourceClient. +func (d *directResourceClient) BatchProcess(ctx context.Context, opts ...grpc.CallOption) (resource.BatchStore_BatchProcessClient, error) { + return nil, fmt.Errorf("BatchProcess not supported with direct resource client") } diff --git a/pkg/registry/apis/dashboard/legacy/migrate.go b/pkg/registry/apis/dashboard/legacy/migrate.go new file mode 100644 index 00000000000..c1236ef4f2d --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/migrate.go @@ -0,0 +1,411 @@ +package legacy + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + + "google.golang.org/grpc/metadata" + "k8s.io/apimachinery/pkg/runtime/schema" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/utils" + dashboard "github.com/grafana/grafana/pkg/apis/dashboard" + folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/storage/unified/apistore" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +type MigrateOptions struct { + Namespace string + Store resource.BatchStoreClient + Writer resource.BatchResourceWriter + LargeObjects apistore.LargeObjectSupport + BlobStore resource.BlobStoreClient + Resources []schema.GroupResource + WithHistory bool // only applies to dashboards + OnlyCount bool // just count the values + Progress func(count int, msg string) +} + +// Read from legacy and write into unified storage +type LegacyMigrator interface { + Migrate(ctx context.Context, opts MigrateOptions) (*resource.BatchResponse, error) +} + +type BlobStoreInfo struct { + Count int64 + Size int64 +} + +// migrate function -- works for a single kind +type migrator = func(ctx context.Context, orgId int64, opts MigrateOptions, stream resource.BatchStore_BatchProcessClient) (*BlobStoreInfo, error) + +func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (*resource.BatchResponse, error) { + info, err := authlib.ParseNamespace(opts.Namespace) + if err != nil { + return nil, err + } + + // Migrate everything + if len(opts.Resources) < 1 { + return nil, fmt.Errorf("missing resource selector") + } + + migrators := []migrator{} + settings := resource.BatchSettings{ + RebuildCollection: true, + SkipValidation: true, + } + + for _, res := range opts.Resources { + switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { + case "folder.grafana.app/folders": + migrators = append(migrators, a.migrateFolders) + settings.Collection = append(settings.Collection, &resource.ResourceKey{ + Namespace: opts.Namespace, + Group: folders.GROUP, + Resource: folders.RESOURCE, + }) + + case "dashboard.grafana.app/librarypanels": + migrators = append(migrators, a.migratePanels) + settings.Collection = append(settings.Collection, &resource.ResourceKey{ + Namespace: opts.Namespace, + Group: dashboard.GROUP, + Resource: dashboard.LIBRARY_PANEL_RESOURCE, + }) + + case "dashboard.grafana.app/dashboards": + migrators = append(migrators, a.migrateDashboards) + settings.Collection = append(settings.Collection, &resource.ResourceKey{ + Namespace: opts.Namespace, + Group: dashboard.GROUP, + Resource: dashboard.DASHBOARD_RESOURCE, + }) + default: + return nil, fmt.Errorf("unsupported resource: %s", res) + } + } + + if opts.OnlyCount { + return a.countValues(ctx, opts) + } + + ctx = metadata.NewOutgoingContext(ctx, settings.ToMD()) + stream, err := opts.Store.BatchProcess(ctx) + if err != nil { + return nil, err + } + + // Now run each migration + blobStore := BlobStoreInfo{} + for _, m := range migrators { + blobs, err := m(ctx, info.OrgID, opts, stream) + if err != nil { + return nil, err + } + if blobs != nil { + blobStore.Count += blobs.Count + blobStore.Size += blobs.Size + } + } + fmt.Printf("BLOBS: %+v\n", blobStore) + return stream.CloseAndRecv() +} + +func (a *dashboardSqlAccess) countValues(ctx context.Context, opts MigrateOptions) (*resource.BatchResponse, error) { + sql, err := a.sql(ctx) + if err != nil { + return nil, err + } + ns, err := authlib.ParseNamespace(opts.Namespace) + if err != nil { + return nil, err + } + orgId := ns.OrgID + rsp := &resource.BatchResponse{} + err = sql.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + for _, res := range opts.Resources { + switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { + case "folder.grafana.app/folders": + summary := &resource.BatchResponse_Summary{} + summary.Group = folders.GROUP + summary.Group = folders.RESOURCE + _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("dashboard")+ + " WHERE is_folder=FALSE AND org_id=?", orgId).Get(&summary.Count) + rsp.Summary = append(rsp.Summary, summary) + + case "dashboard.grafana.app/librarypanels": + summary := &resource.BatchResponse_Summary{} + summary.Group = dashboard.GROUP + summary.Resource = dashboard.LIBRARY_PANEL_RESOURCE + _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("library_element")+ + " WHERE org_id=?", orgId).Get(&summary.Count) + rsp.Summary = append(rsp.Summary, summary) + + case "dashboard.grafana.app/dashboards": + summary := &resource.BatchResponse_Summary{} + summary.Group = dashboard.GROUP + summary.Resource = dashboard.DASHBOARD_RESOURCE + rsp.Summary = append(rsp.Summary, summary) + + _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("dashboard")+ + " WHERE is_folder=FALSE AND org_id=?", orgId).Get(&summary.Count) + if err != nil { + return err + } + + // Also count history + _, err = sess.SQL(`SELECT COUNT(*) + FROM `+sql.Table("dashboard_version")+` as dv + JOIN `+sql.Table("dashboard")+` as dd + ON dd.id = dv.dashboard_id + WHERE org_id=?`, orgId).Get(&summary.History) + } + if err != nil { + return err + } + } + return nil + }) + return rsp, nil +} + +func (a *dashboardSqlAccess) migrateDashboards(ctx context.Context, orgId int64, opts MigrateOptions, stream resource.BatchStore_BatchProcessClient) (*BlobStoreInfo, error) { + query := &DashboardQuery{ + OrgID: orgId, + Limit: 100000000, + GetHistory: opts.WithHistory, // include history + } + + blobs := &BlobStoreInfo{} + sql, err := a.sql(ctx) + if err != nil { + return blobs, err + } + + opts.Progress(-1, "migrating dashboards...") + rows, err := a.getRows(ctx, sql, query) + if rows != nil { + defer func() { + _ = rows.Close() + }() + } + if err != nil { + return blobs, err + } + + large := opts.LargeObjects + + // Now send each dashboard + for i := 1; rows.Next(); i++ { + dash := rows.row.Dash + dash.APIVersion = fmt.Sprintf("%s/v0alpha1", dashboard.GROUP) // << eventually v0 + dash.SetNamespace(opts.Namespace) + dash.SetResourceVersion("") // it will be filled in by the backend + + body, err := json.Marshal(dash) + if err != nil { + err = fmt.Errorf("error reading json from: %s // %w", rows.row.Dash.Name, err) + return blobs, err + } + + req := &resource.BatchRequest{ + Key: &resource.ResourceKey{ + Namespace: opts.Namespace, + Group: dashboard.GROUP, + Resource: dashboard.DASHBOARD_RESOURCE, + Name: rows.Name(), + }, + Value: body, + Folder: rows.row.FolderUID, + Action: resource.BatchRequest_ADDED, + } + if dash.Generation > 1 { + req.Action = resource.BatchRequest_MODIFIED + } else if dash.Generation < 0 { + req.Action = resource.BatchRequest_DELETED + } + + // With large object support + if large != nil && len(body) > large.Threshold() { + obj, err := utils.MetaAccessor(dash) + if err != nil { + return blobs, err + } + + opts.Progress(i, fmt.Sprintf("[v:%d] %s Large object (%d)", dash.Generation, dash.Name, len(body))) + err = large.Deconstruct(ctx, req.Key, opts.BlobStore, obj, req.Value) + if err != nil { + return blobs, err + } + + // The smaller version (most of spec removed) + req.Value, err = json.Marshal(dash) + if err != nil { + return blobs, err + } + blobs.Count++ + blobs.Size += int64(len(body)) + } + + opts.Progress(i, fmt.Sprintf("[v:%2d] %s (size:%d / %d|%d)", dash.Generation, dash.Name, len(req.Value), i, rows.count)) + + err = stream.Send(req) + if err != nil { + if errors.Is(err, io.EOF) { + opts.Progress(i, fmt.Sprintf("stream EOF/cancelled. index=%d", i)) + err = nil + } + return blobs, err + } + } + + if len(rows.rejected) > 0 { + for _, row := range rows.rejected { + id := row.Dash.Labels[utils.LabelKeyDeprecatedInternalID] + fmt.Printf("REJECTED: %s / %s\n", id, row.Dash.Name) + opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name)) + } + } + + if rows.Error() != nil { + return blobs, rows.Error() + } + + opts.Progress(-2, fmt.Sprintf("finished dashboards... (%d)", rows.count)) + return blobs, err +} + +func (a *dashboardSqlAccess) migrateFolders(ctx context.Context, orgId int64, opts MigrateOptions, stream resource.BatchStore_BatchProcessClient) (*BlobStoreInfo, error) { + query := &DashboardQuery{ + OrgID: orgId, + Limit: 100000000, + GetFolders: true, + } + + sql, err := a.sql(ctx) + if err != nil { + return nil, err + } + + opts.Progress(-1, "migrating folders...") + rows, err := a.getRows(ctx, sql, query) + if rows != nil { + defer func() { + _ = rows.Close() + }() + } + if err != nil { + return nil, err + } + + // Now send each dashboard + for i := 1; rows.Next(); i++ { + dash := rows.row.Dash + dash.APIVersion = "folder.grafana.app/v0alpha1" + dash.Kind = "Folder" + dash.SetNamespace(opts.Namespace) + dash.SetResourceVersion("") // it will be filled in by the backend + + spec := map[string]any{ + "title": dash.Spec.Object["title"], + } + description := dash.Spec.Object["description"] + if description != nil { + spec["description"] = description + } + dash.Spec.Object = spec + + body, err := json.Marshal(dash) + if err != nil { + return nil, err + } + + req := &resource.BatchRequest{ + Key: &resource.ResourceKey{ + Namespace: opts.Namespace, + Group: "folder.grafana.app", + Resource: "folders", + Name: rows.Name(), + }, + Value: body, + Folder: rows.row.FolderUID, + Action: resource.BatchRequest_ADDED, + } + if dash.Generation > 1 { + req.Action = resource.BatchRequest_MODIFIED + } else if dash.Generation < 0 { + req.Action = resource.BatchRequest_DELETED + } + + opts.Progress(i, fmt.Sprintf("[v:%d] %s (%d)", dash.Generation, dash.Name, len(req.Value))) + + err = stream.Send(req) + if err != nil { + if errors.Is(err, io.EOF) { + err = nil + } + return nil, err + } + } + + if rows.Error() != nil { + return nil, rows.Error() + } + + opts.Progress(-2, fmt.Sprintf("finished folders... (%d)", rows.count)) + return nil, err +} + +func (a *dashboardSqlAccess) migratePanels(ctx context.Context, orgId int64, opts MigrateOptions, stream resource.BatchStore_BatchProcessClient) (*BlobStoreInfo, error) { + opts.Progress(-1, "migrating library panels...") + panels, err := a.GetLibraryPanels(ctx, LibraryPanelQuery{ + OrgID: orgId, + Limit: 1000000, + }) + if err != nil { + return nil, err + } + for i, panel := range panels.Items { + meta, err := utils.MetaAccessor(&panel) + if err != nil { + return nil, err + } + body, err := json.Marshal(panel) + if err != nil { + return nil, err + } + + req := &resource.BatchRequest{ + Key: &resource.ResourceKey{ + Namespace: opts.Namespace, + Group: dashboard.GROUP, + Resource: dashboard.LIBRARY_PANEL_RESOURCE, + Name: panel.Name, + }, + Value: body, + Folder: meta.GetFolder(), + Action: resource.BatchRequest_ADDED, + } + if panel.Generation > 1 { + req.Action = resource.BatchRequest_MODIFIED + } + + opts.Progress(i, fmt.Sprintf("[v:%d] %s (%d)", i, meta.GetName(), len(req.Value))) + + err = stream.Send(req) + if err != nil { + if errors.Is(err, io.EOF) { + err = nil + } + return nil, err + } + } + opts.Progress(-2, fmt.Sprintf("finished panels... (%d)", len(panels.Items))) + return nil, nil +} diff --git a/pkg/registry/apis/dashboard/legacy/types.go b/pkg/registry/apis/dashboard/legacy/types.go index cb01bfe68bb..8a62ca0c8df 100644 --- a/pkg/registry/apis/dashboard/legacy/types.go +++ b/pkg/registry/apis/dashboard/legacy/types.go @@ -49,6 +49,7 @@ type LibraryPanelQuery struct { type DashboardAccess interface { resource.StorageBackend resource.ResourceIndexServer + LegacyMigrator GetDashboard(ctx context.Context, orgId int64, uid string, version int64) (*dashboard.Dashboard, int64, error) SaveDashboard(ctx context.Context, orgId int64, dash *dashboard.Dashboard) (*dashboard.Dashboard, bool, error) diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index b97b2ea51e9..4421b441113 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -529,6 +529,9 @@ func (r resourceClientMock) List(ctx context.Context, in *resource.ListRequest, func (r resourceClientMock) Watch(ctx context.Context, in *resource.WatchRequest, opts ...grpc.CallOption) (resource.ResourceStore_WatchClient, error) { return nil, nil } +func (r resourceClientMock) BatchProcess(ctx context.Context, opts ...grpc.CallOption) (resource.BatchStore_BatchProcessClient, error) { + return nil, nil +} func (r resourceClientMock) Search(ctx context.Context, in *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) { if len(in.Options.Labels) > 0 && in.Options.Labels[0].Key == utils.LabelKeyDeprecatedInternalID && diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index f5e31c31b38..61ab8c11119 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -47,6 +47,7 @@ require ( github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect github.com/BurntSushi/toml v1.4.0 // indirect + github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect @@ -57,8 +58,10 @@ require ( github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect github.com/Yiling-J/theine-go v0.6.0 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect + github.com/andybalholm/brotli v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 // indirect + github.com/apache/thrift v0.21.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/at-wat/mqtt-go v0.19.4 // indirect @@ -218,6 +221,7 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect + github.com/klauspost/asmfmt v1.3.2 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -238,6 +242,8 @@ require ( github.com/mdlayher/vsock v1.2.1 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/miekg/dns v1.1.62 // indirect + github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect + github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 2820fd08a9b..9c7ca47a33a 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -87,6 +87,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 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= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= @@ -1052,6 +1054,8 @@ github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhe github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/pkg/storage/unified/parquet/README.md b/pkg/storage/unified/parquet/README.md new file mode 100644 index 00000000000..221d7a51f58 --- /dev/null +++ b/pkg/storage/unified/parquet/README.md @@ -0,0 +1,6 @@ +# Parquet Support + +This package implements a limited parquet backend that is currently only useful +as a pass-though buffer while batch writing values. + +Eventually this package could evolve into a full storage backend. \ No newline at end of file diff --git a/pkg/storage/unified/parquet/client.go b/pkg/storage/unified/parquet/client.go new file mode 100644 index 00000000000..3b573bdb1f5 --- /dev/null +++ b/pkg/storage/unified/parquet/client.go @@ -0,0 +1,78 @@ +package parquet + +import ( + "context" + "errors" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +var ( + _ resource.BatchStoreClient = (*writerClient)(nil) + _ resource.BatchStore_BatchProcessClient = (*writerClient)(nil) + + errUnimplemented = errors.New("not implemented (BatchResourceWriter as BatchStoreClient shim)") +) + +type writerClient struct { + writer resource.BatchResourceWriter + ctx context.Context +} + +// NewBatchResourceWriterClient wraps a BatchResourceWriter so that it can be used as a ResourceStoreClient +func NewBatchResourceWriterClient(writer resource.BatchResourceWriter) *writerClient { + return &writerClient{writer: writer} +} + +// Send implements resource.ResourceStore_BatchProcessClient. +func (w *writerClient) Send(req *resource.BatchRequest) error { + return w.writer.Write(w.ctx, req.Key, req.Value) +} + +// BatchProcess implements resource.ResourceStoreClient. +func (w *writerClient) BatchProcess(ctx context.Context, opts ...grpc.CallOption) (resource.BatchStore_BatchProcessClient, error) { + if w.ctx != nil { + return nil, fmt.Errorf("only one batch request supported") + } + w.ctx = ctx + return w, nil +} + +// CloseAndRecv implements resource.ResourceStore_BatchProcessClient. +func (w *writerClient) CloseAndRecv() (*resource.BatchResponse, error) { + return w.writer.CloseWithResults() +} + +// CloseSend implements resource.ResourceStore_BatchProcessClient. +func (w *writerClient) CloseSend() error { + return w.writer.Close() +} + +// Context implements resource.ResourceStore_BatchProcessClient. +func (w *writerClient) Context() context.Context { + return w.ctx +} + +// Header implements resource.ResourceStore_BatchProcessClient. +func (w *writerClient) Header() (metadata.MD, error) { + return nil, errUnimplemented +} + +// RecvMsg implements resource.ResourceStore_BatchProcessClient. +func (w *writerClient) RecvMsg(m any) error { + return errUnimplemented +} + +// SendMsg implements resource.ResourceStore_BatchProcessClient. +func (w *writerClient) SendMsg(m any) error { + return errUnimplemented +} + +// Trailer implements resource.ResourceStore_BatchProcessClient. +func (w *writerClient) Trailer() metadata.MD { + return nil +} diff --git a/pkg/storage/unified/parquet/reader.go b/pkg/storage/unified/parquet/reader.go new file mode 100644 index 00000000000..84695d07315 --- /dev/null +++ b/pkg/storage/unified/parquet/reader.go @@ -0,0 +1,264 @@ +package parquet + +import ( + "fmt" + + "github.com/apache/arrow-go/v18/parquet" + "github.com/apache/arrow-go/v18/parquet/file" + + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +var ( + _ resource.BatchRequestIterator = (*parquetReader)(nil) +) + +func NewParquetReader(inputPath string, batchSize int64) (resource.BatchRequestIterator, error) { + return newResourceReader(inputPath, batchSize) +} + +type parquetReader struct { + reader *file.Reader + + namespace *stringColumn + group *stringColumn + resource *stringColumn + name *stringColumn + value *stringColumn + folder *stringColumn + action *int32Column + columns []columnBuffer + + batchSize int64 + + defLevels []int16 + repLevels []int16 + + // how many we already read + bufferSize int + bufferIndex int + rowGroupIDX int + + req *resource.BatchRequest + err error +} + +// Next implements resource.BatchRequestIterator. +func (r *parquetReader) Next() bool { + r.req = nil + for r.err == nil && r.reader != nil { + if r.bufferIndex >= r.bufferSize && r.value.reader.HasNext() { + r.bufferIndex = 0 + r.err = r.readBatch() + if r.err != nil { + return false + } + r.bufferIndex = r.value.count + } + + if r.bufferSize > r.bufferIndex { + i := r.bufferIndex + r.bufferIndex++ + + r.req = &resource.BatchRequest{ + Key: &resource.ResourceKey{ + Group: r.group.buffer[i].String(), + Resource: r.resource.buffer[i].String(), + Namespace: r.namespace.buffer[i].String(), + Name: r.name.buffer[i].String(), + }, + Action: resource.BatchRequest_Action(r.action.buffer[i]), + Value: r.value.buffer[i].Bytes(), + Folder: r.folder.buffer[i].String(), + } + + return true + } + + r.rowGroupIDX++ + if r.rowGroupIDX >= r.reader.NumRowGroups() { + _ = r.reader.Close() + r.reader = nil + return false + } + r.err = r.open(r.reader.RowGroup(r.rowGroupIDX)) + } + + return false +} + +// Request implements resource.BatchRequestIterator. +func (r *parquetReader) Request() *resource.BatchRequest { + return r.req +} + +// RollbackRequested implements resource.BatchRequestIterator. +func (r *parquetReader) RollbackRequested() bool { + return r.err != nil +} + +func newResourceReader(inputPath string, batchSize int64) (*parquetReader, error) { + rdr, err := file.OpenParquetFile(inputPath, true) + if err != nil { + return nil, err + } + + schema := rdr.MetaData().Schema + makeColumn := func(name string) *stringColumn { + index := schema.ColumnIndexByName(name) + if index < 0 { + err = fmt.Errorf("missing column: %s", name) + } + return &stringColumn{ + index: index, + buffer: make([]parquet.ByteArray, batchSize), + } + } + + reader := &parquetReader{ + reader: rdr, + + namespace: makeColumn("namespace"), + group: makeColumn("group"), + resource: makeColumn("resource"), + name: makeColumn("name"), + value: makeColumn("value"), + folder: makeColumn("folder"), + + action: &int32Column{ + index: schema.ColumnIndexByName("action"), + buffer: make([]int32, batchSize), + }, + + batchSize: batchSize, + defLevels: make([]int16, batchSize), + repLevels: make([]int16, batchSize), + } + + if err != nil { + _ = rdr.Close() + return nil, err + } + + reader.columns = []columnBuffer{ + reader.namespace, + reader.group, + reader.resource, + reader.name, + reader.action, + reader.value, + } + + // Empty file, close and return + if rdr.NumRowGroups() < 1 { + err = rdr.Close() + reader.reader = nil + return reader, err + } + + err = reader.open(rdr.RowGroup(0)) + if err != nil { + _ = rdr.Close() + return nil, err + } + + // get the first batch + err = reader.readBatch() + if err != nil { + _ = rdr.Close() + return nil, err + } + + return reader, nil +} + +func (r *parquetReader) open(rgr *file.RowGroupReader) error { + for _, c := range r.columns { + err := c.open(rgr) + if err != nil { + return err + } + } + return nil +} + +func (r *parquetReader) readBatch() error { + r.bufferIndex = 0 + r.bufferSize = 0 + for i, c := range r.columns { + count, err := c.batch(r.batchSize, r.defLevels, r.repLevels) + if err != nil { + return err + } + if i > 0 && r.bufferSize != count { + return fmt.Errorf("expecting the same size for all columns") + } + r.bufferSize = count + } + return nil +} + +//------------------------------- +// Column support +//------------------------------- + +type columnBuffer interface { + open(rgr *file.RowGroupReader) error + batch(batchSize int64, defLevels []int16, repLevels []int16) (int, error) +} + +type stringColumn struct { + index int // within the schema + reader *file.ByteArrayColumnChunkReader + buffer []parquet.ByteArray + count int // the active count +} + +func (c *stringColumn) open(rgr *file.RowGroupReader) error { + tmp, err := rgr.Column(c.index) + if err != nil { + return err + } + var ok bool + c.reader, ok = tmp.(*file.ByteArrayColumnChunkReader) + if !ok { + return fmt.Errorf("expected resource strings") + } + return nil +} + +func (c *stringColumn) batch(batchSize int64, defLevels []int16, repLevels []int16) (int, error) { + _, count, err := c.reader.ReadBatch(batchSize, c.buffer, defLevels, repLevels) + c.count = count + return count, err +} + +type int32Column struct { + index int // within the schemna + reader *file.Int32ColumnChunkReader + buffer []int32 + count int // the active count +} + +func (c *int32Column) open(rgr *file.RowGroupReader) error { + tmp, err := rgr.Column(c.index) + if err != nil { + return err + } + var ok bool + c.reader, ok = tmp.(*file.Int32ColumnChunkReader) + if !ok { + return fmt.Errorf("expected resource strings") + } + return nil +} + +func (c *int32Column) batch(batchSize int64, defLevels []int16, repLevels []int16) (int, error) { + _, count, err := c.reader.ReadBatch(batchSize, c.buffer, defLevels, repLevels) + c.count = count + return count, err +} + +//------------------------------- +// Column support +//------------------------------- diff --git a/pkg/storage/unified/parquet/reader_test.go b/pkg/storage/unified/parquet/reader_test.go new file mode 100644 index 00000000000..e8414c36d74 --- /dev/null +++ b/pkg/storage/unified/parquet/reader_test.go @@ -0,0 +1,125 @@ +package parquet + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +func TestParquetWriteThenRead(t *testing.T) { + t.Run("read-write-couple-rows", func(t *testing.T) { + file, err := os.CreateTemp(t.TempDir(), "temp-*.parquet") + require.NoError(t, err) + defer func() { _ = os.Remove(file.Name()) }() + + writer, err := NewParquetWriter(file) + require.NoError(t, err) + ctx := context.Background() + + require.NoError(t, writer.Write(toKeyAndBytes(ctx, "ggg", "rrr", &unstructured.Unstructured{ + Object: map[string]any{ + "metadata": map[string]any{ + "namespace": "ns", + "name": "aaa", + "resourceVersion": "1234", + "annotations": map[string]string{ + utils.AnnoKeyFolder: "xyz", + }, + }, + "spec": map[string]any{ + "hello": "first", + }, + }, + }))) + + require.NoError(t, writer.Write(toKeyAndBytes(ctx, "ggg", "rrr", &unstructured.Unstructured{ + Object: map[string]any{ + "metadata": map[string]any{ + "namespace": "ns", + "name": "bbb", + "resourceVersion": "5678", + "generation": -999, // deleted action + }, + "spec": map[string]any{ + "hello": "second", + }, + }, + }))) + + require.NoError(t, writer.Write(toKeyAndBytes(ctx, "ggg", "rrr", &unstructured.Unstructured{ + Object: map[string]any{ + "metadata": map[string]any{ + "namespace": "ns", + "name": "ccc", + "resourceVersion": "789", + "generation": 3, // modified action + }, + "spec": map[string]any{ + "hello": "thirt", + }, + }, + }))) + + res, err := writer.CloseWithResults() + require.NoError(t, err) + require.Equal(t, int64(3), res.Processed) + + var keys []string + reader, err := newResourceReader(file.Name(), 20) + require.NoError(t, err) + for reader.Next() { + req := reader.Request() + keys = append(keys, req.Key.SearchID()) + } + + // Verify that we read all values + require.Equal(t, []string{ + "rrr/ns/ggg/aaa", + "rrr/ns/ggg/bbb", + "rrr/ns/ggg/ccc", + }, keys) + }) + + t.Run("read-write-empty-db", func(t *testing.T) { + file, err := os.CreateTemp(t.TempDir(), "temp-*.parquet") + require.NoError(t, err) + defer func() { _ = os.Remove(file.Name()) }() + + writer, err := NewParquetWriter(file) + require.NoError(t, err) + err = writer.Close() + require.NoError(t, err) + + var keys []string + reader, err := newResourceReader(file.Name(), 20) + require.NoError(t, err) + for reader.Next() { + req := reader.Request() + keys = append(keys, req.Key.SearchID()) + } + require.NoError(t, reader.err) + require.Empty(t, keys) + }) +} + +func toKeyAndBytes(ctx context.Context, group string, res string, obj *unstructured.Unstructured) (context.Context, *resource.ResourceKey, []byte) { + if obj.GetKind() == "" { + obj.SetKind(res) + } + if obj.GetAPIVersion() == "" { + obj.SetAPIVersion(group + "/vXyz") + } + data, _ := obj.MarshalJSON() + return ctx, &resource.ResourceKey{ + Namespace: obj.GetNamespace(), + Resource: res, + Group: group, + Name: obj.GetName(), + }, data +} diff --git a/pkg/storage/unified/parquet/writer.go b/pkg/storage/unified/parquet/writer.go new file mode 100644 index 00000000000..421946b71a7 --- /dev/null +++ b/pkg/storage/unified/parquet/writer.go @@ -0,0 +1,209 @@ +package parquet + +import ( + "context" + "io" + + "github.com/apache/arrow-go/v18/arrow" + "github.com/apache/arrow-go/v18/arrow/array" + "github.com/apache/arrow-go/v18/arrow/memory" + "github.com/apache/arrow-go/v18/parquet" + "github.com/apache/arrow-go/v18/parquet/compress" + "github.com/apache/arrow-go/v18/parquet/pqarrow" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +var ( + _ resource.BatchResourceWriter = (*parquetWriter)(nil) +) + +// Write resources into a parquet file +func NewParquetWriter(f io.Writer) (*parquetWriter, error) { + w := &parquetWriter{ + pool: memory.DefaultAllocator, + schema: newSchema(nil), + buffer: 1024 * 10 * 100 * 10, // 10MB + logger: logging.DefaultLogger.With("logger", "parquet.writer"), + rsp: &resource.BatchResponse{}, + summary: make(map[string]*resource.BatchResponse_Summary), + } + + props := parquet.NewWriterProperties( + parquet.WithCompression(compress.Codecs.Brotli), + ) + writer, err := pqarrow.NewFileWriter(w.schema, f, props, pqarrow.DefaultWriterProps()) + if err != nil { + return nil, err + } + w.writer = writer + return w, w.init() +} + +// ProcessBatch implements resource.BatchProcessingBackend. +func (w *parquetWriter) ProcessBatch(ctx context.Context, setting resource.BatchSettings, iter resource.BatchRequestIterator) *resource.BatchResponse { + defer func() { _ = w.Close() }() + + var err error + for iter.Next() { + if iter.RollbackRequested() { + break + } + + req := iter.Request() + + err = w.Write(ctx, req.Key, req.Value) + if err != nil { + break + } + } + + rsp, err := w.CloseWithResults() + if err != nil { + w.logger.Warn("error closing parquet file", "err", err) + } + if rsp == nil { + rsp = &resource.BatchResponse{} + } + if err != nil { + rsp.Error = resource.AsErrorResult(err) + } + return rsp +} + +type parquetWriter struct { + pool memory.Allocator + buffer int + wrote int + + schema *arrow.Schema + writer *pqarrow.FileWriter + logger logging.Logger + + rv *array.Int64Builder + namespace *array.StringBuilder + group *array.StringBuilder + resource *array.StringBuilder + name *array.StringBuilder + folder *array.StringBuilder + action *array.Int8Builder + value *array.StringBuilder + + rsp *resource.BatchResponse + summary map[string]*resource.BatchResponse_Summary +} + +func (w *parquetWriter) CloseWithResults() (*resource.BatchResponse, error) { + err := w.Close() + return w.rsp, err +} + +func (w *parquetWriter) Close() error { + if w.rv.Len() > 0 { + _ = w.flush() + } + w.logger.Info("close") + return w.writer.Close() +} + +// writes the current buffer to parquet and re-inits the arrow buffer +func (w *parquetWriter) flush() error { + w.logger.Info("flush", "count", w.rv.Len()) + rec := array.NewRecord(w.schema, []arrow.Array{ + w.rv.NewArray(), + w.namespace.NewArray(), + w.group.NewArray(), + w.resource.NewArray(), + w.name.NewArray(), + w.folder.NewArray(), + w.action.NewArray(), + w.value.NewArray(), + }, int64(w.rv.Len())) + defer rec.Release() + err := w.writer.Write(rec) + if err != nil { + return err + } + return w.init() +} + +func (w *parquetWriter) init() error { + w.rv = array.NewInt64Builder(w.pool) + w.namespace = array.NewStringBuilder(w.pool) + w.group = array.NewStringBuilder(w.pool) + w.resource = array.NewStringBuilder(w.pool) + w.name = array.NewStringBuilder(w.pool) + w.folder = array.NewStringBuilder(w.pool) + w.action = array.NewInt8Builder(w.pool) + w.value = array.NewStringBuilder(w.pool) + w.wrote = 0 + return nil +} + +func (w *parquetWriter) Write(ctx context.Context, key *resource.ResourceKey, value []byte) error { + w.rsp.Processed++ + obj := &unstructured.Unstructured{} + err := obj.UnmarshalJSON(value) + if err != nil { + return err + } + meta, err := utils.MetaAccessor(obj) + if err != nil { + return err + } + rv, _ := meta.GetResourceVersionInt64() // it can be empty + + w.rv.Append(rv) + w.namespace.Append(key.Namespace) + w.group.Append(key.Group) + w.resource.Append(key.Resource) + w.name.Append(key.Name) + w.folder.Append(meta.GetFolder()) + w.value.Append(string(value)) + + var action resource.WatchEvent_Type + switch meta.GetGeneration() { + case 0, 1: + action = resource.WatchEvent_ADDED + case utils.DeletedGeneration: + action = resource.WatchEvent_DELETED + default: + action = resource.WatchEvent_MODIFIED + } + w.action.Append(int8(action)) + + w.wrote = w.wrote + len(value) + if w.wrote > w.buffer { + w.logger.Info("buffer full", "buffer", w.wrote, "max", w.buffer) + return w.flush() + } + + summary := w.summary[key.BatchID()] + if summary == nil { + summary = &resource.BatchResponse_Summary{ + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + } + w.summary[key.BatchID()] = summary + w.rsp.Summary = append(w.rsp.Summary, summary) + } + summary.Count++ + return nil +} + +func newSchema(metadata *arrow.Metadata) *arrow.Schema { + return arrow.NewSchema([]arrow.Field{ + {Name: "resource_version", Type: &arrow.Int64Type{}, Nullable: false}, + {Name: "group", Type: &arrow.StringType{}, Nullable: false}, + {Name: "resource", Type: &arrow.StringType{}, Nullable: false}, + {Name: "namespace", Type: &arrow.StringType{}, Nullable: false}, + {Name: "name", Type: &arrow.StringType{}, Nullable: false}, + {Name: "folder", Type: &arrow.StringType{}, Nullable: false}, + {Name: "action", Type: &arrow.Int8Type{}, Nullable: false}, // 1,2,3 + {Name: "value", Type: &arrow.StringType{}, Nullable: false}, + }, metadata) +} diff --git a/pkg/storage/unified/resource/batch.go b/pkg/storage/unified/resource/batch.go new file mode 100644 index 00000000000..4997706a5e1 --- /dev/null +++ b/pkg/storage/unified/resource/batch.go @@ -0,0 +1,297 @@ +package resource + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + + "google.golang.org/grpc/metadata" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +const grpcMetaKeyCollection = "x-gf-batch-collection" +const grpcMetaKeyRebuildCollection = "x-gf-batch-rebuild-collection" +const grpcMetaKeySkipValidation = "x-gf-batch-skip-validation" + +func grpcMetaValueIsTrue(vals []string) bool { + return len(vals) == 1 && vals[0] == "true" +} + +type BatchRequestIterator interface { + Next() bool + + // The next event we should process + Request() *BatchRequest + + // Rollback requested + RollbackRequested() bool +} + +type BatchProcessingBackend interface { + ProcessBatch(ctx context.Context, setting BatchSettings, iter BatchRequestIterator) *BatchResponse +} + +type BatchResourceWriter interface { + io.Closer + + Write(ctx context.Context, key *ResourceKey, value []byte) error + + // Called when finished writing + CloseWithResults() (*BatchResponse, error) +} + +type BatchSettings struct { + // All requests will be within this namespace/group/resource + Collection []*ResourceKey + + // The batch will include everything from the collection + // - all existing values will be removed/replaced if the batch completes successfully + RebuildCollection bool + + // The byte[] payload and folder has already been validated - no need to decode and verify + SkipValidation bool +} + +func (x *BatchSettings) ToMD() metadata.MD { + md := make(metadata.MD) + if len(x.Collection) > 0 { + for _, v := range x.Collection { + md[grpcMetaKeyCollection] = append(md[grpcMetaKeyCollection], v.SearchID()) + } + } + if x.RebuildCollection { + md[grpcMetaKeyRebuildCollection] = []string{"true"} + } + if x.SkipValidation { + md[grpcMetaKeySkipValidation] = []string{"true"} + } + return md +} + +func NewBatchSettings(md metadata.MD) (BatchSettings, error) { + settings := BatchSettings{} + for k, v := range md { + switch k { + case grpcMetaKeyCollection: + for _, c := range v { + key := &ResourceKey{} + err := key.ReadSearchID(c) + if err != nil { + return settings, fmt.Errorf("error reading collection metadata: %s / %w", c, err) + } + settings.Collection = append(settings.Collection, key) + } + case grpcMetaKeyRebuildCollection: + settings.RebuildCollection = grpcMetaValueIsTrue(v) + case grpcMetaKeySkipValidation: + settings.SkipValidation = grpcMetaValueIsTrue(v) + } + } + return settings, nil +} + +// BatchWrite implements ResourceServer. +// All requests must be to the same NAMESPACE/GROUP/RESOURCE +func (s *server) BatchProcess(stream BatchStore_BatchProcessServer) error { + ctx := stream.Context() + user, ok := authlib.AuthInfoFrom(ctx) + if !ok || user == nil { + return stream.SendAndClose(&BatchResponse{ + Error: &ErrorResult{ + Message: "no user found in context", + Code: http.StatusUnauthorized, + }, + }) + } + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return stream.SendAndClose(&BatchResponse{ + Error: &ErrorResult{ + Message: "unable to read metadata gRPC request", + Code: http.StatusPreconditionFailed, + }, + }) + } + runner := &batchRunner{ + checker: make(map[string]authlib.ItemChecker), // Can create + stream: stream, + } + settings, err := NewBatchSettings(md) + if err != nil { + return stream.SendAndClose(&BatchResponse{ + Error: &ErrorResult{ + Message: "error reading settings", + Reason: err.Error(), + Code: http.StatusPreconditionFailed, + }, + }) + } + + if len(settings.Collection) < 1 { + return stream.SendAndClose(&BatchResponse{ + Error: &ErrorResult{ + Message: "Missing target collection(s) in request header", + Code: http.StatusBadRequest, + }, + }) + } + + // HACK!!! always allow everything!!!!!! + access := authlib.FixedAccessClient(true) + + if settings.RebuildCollection { + for _, k := range settings.Collection { + // Can we delete the whole collection + rsp, err := access.Check(ctx, user, authlib.CheckRequest{ + Namespace: k.Namespace, + Group: k.Group, + Resource: k.Resource, + Verb: utils.VerbDeleteCollection, + }) + if err != nil || !rsp.Allowed { + return stream.SendAndClose(&BatchResponse{ + Error: &ErrorResult{ + Message: fmt.Sprintf("Requester must be able to: %s", utils.VerbDeleteCollection), + Code: http.StatusForbidden, + }, + }) + } + + // This will be called for each request -- with the folder ID + runner.checker[k.BatchID()], err = access.Compile(ctx, user, authlib.ListRequest{ + Namespace: k.Namespace, + Group: k.Group, + Resource: k.Resource, + Verb: utils.VerbCreate, + }) + if err != nil { + return stream.SendAndClose(&BatchResponse{ + Error: &ErrorResult{ + Message: "Unable to check `create` permission", + Code: http.StatusForbidden, + }, + }) + } + } + } else { + return stream.SendAndClose(&BatchResponse{ + Error: &ErrorResult{ + Message: "Batch currently only supports RebuildCollection", + Code: http.StatusBadRequest, + }, + }) + } + + backend, ok := s.backend.(BatchProcessingBackend) + if !ok { + return stream.SendAndClose(&BatchResponse{ + Error: &ErrorResult{ + Message: "The server backend does not support batch processing", + Code: http.StatusNotImplemented, + }, + }) + } + + // BatchProcess requests + rsp := backend.ProcessBatch(ctx, settings, runner) + if rsp == nil { + rsp = &BatchResponse{ + Error: &ErrorResult{ + Code: http.StatusInternalServerError, + Message: "Nothing returned from process batch", + }, + } + } + if runner.err != nil { + rsp.Error = AsErrorResult(runner.err) + } + + if rsp.Error == nil && s.search != nil { + // Rebuild any changed indexes + for _, summary := range rsp.Summary { + _, _, err := s.search.build(ctx, NamespacedResource{ + Namespace: summary.Namespace, + Group: summary.Group, + Resource: summary.Resource, + }, summary.Count, summary.ResourceVersion) + if err != nil { + s.log.Warn("error building search index after batch load", "err", err) + rsp.Error = &ErrorResult{ + Code: http.StatusInternalServerError, + Message: "err building search index: " + summary.Resource, + Reason: err.Error(), + } + } + } + } + return stream.SendAndClose(rsp) +} + +var ( + _ BatchRequestIterator = (*batchRunner)(nil) +) + +type batchRunner struct { + stream BatchStore_BatchProcessServer + rollback bool + request *BatchRequest + err error + checker map[string]authlib.ItemChecker +} + +// Next implements BatchRequestIterator. +func (b *batchRunner) Next() bool { + if b.rollback { + return true + } + + b.request, b.err = b.stream.Recv() + if errors.Is(b.err, io.EOF) { + b.err = nil + b.rollback = false + b.request = nil + return false + } + + if b.err != nil { + b.rollback = true + return true + } + + if b.request != nil { + key := b.request.Key + k := key.BatchID() + checker, ok := b.checker[k] + 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) { + b.err = fmt.Errorf("not allowed to create resource") + b.rollback = true + } + return true + } + return false +} + +// Request implements BatchRequestIterator. +func (b *batchRunner) Request() *BatchRequest { + if b.rollback { + return nil + } + return b.request +} + +// RollbackRequested implements BatchRequestIterator. +func (b *batchRunner) RollbackRequested() bool { + if b.rollback { + b.rollback = false // break iterator + return true + } + return false +} diff --git a/pkg/storage/unified/resource/client.go b/pkg/storage/unified/resource/client.go index bd67e68d428..a345bdd3376 100644 --- a/pkg/storage/unified/resource/client.go +++ b/pkg/storage/unified/resource/client.go @@ -25,6 +25,7 @@ type ResourceClient interface { ResourceStoreClient ResourceIndexClient RepositoryIndexClient + BatchStoreClient BlobStoreClient DiagnosticsClient } @@ -34,6 +35,7 @@ type resourceClient struct { ResourceStoreClient ResourceIndexClient RepositoryIndexClient + BatchStoreClient BlobStoreClient DiagnosticsClient } @@ -44,6 +46,7 @@ func NewLegacyResourceClient(channel *grpc.ClientConn) ResourceClient { ResourceStoreClient: NewResourceStoreClient(cc), ResourceIndexClient: NewResourceIndexClient(cc), RepositoryIndexClient: NewRepositoryIndexClient(cc), + BatchStoreClient: NewBatchStoreClient(cc), BlobStoreClient: NewBlobStoreClient(cc), DiagnosticsClient: NewDiagnosticsClient(cc), } @@ -59,6 +62,7 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient { &ResourceIndex_ServiceDesc, &RepositoryIndex_ServiceDesc, &BlobStore_ServiceDesc, + &BatchStore_ServiceDesc, &Diagnostics_ServiceDesc, } { channel.RegisterService( @@ -82,6 +86,7 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient { ResourceStoreClient: NewResourceStoreClient(cc), ResourceIndexClient: NewResourceIndexClient(cc), RepositoryIndexClient: NewRepositoryIndexClient(cc), + BatchStoreClient: NewBatchStoreClient(cc), BlobStoreClient: NewBlobStoreClient(cc), DiagnosticsClient: NewDiagnosticsClient(cc), } @@ -101,10 +106,12 @@ func NewGRPCResourceClient(tracer tracing.Tracer, conn *grpc.ClientConn) (Resour cc := grpchan.InterceptClientConn(conn, clientInt.UnaryClientInterceptor, clientInt.StreamClientInterceptor) return &resourceClient{ - ResourceStoreClient: NewResourceStoreClient(cc), - ResourceIndexClient: NewResourceIndexClient(cc), - BlobStoreClient: NewBlobStoreClient(cc), - DiagnosticsClient: NewDiagnosticsClient(cc), + ResourceStoreClient: NewResourceStoreClient(cc), + ResourceIndexClient: NewResourceIndexClient(cc), + BlobStoreClient: NewBlobStoreClient(cc), + BatchStoreClient: NewBatchStoreClient(cc), + RepositoryIndexClient: NewRepositoryIndexClient(cc), + DiagnosticsClient: NewDiagnosticsClient(cc), }, nil } @@ -126,10 +133,12 @@ func NewCloudResourceClient(tracer tracing.Tracer, conn *grpc.ClientConn, cfg au cc := grpchan.InterceptClientConn(conn, clientInt.UnaryClientInterceptor, clientInt.StreamClientInterceptor) return &resourceClient{ - ResourceStoreClient: NewResourceStoreClient(cc), - ResourceIndexClient: NewResourceIndexClient(cc), - BlobStoreClient: NewBlobStoreClient(cc), - DiagnosticsClient: NewDiagnosticsClient(cc), + ResourceStoreClient: NewResourceStoreClient(cc), + ResourceIndexClient: NewResourceIndexClient(cc), + BlobStoreClient: NewBlobStoreClient(cc), + BatchStoreClient: NewBatchStoreClient(cc), + RepositoryIndexClient: NewRepositoryIndexClient(cc), + DiagnosticsClient: NewDiagnosticsClient(cc), }, nil } diff --git a/pkg/storage/unified/resource/keys.go b/pkg/storage/unified/resource/keys.go index 7e0c7e049ab..d323e107ea1 100644 --- a/pkg/storage/unified/resource/keys.go +++ b/pkg/storage/unified/resource/keys.go @@ -48,24 +48,43 @@ func (x *ResourceKey) SearchID() string { sb.WriteString(x.Group) sb.WriteString("/") sb.WriteString(x.Resource) - sb.WriteString("/") - sb.WriteString(x.Name) + if x.Name != "" { + sb.WriteString("/") + sb.WriteString(x.Name) + } return sb.String() } func (x *ResourceKey) ReadSearchID(v string) error { parts := strings.Split(v, "/") - if len(parts) != 4 { + if len(parts) < 3 { return fmt.Errorf("invalid search id (expecting 3 slashes)") } x.Namespace = parts[0] x.Group = parts[1] x.Resource = parts[2] - x.Name = parts[3] + if len(parts) > 3 { + x.Name = parts[3] + } if x.Namespace == clusterNamespace { x.Namespace = "" } return nil } + +// The namespace/group/resource +func (x *ResourceKey) BatchID() string { + var sb strings.Builder + if x.Namespace == "" { + sb.WriteString(clusterNamespace) + } else { + sb.WriteString(x.Namespace) + } + sb.WriteString("/") + sb.WriteString(x.Group) + sb.WriteString("/") + sb.WriteString(x.Resource) + return sb.String() +} diff --git a/pkg/storage/unified/resource/keys_test.go b/pkg/storage/unified/resource/keys_test.go index 0e9de7325f2..96d4da3555b 100644 --- a/pkg/storage/unified/resource/keys_test.go +++ b/pkg/storage/unified/resource/keys_test.go @@ -33,13 +33,20 @@ func TestSearchIDKeys(t *testing.T) { Resource: "resource", Name: "name", }}, - {input: "/group/resource/", // missing name + {input: "/group/resource/", expected: &ResourceKey{ Namespace: "", Group: "group", Resource: "resource", Name: "", }}, + {input: "default/group/resource", + expected: &ResourceKey{ + Namespace: "default", + Group: "group", + Resource: "resource", + Name: "", + }}, {input: "**cluster**/group/resource/aaa", // cluster namespace expected: &ResourceKey{ Namespace: "", diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index 0e72212de6f..ee8801f361b 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.1 +// protoc-gen-go v1.36.4 // protoc (unknown) // source: resource.proto @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -173,6 +174,60 @@ func (WatchEvent_Type) EnumDescriptor() ([]byte, []int) { return file_resource_proto_rawDescGZIP(), []int{18, 0} } +type BatchRequest_Action int32 + +const ( + // will be an error + BatchRequest_UNKNOWN BatchRequest_Action = 0 + // Matches Watch event enum + BatchRequest_ADDED BatchRequest_Action = 1 + BatchRequest_MODIFIED BatchRequest_Action = 2 + BatchRequest_DELETED BatchRequest_Action = 3 +) + +// Enum value maps for BatchRequest_Action. +var ( + BatchRequest_Action_name = map[int32]string{ + 0: "UNKNOWN", + 1: "ADDED", + 2: "MODIFIED", + 3: "DELETED", + } + BatchRequest_Action_value = map[string]int32{ + "UNKNOWN": 0, + "ADDED": 1, + "MODIFIED": 2, + "DELETED": 3, + } +) + +func (x BatchRequest_Action) Enum() *BatchRequest_Action { + p := new(BatchRequest_Action) + *p = x + return p +} + +func (x BatchRequest_Action) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (BatchRequest_Action) Descriptor() protoreflect.EnumDescriptor { + return file_resource_proto_enumTypes[3].Descriptor() +} + +func (BatchRequest_Action) Type() protoreflect.EnumType { + return &file_resource_proto_enumTypes[3] +} + +func (x BatchRequest_Action) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use BatchRequest_Action.Descriptor instead. +func (BatchRequest_Action) EnumDescriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{19, 0} +} + type HealthCheckResponse_ServingStatus int32 const ( @@ -209,11 +264,11 @@ func (x HealthCheckResponse_ServingStatus) String() string { } func (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor { - return file_resource_proto_enumTypes[3].Descriptor() + return file_resource_proto_enumTypes[4].Descriptor() } func (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType { - return &file_resource_proto_enumTypes[3] + return &file_resource_proto_enumTypes[4] } func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber { @@ -222,7 +277,7 @@ func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber { // Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead. func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{28, 0} + return file_resource_proto_rawDescGZIP(), []int{30, 0} } // See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more. @@ -284,11 +339,11 @@ func (x ResourceTableColumnDefinition_ColumnType) String() string { } func (ResourceTableColumnDefinition_ColumnType) Descriptor() protoreflect.EnumDescriptor { - return file_resource_proto_enumTypes[4].Descriptor() + return file_resource_proto_enumTypes[5].Descriptor() } func (ResourceTableColumnDefinition_ColumnType) Type() protoreflect.EnumType { - return &file_resource_proto_enumTypes[4] + return &file_resource_proto_enumTypes[5] } func (x ResourceTableColumnDefinition_ColumnType) Number() protoreflect.EnumNumber { @@ -297,7 +352,7 @@ func (x ResourceTableColumnDefinition_ColumnType) Number() protoreflect.EnumNumb // Deprecated: Use ResourceTableColumnDefinition_ColumnType.Descriptor instead. func (ResourceTableColumnDefinition_ColumnType) EnumDescriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{30, 0} + return file_resource_proto_rawDescGZIP(), []int{32, 0} } type PutBlobRequest_Method int32 @@ -332,11 +387,11 @@ func (x PutBlobRequest_Method) String() string { } func (PutBlobRequest_Method) Descriptor() protoreflect.EnumDescriptor { - return file_resource_proto_enumTypes[5].Descriptor() + return file_resource_proto_enumTypes[6].Descriptor() } func (PutBlobRequest_Method) Type() protoreflect.EnumType { - return &file_resource_proto_enumTypes[5] + return &file_resource_proto_enumTypes[6] } func (x PutBlobRequest_Method) Number() protoreflect.EnumNumber { @@ -345,7 +400,7 @@ func (x PutBlobRequest_Method) Number() protoreflect.EnumNumber { // Deprecated: Use PutBlobRequest_Method.Descriptor instead. func (PutBlobRequest_Method) EnumDescriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{34, 0} + return file_resource_proto_rawDescGZIP(), []int{36, 0} } type ResourceKey struct { @@ -1602,7 +1657,7 @@ type WatchEvent struct { state protoimpl.MessageState `protogen:"open.v1"` // Timestamp the event was sent Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - // Timestamp the event was sent + // The event type Type WatchEvent_Type `protobuf:"varint,2,opt,name=type,proto3,enum=resource.WatchEvent_Type" json:"type,omitempty"` // Resource version for the object Resource *WatchEvent_Resource `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"` @@ -1670,6 +1725,150 @@ func (x *WatchEvent) GetPrevious() *WatchEvent_Resource { return nil } +type BatchRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // NOTE everything in the same stream must share the same Namespace/Group/Resource + Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // Requested action + Action BatchRequest_Action `protobuf:"varint,2,opt,name=action,proto3,enum=resource.BatchRequest_Action" json:"action,omitempty"` + // The resource value + Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` + // Hint that a new version will be written on-top of this + Folder string `protobuf:"bytes,4,opt,name=folder,proto3" json:"folder,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BatchRequest) Reset() { + *x = BatchRequest{} + mi := &file_resource_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BatchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchRequest) ProtoMessage() {} + +func (x *BatchRequest) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchRequest.ProtoReflect.Descriptor instead. +func (*BatchRequest) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{19} +} + +func (x *BatchRequest) GetKey() *ResourceKey { + if x != nil { + return x.Key + } + return nil +} + +func (x *BatchRequest) GetAction() BatchRequest_Action { + if x != nil { + return x.Action + } + return BatchRequest_UNKNOWN +} + +func (x *BatchRequest) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +func (x *BatchRequest) GetFolder() string { + if x != nil { + return x.Folder + } + return "" +} + +type BatchResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Error details + Error *ErrorResult `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + // Total events processed + Processed int64 `protobuf:"varint,2,opt,name=processed,proto3" json:"processed,omitempty"` + // Summary status for the processed values + Summary []*BatchResponse_Summary `protobuf:"bytes,3,rep,name=summary,proto3" json:"summary,omitempty"` + // Rejected + Rejected []*BatchResponse_Rejected `protobuf:"bytes,4,rep,name=rejected,proto3" json:"rejected,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BatchResponse) Reset() { + *x = BatchResponse{} + mi := &file_resource_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BatchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchResponse) ProtoMessage() {} + +func (x *BatchResponse) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchResponse.ProtoReflect.Descriptor instead. +func (*BatchResponse) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{20} +} + +func (x *BatchResponse) GetError() *ErrorResult { + if x != nil { + return x.Error + } + return nil +} + +func (x *BatchResponse) GetProcessed() int64 { + if x != nil { + return x.Processed + } + return 0 +} + +func (x *BatchResponse) GetSummary() []*BatchResponse_Summary { + if x != nil { + return x.Summary + } + return nil +} + +func (x *BatchResponse) GetRejected() []*BatchResponse_Rejected { + if x != nil { + return x.Rejected + } + return nil +} + // Get statistics across multiple resources // For these queries, we do not need authorization to see the actual values type ResourceStatsRequest struct { @@ -1688,7 +1887,7 @@ type ResourceStatsRequest struct { func (x *ResourceStatsRequest) Reset() { *x = ResourceStatsRequest{} - mi := &file_resource_proto_msgTypes[19] + mi := &file_resource_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1700,7 +1899,7 @@ func (x *ResourceStatsRequest) String() string { func (*ResourceStatsRequest) ProtoMessage() {} func (x *ResourceStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[19] + mi := &file_resource_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1713,7 +1912,7 @@ func (x *ResourceStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceStatsRequest.ProtoReflect.Descriptor instead. func (*ResourceStatsRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{19} + return file_resource_proto_rawDescGZIP(), []int{21} } func (x *ResourceStatsRequest) GetNamespace() string { @@ -1749,7 +1948,7 @@ type ResourceStatsResponse struct { func (x *ResourceStatsResponse) Reset() { *x = ResourceStatsResponse{} - mi := &file_resource_proto_msgTypes[20] + mi := &file_resource_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1761,7 +1960,7 @@ func (x *ResourceStatsResponse) String() string { func (*ResourceStatsResponse) ProtoMessage() {} func (x *ResourceStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[20] + mi := &file_resource_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1774,7 +1973,7 @@ func (x *ResourceStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceStatsResponse.ProtoReflect.Descriptor instead. func (*ResourceStatsResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{20} + return file_resource_proto_rawDescGZIP(), []int{22} } func (x *ResourceStatsResponse) GetError() *ErrorResult { @@ -1823,7 +2022,7 @@ type ResourceSearchRequest struct { func (x *ResourceSearchRequest) Reset() { *x = ResourceSearchRequest{} - mi := &file_resource_proto_msgTypes[21] + mi := &file_resource_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1835,7 +2034,7 @@ func (x *ResourceSearchRequest) String() string { func (*ResourceSearchRequest) ProtoMessage() {} func (x *ResourceSearchRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[21] + mi := &file_resource_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1848,7 +2047,7 @@ func (x *ResourceSearchRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceSearchRequest.ProtoReflect.Descriptor instead. func (*ResourceSearchRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{21} + return file_resource_proto_rawDescGZIP(), []int{23} } func (x *ResourceSearchRequest) GetOptions() *ListOptions { @@ -1950,7 +2149,7 @@ type ResourceSearchResponse struct { func (x *ResourceSearchResponse) Reset() { *x = ResourceSearchResponse{} - mi := &file_resource_proto_msgTypes[22] + mi := &file_resource_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1962,7 +2161,7 @@ func (x *ResourceSearchResponse) String() string { func (*ResourceSearchResponse) ProtoMessage() {} func (x *ResourceSearchResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[22] + mi := &file_resource_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1975,7 +2174,7 @@ func (x *ResourceSearchResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceSearchResponse.ProtoReflect.Descriptor instead. func (*ResourceSearchResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{22} + return file_resource_proto_rawDescGZIP(), []int{24} } func (x *ResourceSearchResponse) GetError() *ErrorResult { @@ -2043,7 +2242,7 @@ type ListRepositoryObjectsRequest struct { func (x *ListRepositoryObjectsRequest) Reset() { *x = ListRepositoryObjectsRequest{} - mi := &file_resource_proto_msgTypes[23] + mi := &file_resource_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2055,7 +2254,7 @@ func (x *ListRepositoryObjectsRequest) String() string { func (*ListRepositoryObjectsRequest) ProtoMessage() {} func (x *ListRepositoryObjectsRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[23] + mi := &file_resource_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2068,7 +2267,7 @@ func (x *ListRepositoryObjectsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRepositoryObjectsRequest.ProtoReflect.Descriptor instead. func (*ListRepositoryObjectsRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{23} + return file_resource_proto_rawDescGZIP(), []int{25} } func (x *ListRepositoryObjectsRequest) GetNextPageToken() string { @@ -2106,7 +2305,7 @@ type ListRepositoryObjectsResponse struct { func (x *ListRepositoryObjectsResponse) Reset() { *x = ListRepositoryObjectsResponse{} - mi := &file_resource_proto_msgTypes[24] + mi := &file_resource_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2118,7 +2317,7 @@ func (x *ListRepositoryObjectsResponse) String() string { func (*ListRepositoryObjectsResponse) ProtoMessage() {} func (x *ListRepositoryObjectsResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[24] + mi := &file_resource_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2131,7 +2330,7 @@ func (x *ListRepositoryObjectsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRepositoryObjectsResponse.ProtoReflect.Descriptor instead. func (*ListRepositoryObjectsResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{24} + return file_resource_proto_rawDescGZIP(), []int{26} } func (x *ListRepositoryObjectsResponse) GetItems() []*ListRepositoryObjectsResponse_Item { @@ -2169,7 +2368,7 @@ type CountRepositoryObjectsRequest struct { func (x *CountRepositoryObjectsRequest) Reset() { *x = CountRepositoryObjectsRequest{} - mi := &file_resource_proto_msgTypes[25] + mi := &file_resource_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2181,7 +2380,7 @@ func (x *CountRepositoryObjectsRequest) String() string { func (*CountRepositoryObjectsRequest) ProtoMessage() {} func (x *CountRepositoryObjectsRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[25] + mi := &file_resource_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2194,7 +2393,7 @@ func (x *CountRepositoryObjectsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CountRepositoryObjectsRequest.ProtoReflect.Descriptor instead. func (*CountRepositoryObjectsRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{25} + return file_resource_proto_rawDescGZIP(), []int{27} } func (x *CountRepositoryObjectsRequest) GetNamespace() string { @@ -2224,7 +2423,7 @@ type CountRepositoryObjectsResponse struct { func (x *CountRepositoryObjectsResponse) Reset() { *x = CountRepositoryObjectsResponse{} - mi := &file_resource_proto_msgTypes[26] + mi := &file_resource_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2236,7 +2435,7 @@ func (x *CountRepositoryObjectsResponse) String() string { func (*CountRepositoryObjectsResponse) ProtoMessage() {} func (x *CountRepositoryObjectsResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[26] + mi := &file_resource_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2249,7 +2448,7 @@ func (x *CountRepositoryObjectsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CountRepositoryObjectsResponse.ProtoReflect.Descriptor instead. func (*CountRepositoryObjectsResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{26} + return file_resource_proto_rawDescGZIP(), []int{28} } func (x *CountRepositoryObjectsResponse) GetItems() []*CountRepositoryObjectsResponse_ResourceCount { @@ -2275,7 +2474,7 @@ type HealthCheckRequest struct { func (x *HealthCheckRequest) Reset() { *x = HealthCheckRequest{} - mi := &file_resource_proto_msgTypes[27] + mi := &file_resource_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2287,7 +2486,7 @@ func (x *HealthCheckRequest) String() string { func (*HealthCheckRequest) ProtoMessage() {} func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[27] + mi := &file_resource_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2300,7 +2499,7 @@ func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead. func (*HealthCheckRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{27} + return file_resource_proto_rawDescGZIP(), []int{29} } func (x *HealthCheckRequest) GetService() string { @@ -2319,7 +2518,7 @@ type HealthCheckResponse struct { func (x *HealthCheckResponse) Reset() { *x = HealthCheckResponse{} - mi := &file_resource_proto_msgTypes[28] + mi := &file_resource_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2331,7 +2530,7 @@ func (x *HealthCheckResponse) String() string { func (*HealthCheckResponse) ProtoMessage() {} func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[28] + mi := &file_resource_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2344,7 +2543,7 @@ func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead. func (*HealthCheckResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{28} + return file_resource_proto_rawDescGZIP(), []int{30} } func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus { @@ -2385,7 +2584,7 @@ type ResourceTable struct { func (x *ResourceTable) Reset() { *x = ResourceTable{} - mi := &file_resource_proto_msgTypes[29] + mi := &file_resource_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2397,7 +2596,7 @@ func (x *ResourceTable) String() string { func (*ResourceTable) ProtoMessage() {} func (x *ResourceTable) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[29] + mi := &file_resource_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2410,7 +2609,7 @@ func (x *ResourceTable) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceTable.ProtoReflect.Descriptor instead. func (*ResourceTable) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{29} + return file_resource_proto_rawDescGZIP(), []int{31} } func (x *ResourceTable) GetColumns() []*ResourceTableColumnDefinition { @@ -2471,7 +2670,7 @@ type ResourceTableColumnDefinition struct { func (x *ResourceTableColumnDefinition) Reset() { *x = ResourceTableColumnDefinition{} - mi := &file_resource_proto_msgTypes[30] + mi := &file_resource_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2483,7 +2682,7 @@ func (x *ResourceTableColumnDefinition) String() string { func (*ResourceTableColumnDefinition) ProtoMessage() {} func (x *ResourceTableColumnDefinition) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[30] + mi := &file_resource_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2496,7 +2695,7 @@ func (x *ResourceTableColumnDefinition) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceTableColumnDefinition.ProtoReflect.Descriptor instead. func (*ResourceTableColumnDefinition) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{30} + return file_resource_proto_rawDescGZIP(), []int{32} } func (x *ResourceTableColumnDefinition) GetName() string { @@ -2564,7 +2763,7 @@ type ResourceTableRow struct { func (x *ResourceTableRow) Reset() { *x = ResourceTableRow{} - mi := &file_resource_proto_msgTypes[31] + mi := &file_resource_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2576,7 +2775,7 @@ func (x *ResourceTableRow) String() string { func (*ResourceTableRow) ProtoMessage() {} func (x *ResourceTableRow) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[31] + mi := &file_resource_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2589,7 +2788,7 @@ func (x *ResourceTableRow) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceTableRow.ProtoReflect.Descriptor instead. func (*ResourceTableRow) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{31} + return file_resource_proto_rawDescGZIP(), []int{33} } func (x *ResourceTableRow) GetKey() *ResourceKey { @@ -2632,7 +2831,7 @@ type RestoreRequest struct { func (x *RestoreRequest) Reset() { *x = RestoreRequest{} - mi := &file_resource_proto_msgTypes[32] + mi := &file_resource_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2644,7 +2843,7 @@ func (x *RestoreRequest) String() string { func (*RestoreRequest) ProtoMessage() {} func (x *RestoreRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[32] + mi := &file_resource_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2657,7 +2856,7 @@ func (x *RestoreRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreRequest.ProtoReflect.Descriptor instead. func (*RestoreRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{32} + return file_resource_proto_rawDescGZIP(), []int{34} } func (x *RestoreRequest) GetKey() *ResourceKey { @@ -2686,7 +2885,7 @@ type RestoreResponse struct { func (x *RestoreResponse) Reset() { *x = RestoreResponse{} - mi := &file_resource_proto_msgTypes[33] + mi := &file_resource_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2698,7 +2897,7 @@ func (x *RestoreResponse) String() string { func (*RestoreResponse) ProtoMessage() {} func (x *RestoreResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[33] + mi := &file_resource_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2711,7 +2910,7 @@ func (x *RestoreResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreResponse.ProtoReflect.Descriptor instead. func (*RestoreResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{33} + return file_resource_proto_rawDescGZIP(), []int{35} } func (x *RestoreResponse) GetError() *ErrorResult { @@ -2746,7 +2945,7 @@ type PutBlobRequest struct { func (x *PutBlobRequest) Reset() { *x = PutBlobRequest{} - mi := &file_resource_proto_msgTypes[34] + mi := &file_resource_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2758,7 +2957,7 @@ func (x *PutBlobRequest) String() string { func (*PutBlobRequest) ProtoMessage() {} func (x *PutBlobRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[34] + mi := &file_resource_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2771,7 +2970,7 @@ func (x *PutBlobRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PutBlobRequest.ProtoReflect.Descriptor instead. func (*PutBlobRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{34} + return file_resource_proto_rawDescGZIP(), []int{36} } func (x *PutBlobRequest) GetResource() *ResourceKey { @@ -2824,7 +3023,7 @@ type PutBlobResponse struct { func (x *PutBlobResponse) Reset() { *x = PutBlobResponse{} - mi := &file_resource_proto_msgTypes[35] + mi := &file_resource_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2836,7 +3035,7 @@ func (x *PutBlobResponse) String() string { func (*PutBlobResponse) ProtoMessage() {} func (x *PutBlobResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[35] + mi := &file_resource_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2849,7 +3048,7 @@ func (x *PutBlobResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PutBlobResponse.ProtoReflect.Descriptor instead. func (*PutBlobResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{35} + return file_resource_proto_rawDescGZIP(), []int{37} } func (x *PutBlobResponse) GetError() *ErrorResult { @@ -2914,7 +3113,7 @@ type GetBlobRequest struct { func (x *GetBlobRequest) Reset() { *x = GetBlobRequest{} - mi := &file_resource_proto_msgTypes[36] + mi := &file_resource_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2926,7 +3125,7 @@ func (x *GetBlobRequest) String() string { func (*GetBlobRequest) ProtoMessage() {} func (x *GetBlobRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[36] + mi := &file_resource_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2939,7 +3138,7 @@ func (x *GetBlobRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBlobRequest.ProtoReflect.Descriptor instead. func (*GetBlobRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{36} + return file_resource_proto_rawDescGZIP(), []int{38} } func (x *GetBlobRequest) GetResource() *ResourceKey { @@ -2981,7 +3180,7 @@ type GetBlobResponse struct { func (x *GetBlobResponse) Reset() { *x = GetBlobResponse{} - mi := &file_resource_proto_msgTypes[37] + mi := &file_resource_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2993,7 +3192,7 @@ func (x *GetBlobResponse) String() string { func (*GetBlobResponse) ProtoMessage() {} func (x *GetBlobResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[37] + mi := &file_resource_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3006,7 +3205,7 @@ func (x *GetBlobResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBlobResponse.ProtoReflect.Descriptor instead. func (*GetBlobResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{37} + return file_resource_proto_rawDescGZIP(), []int{39} } func (x *GetBlobResponse) GetError() *ErrorResult { @@ -3047,7 +3246,7 @@ type WatchEvent_Resource struct { func (x *WatchEvent_Resource) Reset() { *x = WatchEvent_Resource{} - mi := &file_resource_proto_msgTypes[38] + mi := &file_resource_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3059,7 +3258,7 @@ func (x *WatchEvent_Resource) String() string { func (*WatchEvent_Resource) ProtoMessage() {} func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[38] + mi := &file_resource_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3089,6 +3288,168 @@ func (x *WatchEvent_Resource) GetValue() []byte { return nil } +type BatchResponse_Summary struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Group string `protobuf:"bytes,2,opt,name=group,proto3" json:"group,omitempty"` + Resource string `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"` + Count int64 `protobuf:"varint,4,opt,name=count,proto3" json:"count,omitempty"` + History int64 `protobuf:"varint,5,opt,name=history,proto3" json:"history,omitempty"` + ResourceVersion int64 `protobuf:"varint,6,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` // The max saved RV + // The previous count + PreviousCount int64 `protobuf:"varint,7,opt,name=previous_count,json=previousCount,proto3" json:"previous_count,omitempty"` + PreviousHistory int64 `protobuf:"varint,8,opt,name=previous_history,json=previousHistory,proto3" json:"previous_history,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BatchResponse_Summary) Reset() { + *x = BatchResponse_Summary{} + mi := &file_resource_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BatchResponse_Summary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchResponse_Summary) ProtoMessage() {} + +func (x *BatchResponse_Summary) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchResponse_Summary.ProtoReflect.Descriptor instead. +func (*BatchResponse_Summary) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{20, 0} +} + +func (x *BatchResponse_Summary) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *BatchResponse_Summary) GetGroup() string { + if x != nil { + return x.Group + } + return "" +} + +func (x *BatchResponse_Summary) GetResource() string { + if x != nil { + return x.Resource + } + return "" +} + +func (x *BatchResponse_Summary) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *BatchResponse_Summary) GetHistory() int64 { + if x != nil { + return x.History + } + return 0 +} + +func (x *BatchResponse_Summary) GetResourceVersion() int64 { + if x != nil { + return x.ResourceVersion + } + return 0 +} + +func (x *BatchResponse_Summary) GetPreviousCount() int64 { + if x != nil { + return x.PreviousCount + } + return 0 +} + +func (x *BatchResponse_Summary) GetPreviousHistory() int64 { + if x != nil { + return x.PreviousHistory + } + return 0 +} + +// Collect a few invalid messages +type BatchResponse_Rejected struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Action BatchRequest_Action `protobuf:"varint,2,opt,name=action,proto3,enum=resource.BatchRequest_Action" json:"action,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BatchResponse_Rejected) Reset() { + *x = BatchResponse_Rejected{} + mi := &file_resource_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BatchResponse_Rejected) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchResponse_Rejected) ProtoMessage() {} + +func (x *BatchResponse_Rejected) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchResponse_Rejected.ProtoReflect.Descriptor instead. +func (*BatchResponse_Rejected) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{20, 1} +} + +func (x *BatchResponse_Rejected) GetKey() *ResourceKey { + if x != nil { + return x.Key + } + return nil +} + +func (x *BatchResponse_Rejected) GetAction() BatchRequest_Action { + if x != nil { + return x.Action + } + return BatchRequest_UNKNOWN +} + +func (x *BatchResponse_Rejected) GetError() string { + if x != nil { + return x.Error + } + return "" +} + type ResourceStatsResponse_Stats struct { state protoimpl.MessageState `protogen:"open.v1"` // Resource group @@ -3103,7 +3464,7 @@ type ResourceStatsResponse_Stats struct { func (x *ResourceStatsResponse_Stats) Reset() { *x = ResourceStatsResponse_Stats{} - mi := &file_resource_proto_msgTypes[39] + mi := &file_resource_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3115,7 +3476,7 @@ func (x *ResourceStatsResponse_Stats) String() string { func (*ResourceStatsResponse_Stats) ProtoMessage() {} func (x *ResourceStatsResponse_Stats) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[39] + mi := &file_resource_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3128,7 +3489,7 @@ func (x *ResourceStatsResponse_Stats) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceStatsResponse_Stats.ProtoReflect.Descriptor instead. func (*ResourceStatsResponse_Stats) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{20, 0} + return file_resource_proto_rawDescGZIP(), []int{22, 0} } func (x *ResourceStatsResponse_Stats) GetGroup() string { @@ -3162,7 +3523,7 @@ type ResourceSearchRequest_Sort struct { func (x *ResourceSearchRequest_Sort) Reset() { *x = ResourceSearchRequest_Sort{} - mi := &file_resource_proto_msgTypes[40] + mi := &file_resource_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3174,7 +3535,7 @@ func (x *ResourceSearchRequest_Sort) String() string { func (*ResourceSearchRequest_Sort) ProtoMessage() {} func (x *ResourceSearchRequest_Sort) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[40] + mi := &file_resource_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3187,7 +3548,7 @@ func (x *ResourceSearchRequest_Sort) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceSearchRequest_Sort.ProtoReflect.Descriptor instead. func (*ResourceSearchRequest_Sort) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{21, 0} + return file_resource_proto_rawDescGZIP(), []int{23, 0} } func (x *ResourceSearchRequest_Sort) GetField() string { @@ -3214,7 +3575,7 @@ type ResourceSearchRequest_Facet struct { func (x *ResourceSearchRequest_Facet) Reset() { *x = ResourceSearchRequest_Facet{} - mi := &file_resource_proto_msgTypes[41] + mi := &file_resource_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3226,7 +3587,7 @@ func (x *ResourceSearchRequest_Facet) String() string { func (*ResourceSearchRequest_Facet) ProtoMessage() {} func (x *ResourceSearchRequest_Facet) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[41] + mi := &file_resource_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3239,7 +3600,7 @@ func (x *ResourceSearchRequest_Facet) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceSearchRequest_Facet.ProtoReflect.Descriptor instead. func (*ResourceSearchRequest_Facet) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{21, 1} + return file_resource_proto_rawDescGZIP(), []int{23, 1} } func (x *ResourceSearchRequest_Facet) GetField() string { @@ -3271,7 +3632,7 @@ type ResourceSearchResponse_Facet struct { func (x *ResourceSearchResponse_Facet) Reset() { *x = ResourceSearchResponse_Facet{} - mi := &file_resource_proto_msgTypes[43] + mi := &file_resource_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3283,7 +3644,7 @@ func (x *ResourceSearchResponse_Facet) String() string { func (*ResourceSearchResponse_Facet) ProtoMessage() {} func (x *ResourceSearchResponse_Facet) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[43] + mi := &file_resource_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3296,7 +3657,7 @@ func (x *ResourceSearchResponse_Facet) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceSearchResponse_Facet.ProtoReflect.Descriptor instead. func (*ResourceSearchResponse_Facet) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{22, 0} + return file_resource_proto_rawDescGZIP(), []int{24, 0} } func (x *ResourceSearchResponse_Facet) GetField() string { @@ -3337,7 +3698,7 @@ type ResourceSearchResponse_TermFacet struct { func (x *ResourceSearchResponse_TermFacet) Reset() { *x = ResourceSearchResponse_TermFacet{} - mi := &file_resource_proto_msgTypes[44] + mi := &file_resource_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3349,7 +3710,7 @@ func (x *ResourceSearchResponse_TermFacet) String() string { func (*ResourceSearchResponse_TermFacet) ProtoMessage() {} func (x *ResourceSearchResponse_TermFacet) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[44] + mi := &file_resource_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3362,7 +3723,7 @@ func (x *ResourceSearchResponse_TermFacet) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceSearchResponse_TermFacet.ProtoReflect.Descriptor instead. func (*ResourceSearchResponse_TermFacet) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{22, 1} + return file_resource_proto_rawDescGZIP(), []int{24, 1} } func (x *ResourceSearchResponse_TermFacet) GetTerm() string { @@ -3399,7 +3760,7 @@ type ListRepositoryObjectsResponse_Item struct { func (x *ListRepositoryObjectsResponse_Item) Reset() { *x = ListRepositoryObjectsResponse_Item{} - mi := &file_resource_proto_msgTypes[46] + mi := &file_resource_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3411,7 +3772,7 @@ func (x *ListRepositoryObjectsResponse_Item) String() string { func (*ListRepositoryObjectsResponse_Item) ProtoMessage() {} func (x *ListRepositoryObjectsResponse_Item) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[46] + mi := &file_resource_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3424,7 +3785,7 @@ func (x *ListRepositoryObjectsResponse_Item) ProtoReflect() protoreflect.Message // Deprecated: Use ListRepositoryObjectsResponse_Item.ProtoReflect.Descriptor instead. func (*ListRepositoryObjectsResponse_Item) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{24, 0} + return file_resource_proto_rawDescGZIP(), []int{26, 0} } func (x *ListRepositoryObjectsResponse_Item) GetObject() *ResourceKey { @@ -3481,7 +3842,7 @@ type CountRepositoryObjectsResponse_ResourceCount struct { func (x *CountRepositoryObjectsResponse_ResourceCount) Reset() { *x = CountRepositoryObjectsResponse_ResourceCount{} - mi := &file_resource_proto_msgTypes[47] + mi := &file_resource_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3493,7 +3854,7 @@ func (x *CountRepositoryObjectsResponse_ResourceCount) String() string { func (*CountRepositoryObjectsResponse_ResourceCount) ProtoMessage() {} func (x *CountRepositoryObjectsResponse_ResourceCount) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[47] + mi := &file_resource_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3506,7 +3867,7 @@ func (x *CountRepositoryObjectsResponse_ResourceCount) ProtoReflect() protorefle // Deprecated: Use CountRepositoryObjectsResponse_ResourceCount.ProtoReflect.Descriptor instead. func (*CountRepositoryObjectsResponse_ResourceCount) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{26, 0} + return file_resource_proto_rawDescGZIP(), []int{28, 0} } func (x *CountRepositoryObjectsResponse_ResourceCount) GetRepository() string { @@ -3559,7 +3920,7 @@ type ResourceTableColumnDefinition_Properties struct { func (x *ResourceTableColumnDefinition_Properties) Reset() { *x = ResourceTableColumnDefinition_Properties{} - mi := &file_resource_proto_msgTypes[48] + mi := &file_resource_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3571,7 +3932,7 @@ func (x *ResourceTableColumnDefinition_Properties) String() string { func (*ResourceTableColumnDefinition_Properties) ProtoMessage() {} func (x *ResourceTableColumnDefinition_Properties) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[48] + mi := &file_resource_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3584,7 +3945,7 @@ func (x *ResourceTableColumnDefinition_Properties) ProtoReflect() protoreflect.M // Deprecated: Use ResourceTableColumnDefinition_Properties.ProtoReflect.Descriptor instead. func (*ResourceTableColumnDefinition_Properties) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{30, 0} + return file_resource_proto_rawDescGZIP(), []int{32, 0} } func (x *ResourceTableColumnDefinition_Properties) GetUniqueValues() bool { @@ -3624,7 +3985,7 @@ func (x *ResourceTableColumnDefinition_Properties) GetDefaultValue() []byte { var File_resource_proto protoreflect.FileDescriptor -var file_resource_proto_rawDesc = []byte{ +var file_resource_proto_rawDesc = string([]byte{ 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x71, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, @@ -3806,527 +4167,598 @@ var file_resource_proto_rawDesc = []byte{ 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52, 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x62, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, - 0x05, 0x6b, 0x69, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x69, - 0x6e, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0xd2, 0x01, 0x0a, 0x15, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0xd9, 0x01, 0x0a, 0x0c, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x35, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x3b, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, + 0x05, 0x41, 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x4f, 0x44, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, + 0x44, 0x10, 0x03, 0x22, 0xdf, 0x04, 0x0a, 0x0d, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x12, 0x3b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x1a, - 0x4f, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, - 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x22, 0xee, 0x04, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, - 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x66, - 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, - 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, - 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, - 0x66, 0x73, 0x65, 0x74, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x18, 0x06, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, - 0x42, 0x79, 0x12, 0x40, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, - 0x61, 0x63, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x08, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, - 0x65, 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, - 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, - 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46, - 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, - 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, - 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, - 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, - 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, - 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, - 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, 0x43, - 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, - 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x61, - 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, - 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, - 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6d, 0x69, 0x73, 0x73, - 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, - 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, - 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a, - 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, - 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x78, - 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, - 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, - 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, - 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xda, 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73, - 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x69, 0x74, - 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, - 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, - 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, - 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x1a, 0x9f, 0x01, 0x0a, 0x04, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x2d, 0x0a, 0x06, - 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, + 0x6f, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, + 0x12, 0x39, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x75, 0x6d, 0x6d, 0x61, + 0x72, 0x79, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x3c, 0x0a, 0x08, 0x72, + 0x65, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x52, + 0x08, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x1a, 0x86, 0x02, 0x0a, 0x07, 0x53, 0x75, + 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x68, + 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x68, 0x69, + 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, + 0x75, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x70, 0x72, 0x65, 0x76, 0x69, + 0x6f, 0x75, 0x73, 0x5f, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0f, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x48, 0x69, 0x73, 0x74, 0x6f, + 0x72, 0x79, 0x1a, 0x80, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, + 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, - 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, - 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, - 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, - 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x51, 0x0a, 0x1d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x94, 0x02, 0x0a, 0x1e, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, - 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, - 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x77, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, - 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, - 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, - 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, - 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, - 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, - 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, - 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x87, 0x02, - 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, - 0x41, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, - 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, - 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x52, 0x04, 0x72, 0x6f, - 0x77, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, - 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, - 0x6e, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74, - 0x65, 0x6d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xf1, 0x04, 0x0a, 0x1d, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, - 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, - 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x61, 0x72, 0x72, 0x61, - 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x41, 0x72, 0x72, 0x61, 0x79, - 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x52, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, - 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70, - 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, - 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, - 0x74, 0x79, 0x1a, 0xae, 0x01, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, - 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x74, - 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x66, 0x72, 0x65, 0x65, 0x54, - 0x65, 0x78, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, 0x62, 0x6c, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, - 0x62, 0x6c, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x74, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6e, 0x6f, 0x74, 0x4e, 0x75, 0x6c, 0x6c, 0x12, 0x23, - 0x0a, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x22, 0x95, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x54, 0x59, - 0x50, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, - 0x12, 0x0b, 0x0a, 0x07, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x02, 0x12, 0x09, 0x0a, - 0x05, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x36, - 0x34, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, 0x05, 0x12, 0x0a, - 0x0a, 0x06, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x06, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, - 0x54, 0x45, 0x10, 0x07, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x49, 0x4d, - 0x45, 0x10, 0x08, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x09, 0x12, - 0x0a, 0x0a, 0x06, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0a, 0x22, 0x94, 0x01, 0x0a, 0x10, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, - 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x35, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x62, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, + 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6b, + 0x69, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x69, 0x6e, 0x64, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0xd2, 0x01, 0x0a, 0x15, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x12, 0x3b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x1a, 0x4f, 0x0a, + 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xee, + 0x04, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x66, 0x65, 0x64, + 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4b, 0x65, 0x79, 0x52, 0x09, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x12, 0x14, + 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, + 0x75, 0x65, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, + 0x66, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, + 0x65, 0x74, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x18, 0x06, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, + 0x12, 0x40, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x61, 0x63, + 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, + 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x78, 0x70, + 0x6c, 0x61, 0x69, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, + 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46, 0x61, 0x63, + 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x1a, 0x5f, + 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3b, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x22, 0x64, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, - 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x69, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x22, 0xd3, 0x01, 0x0a, 0x0e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x6d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, - 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, - 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1c, 0x0a, 0x06, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x52, 0x50, 0x43, 0x10, 0x00, 0x12, - 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x01, 0x22, 0xc1, 0x01, 0x0a, 0x0f, 0x50, 0x75, - 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, - 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, - 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, - 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x22, 0x98, 0x01, - 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x28, - 0x0a, 0x10, 0x6d, 0x75, 0x73, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x62, 0x79, 0x74, - 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6d, 0x75, 0x73, 0x74, 0x50, 0x72, - 0x6f, 0x78, 0x79, 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, 0x89, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, - 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, - 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, - 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xad, 0x03, 0x0a, 0x0d, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, - 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, - 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x52, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, - 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, - 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0xa9, 0x01, 0x0a, 0x0d, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, - 0x74, 0x61, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xe8, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, - 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x6b, 0x0a, 0x16, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, - 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, + 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x69, 0x74, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x69, + 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x73, + 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x41, + 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, + 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x61, 0x63, 0x65, + 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, + 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, + 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, + 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x74, 0x65, + 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a, 0x46, 0x61, + 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, + 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, + 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x78, 0x0a, 0x1c, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, + 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xda, 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x32, 0x8b, 0x01, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x62, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e, - 0x0a, 0x07, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, - 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, - 0x0a, 0x07, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, - 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, - 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, - 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, - 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, - 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} + 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, + 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x1a, 0x9f, 0x01, 0x0a, 0x04, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x2d, 0x0a, 0x06, 0x6f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, + 0x79, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, + 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, + 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, + 0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, + 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x22, 0x51, 0x0a, 0x1d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x94, 0x02, 0x0a, 0x1e, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x77, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x2e, 0x0a, + 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, + 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, + 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, + 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, + 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, + 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x87, 0x02, 0x0a, 0x0d, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x41, 0x0a, + 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, + 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, + 0x12, 0x2e, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, + 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, + 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, + 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74, 0x65, 0x6d, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xf1, 0x04, 0x0a, 0x1d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, + 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, + 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x41, 0x72, 0x72, 0x61, 0x79, 0x12, 0x20, + 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x52, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, + 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x72, + 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, + 0x74, 0x69, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, + 0x1a, 0xae, 0x01, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, + 0x23, 0x0a, 0x0d, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x74, 0x65, 0x78, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x66, 0x72, 0x65, 0x65, 0x54, 0x65, 0x78, + 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, 0x62, 0x6c, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, 0x62, 0x6c, + 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x74, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x6e, 0x6f, 0x74, 0x4e, 0x75, 0x6c, 0x6c, 0x12, 0x23, 0x0a, 0x0d, + 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x22, 0x95, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0b, + 0x0a, 0x07, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x49, + 0x4e, 0x54, 0x33, 0x32, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x10, + 0x04, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, + 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x06, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, 0x45, + 0x10, 0x07, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x10, + 0x08, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x09, 0x12, 0x0a, 0x0a, + 0x06, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0a, 0x22, 0x94, 0x01, 0x0a, 0x10, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x12, 0x27, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, + 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0c, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x22, 0x64, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x69, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x22, 0xd3, 0x01, 0x0a, 0x0e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1c, 0x0a, 0x06, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x52, 0x50, 0x43, 0x10, 0x00, 0x12, 0x08, 0x0a, + 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x01, 0x22, 0xc1, 0x01, 0x0a, 0x0f, 0x50, 0x75, 0x74, 0x42, + 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, + 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, + 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x68, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x22, 0x98, 0x01, 0x0a, 0x0e, + 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, + 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, + 0x6d, 0x75, 0x73, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6d, 0x75, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x78, + 0x79, 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, 0x89, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x6c, + 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, + 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, + 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xad, 0x03, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, + 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, + 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, + 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, + 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, + 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0x4f, 0x0a, 0x0a, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x50, 0x72, + 0x6f, 0x63, 0x65, 0x73, 0x73, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x32, 0xa9, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xe8, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x6b, 0x0a, 0x16, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x73, 0x12, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x26, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, + 0x8b, 0x01, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x62, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e, 0x0a, + 0x07, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, + 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, + 0x07, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, + 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, 0x0a, + 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, 0x09, + 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, + 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) var ( file_resource_proto_rawDescOnce sync.Once - file_resource_proto_rawDescData = file_resource_proto_rawDesc + file_resource_proto_rawDescData []byte ) func file_resource_proto_rawDescGZIP() []byte { file_resource_proto_rawDescOnce.Do(func() { - file_resource_proto_rawDescData = protoimpl.X.CompressGZIP(file_resource_proto_rawDescData) + file_resource_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_resource_proto_rawDesc), len(file_resource_proto_rawDesc))) }) return file_resource_proto_rawDescData } -var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 49) +var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 7) +var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 53) var file_resource_proto_goTypes = []any{ (ResourceVersionMatch)(0), // 0: resource.ResourceVersionMatch (ListRequest_Source)(0), // 1: resource.ListRequest.Source (WatchEvent_Type)(0), // 2: resource.WatchEvent.Type - (HealthCheckResponse_ServingStatus)(0), // 3: resource.HealthCheckResponse.ServingStatus - (ResourceTableColumnDefinition_ColumnType)(0), // 4: resource.ResourceTableColumnDefinition.ColumnType - (PutBlobRequest_Method)(0), // 5: resource.PutBlobRequest.Method - (*ResourceKey)(nil), // 6: resource.ResourceKey - (*ResourceWrapper)(nil), // 7: resource.ResourceWrapper - (*ErrorResult)(nil), // 8: resource.ErrorResult - (*ErrorDetails)(nil), // 9: resource.ErrorDetails - (*ErrorCause)(nil), // 10: resource.ErrorCause - (*CreateRequest)(nil), // 11: resource.CreateRequest - (*CreateResponse)(nil), // 12: resource.CreateResponse - (*UpdateRequest)(nil), // 13: resource.UpdateRequest - (*UpdateResponse)(nil), // 14: resource.UpdateResponse - (*DeleteRequest)(nil), // 15: resource.DeleteRequest - (*DeleteResponse)(nil), // 16: resource.DeleteResponse - (*ReadRequest)(nil), // 17: resource.ReadRequest - (*ReadResponse)(nil), // 18: resource.ReadResponse - (*Requirement)(nil), // 19: resource.Requirement - (*ListOptions)(nil), // 20: resource.ListOptions - (*ListRequest)(nil), // 21: resource.ListRequest - (*ListResponse)(nil), // 22: resource.ListResponse - (*WatchRequest)(nil), // 23: resource.WatchRequest - (*WatchEvent)(nil), // 24: resource.WatchEvent - (*ResourceStatsRequest)(nil), // 25: resource.ResourceStatsRequest - (*ResourceStatsResponse)(nil), // 26: resource.ResourceStatsResponse - (*ResourceSearchRequest)(nil), // 27: resource.ResourceSearchRequest - (*ResourceSearchResponse)(nil), // 28: resource.ResourceSearchResponse - (*ListRepositoryObjectsRequest)(nil), // 29: resource.ListRepositoryObjectsRequest - (*ListRepositoryObjectsResponse)(nil), // 30: resource.ListRepositoryObjectsResponse - (*CountRepositoryObjectsRequest)(nil), // 31: resource.CountRepositoryObjectsRequest - (*CountRepositoryObjectsResponse)(nil), // 32: resource.CountRepositoryObjectsResponse - (*HealthCheckRequest)(nil), // 33: resource.HealthCheckRequest - (*HealthCheckResponse)(nil), // 34: resource.HealthCheckResponse - (*ResourceTable)(nil), // 35: resource.ResourceTable - (*ResourceTableColumnDefinition)(nil), // 36: resource.ResourceTableColumnDefinition - (*ResourceTableRow)(nil), // 37: resource.ResourceTableRow - (*RestoreRequest)(nil), // 38: resource.RestoreRequest - (*RestoreResponse)(nil), // 39: resource.RestoreResponse - (*PutBlobRequest)(nil), // 40: resource.PutBlobRequest - (*PutBlobResponse)(nil), // 41: resource.PutBlobResponse - (*GetBlobRequest)(nil), // 42: resource.GetBlobRequest - (*GetBlobResponse)(nil), // 43: resource.GetBlobResponse - (*WatchEvent_Resource)(nil), // 44: resource.WatchEvent.Resource - (*ResourceStatsResponse_Stats)(nil), // 45: resource.ResourceStatsResponse.Stats - (*ResourceSearchRequest_Sort)(nil), // 46: resource.ResourceSearchRequest.Sort - (*ResourceSearchRequest_Facet)(nil), // 47: resource.ResourceSearchRequest.Facet - nil, // 48: resource.ResourceSearchRequest.FacetEntry - (*ResourceSearchResponse_Facet)(nil), // 49: resource.ResourceSearchResponse.Facet - (*ResourceSearchResponse_TermFacet)(nil), // 50: resource.ResourceSearchResponse.TermFacet - nil, // 51: resource.ResourceSearchResponse.FacetEntry - (*ListRepositoryObjectsResponse_Item)(nil), // 52: resource.ListRepositoryObjectsResponse.Item - (*CountRepositoryObjectsResponse_ResourceCount)(nil), // 53: resource.CountRepositoryObjectsResponse.ResourceCount - (*ResourceTableColumnDefinition_Properties)(nil), // 54: resource.ResourceTableColumnDefinition.Properties + (BatchRequest_Action)(0), // 3: resource.BatchRequest.Action + (HealthCheckResponse_ServingStatus)(0), // 4: resource.HealthCheckResponse.ServingStatus + (ResourceTableColumnDefinition_ColumnType)(0), // 5: resource.ResourceTableColumnDefinition.ColumnType + (PutBlobRequest_Method)(0), // 6: resource.PutBlobRequest.Method + (*ResourceKey)(nil), // 7: resource.ResourceKey + (*ResourceWrapper)(nil), // 8: resource.ResourceWrapper + (*ErrorResult)(nil), // 9: resource.ErrorResult + (*ErrorDetails)(nil), // 10: resource.ErrorDetails + (*ErrorCause)(nil), // 11: resource.ErrorCause + (*CreateRequest)(nil), // 12: resource.CreateRequest + (*CreateResponse)(nil), // 13: resource.CreateResponse + (*UpdateRequest)(nil), // 14: resource.UpdateRequest + (*UpdateResponse)(nil), // 15: resource.UpdateResponse + (*DeleteRequest)(nil), // 16: resource.DeleteRequest + (*DeleteResponse)(nil), // 17: resource.DeleteResponse + (*ReadRequest)(nil), // 18: resource.ReadRequest + (*ReadResponse)(nil), // 19: resource.ReadResponse + (*Requirement)(nil), // 20: resource.Requirement + (*ListOptions)(nil), // 21: resource.ListOptions + (*ListRequest)(nil), // 22: resource.ListRequest + (*ListResponse)(nil), // 23: resource.ListResponse + (*WatchRequest)(nil), // 24: resource.WatchRequest + (*WatchEvent)(nil), // 25: resource.WatchEvent + (*BatchRequest)(nil), // 26: resource.BatchRequest + (*BatchResponse)(nil), // 27: resource.BatchResponse + (*ResourceStatsRequest)(nil), // 28: resource.ResourceStatsRequest + (*ResourceStatsResponse)(nil), // 29: resource.ResourceStatsResponse + (*ResourceSearchRequest)(nil), // 30: resource.ResourceSearchRequest + (*ResourceSearchResponse)(nil), // 31: resource.ResourceSearchResponse + (*ListRepositoryObjectsRequest)(nil), // 32: resource.ListRepositoryObjectsRequest + (*ListRepositoryObjectsResponse)(nil), // 33: resource.ListRepositoryObjectsResponse + (*CountRepositoryObjectsRequest)(nil), // 34: resource.CountRepositoryObjectsRequest + (*CountRepositoryObjectsResponse)(nil), // 35: resource.CountRepositoryObjectsResponse + (*HealthCheckRequest)(nil), // 36: resource.HealthCheckRequest + (*HealthCheckResponse)(nil), // 37: resource.HealthCheckResponse + (*ResourceTable)(nil), // 38: resource.ResourceTable + (*ResourceTableColumnDefinition)(nil), // 39: resource.ResourceTableColumnDefinition + (*ResourceTableRow)(nil), // 40: resource.ResourceTableRow + (*RestoreRequest)(nil), // 41: resource.RestoreRequest + (*RestoreResponse)(nil), // 42: resource.RestoreResponse + (*PutBlobRequest)(nil), // 43: resource.PutBlobRequest + (*PutBlobResponse)(nil), // 44: resource.PutBlobResponse + (*GetBlobRequest)(nil), // 45: resource.GetBlobRequest + (*GetBlobResponse)(nil), // 46: resource.GetBlobResponse + (*WatchEvent_Resource)(nil), // 47: resource.WatchEvent.Resource + (*BatchResponse_Summary)(nil), // 48: resource.BatchResponse.Summary + (*BatchResponse_Rejected)(nil), // 49: resource.BatchResponse.Rejected + (*ResourceStatsResponse_Stats)(nil), // 50: resource.ResourceStatsResponse.Stats + (*ResourceSearchRequest_Sort)(nil), // 51: resource.ResourceSearchRequest.Sort + (*ResourceSearchRequest_Facet)(nil), // 52: resource.ResourceSearchRequest.Facet + nil, // 53: resource.ResourceSearchRequest.FacetEntry + (*ResourceSearchResponse_Facet)(nil), // 54: resource.ResourceSearchResponse.Facet + (*ResourceSearchResponse_TermFacet)(nil), // 55: resource.ResourceSearchResponse.TermFacet + nil, // 56: resource.ResourceSearchResponse.FacetEntry + (*ListRepositoryObjectsResponse_Item)(nil), // 57: resource.ListRepositoryObjectsResponse.Item + (*CountRepositoryObjectsResponse_ResourceCount)(nil), // 58: resource.CountRepositoryObjectsResponse.ResourceCount + (*ResourceTableColumnDefinition_Properties)(nil), // 59: resource.ResourceTableColumnDefinition.Properties } var file_resource_proto_depIdxs = []int32{ - 9, // 0: resource.ErrorResult.details:type_name -> resource.ErrorDetails - 10, // 1: resource.ErrorDetails.causes:type_name -> resource.ErrorCause - 6, // 2: resource.CreateRequest.key:type_name -> resource.ResourceKey - 8, // 3: resource.CreateResponse.error:type_name -> resource.ErrorResult - 6, // 4: resource.UpdateRequest.key:type_name -> resource.ResourceKey - 8, // 5: resource.UpdateResponse.error:type_name -> resource.ErrorResult - 6, // 6: resource.DeleteRequest.key:type_name -> resource.ResourceKey - 8, // 7: resource.DeleteResponse.error:type_name -> resource.ErrorResult - 6, // 8: resource.ReadRequest.key:type_name -> resource.ResourceKey - 8, // 9: resource.ReadResponse.error:type_name -> resource.ErrorResult - 6, // 10: resource.ListOptions.key:type_name -> resource.ResourceKey - 19, // 11: resource.ListOptions.labels:type_name -> resource.Requirement - 19, // 12: resource.ListOptions.fields:type_name -> resource.Requirement + 10, // 0: resource.ErrorResult.details:type_name -> resource.ErrorDetails + 11, // 1: resource.ErrorDetails.causes:type_name -> resource.ErrorCause + 7, // 2: resource.CreateRequest.key:type_name -> resource.ResourceKey + 9, // 3: resource.CreateResponse.error:type_name -> resource.ErrorResult + 7, // 4: resource.UpdateRequest.key:type_name -> resource.ResourceKey + 9, // 5: resource.UpdateResponse.error:type_name -> resource.ErrorResult + 7, // 6: resource.DeleteRequest.key:type_name -> resource.ResourceKey + 9, // 7: resource.DeleteResponse.error:type_name -> resource.ErrorResult + 7, // 8: resource.ReadRequest.key:type_name -> resource.ResourceKey + 9, // 9: resource.ReadResponse.error:type_name -> resource.ErrorResult + 7, // 10: resource.ListOptions.key:type_name -> resource.ResourceKey + 20, // 11: resource.ListOptions.labels:type_name -> resource.Requirement + 20, // 12: resource.ListOptions.fields:type_name -> resource.Requirement 0, // 13: resource.ListRequest.version_match:type_name -> resource.ResourceVersionMatch - 20, // 14: resource.ListRequest.options:type_name -> resource.ListOptions + 21, // 14: resource.ListRequest.options:type_name -> resource.ListOptions 1, // 15: resource.ListRequest.source:type_name -> resource.ListRequest.Source - 7, // 16: resource.ListResponse.items:type_name -> resource.ResourceWrapper - 8, // 17: resource.ListResponse.error:type_name -> resource.ErrorResult - 20, // 18: resource.WatchRequest.options:type_name -> resource.ListOptions + 8, // 16: resource.ListResponse.items:type_name -> resource.ResourceWrapper + 9, // 17: resource.ListResponse.error:type_name -> resource.ErrorResult + 21, // 18: resource.WatchRequest.options:type_name -> resource.ListOptions 2, // 19: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type - 44, // 20: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource - 44, // 21: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource - 8, // 22: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult - 45, // 23: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats - 20, // 24: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions - 6, // 25: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey - 46, // 26: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort - 48, // 27: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry - 8, // 28: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult - 6, // 29: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey - 35, // 30: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable - 51, // 31: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry - 52, // 32: resource.ListRepositoryObjectsResponse.items:type_name -> resource.ListRepositoryObjectsResponse.Item - 8, // 33: resource.ListRepositoryObjectsResponse.error:type_name -> resource.ErrorResult - 53, // 34: resource.CountRepositoryObjectsResponse.items:type_name -> resource.CountRepositoryObjectsResponse.ResourceCount - 8, // 35: resource.CountRepositoryObjectsResponse.error:type_name -> resource.ErrorResult - 3, // 36: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus - 36, // 37: resource.ResourceTable.columns:type_name -> resource.ResourceTableColumnDefinition - 37, // 38: resource.ResourceTable.rows:type_name -> resource.ResourceTableRow - 4, // 39: resource.ResourceTableColumnDefinition.type:type_name -> resource.ResourceTableColumnDefinition.ColumnType - 54, // 40: resource.ResourceTableColumnDefinition.properties:type_name -> resource.ResourceTableColumnDefinition.Properties - 6, // 41: resource.ResourceTableRow.key:type_name -> resource.ResourceKey - 6, // 42: resource.RestoreRequest.key:type_name -> resource.ResourceKey - 8, // 43: resource.RestoreResponse.error:type_name -> resource.ErrorResult - 6, // 44: resource.PutBlobRequest.resource:type_name -> resource.ResourceKey - 5, // 45: resource.PutBlobRequest.method:type_name -> resource.PutBlobRequest.Method - 8, // 46: resource.PutBlobResponse.error:type_name -> resource.ErrorResult - 6, // 47: resource.GetBlobRequest.resource:type_name -> resource.ResourceKey - 8, // 48: resource.GetBlobResponse.error:type_name -> resource.ErrorResult - 47, // 49: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet - 50, // 50: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet - 49, // 51: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet - 6, // 52: resource.ListRepositoryObjectsResponse.Item.object:type_name -> resource.ResourceKey - 17, // 53: resource.ResourceStore.Read:input_type -> resource.ReadRequest - 11, // 54: resource.ResourceStore.Create:input_type -> resource.CreateRequest - 13, // 55: resource.ResourceStore.Update:input_type -> resource.UpdateRequest - 15, // 56: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest - 38, // 57: resource.ResourceStore.Restore:input_type -> resource.RestoreRequest - 21, // 58: resource.ResourceStore.List:input_type -> resource.ListRequest - 23, // 59: resource.ResourceStore.Watch:input_type -> resource.WatchRequest - 27, // 60: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest - 25, // 61: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest - 31, // 62: resource.RepositoryIndex.CountRepositoryObjects:input_type -> resource.CountRepositoryObjectsRequest - 29, // 63: resource.RepositoryIndex.ListRepositoryObjects:input_type -> resource.ListRepositoryObjectsRequest - 40, // 64: resource.BlobStore.PutBlob:input_type -> resource.PutBlobRequest - 42, // 65: resource.BlobStore.GetBlob:input_type -> resource.GetBlobRequest - 33, // 66: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest - 18, // 67: resource.ResourceStore.Read:output_type -> resource.ReadResponse - 12, // 68: resource.ResourceStore.Create:output_type -> resource.CreateResponse - 14, // 69: resource.ResourceStore.Update:output_type -> resource.UpdateResponse - 16, // 70: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse - 39, // 71: resource.ResourceStore.Restore:output_type -> resource.RestoreResponse - 22, // 72: resource.ResourceStore.List:output_type -> resource.ListResponse - 24, // 73: resource.ResourceStore.Watch:output_type -> resource.WatchEvent - 28, // 74: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse - 26, // 75: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse - 32, // 76: resource.RepositoryIndex.CountRepositoryObjects:output_type -> resource.CountRepositoryObjectsResponse - 30, // 77: resource.RepositoryIndex.ListRepositoryObjects:output_type -> resource.ListRepositoryObjectsResponse - 41, // 78: resource.BlobStore.PutBlob:output_type -> resource.PutBlobResponse - 43, // 79: resource.BlobStore.GetBlob:output_type -> resource.GetBlobResponse - 34, // 80: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse - 67, // [67:81] is the sub-list for method output_type - 53, // [53:67] is the sub-list for method input_type - 53, // [53:53] is the sub-list for extension type_name - 53, // [53:53] is the sub-list for extension extendee - 0, // [0:53] is the sub-list for field type_name + 47, // 20: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource + 47, // 21: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource + 7, // 22: resource.BatchRequest.key:type_name -> resource.ResourceKey + 3, // 23: resource.BatchRequest.action:type_name -> resource.BatchRequest.Action + 9, // 24: resource.BatchResponse.error:type_name -> resource.ErrorResult + 48, // 25: resource.BatchResponse.summary:type_name -> resource.BatchResponse.Summary + 49, // 26: resource.BatchResponse.rejected:type_name -> resource.BatchResponse.Rejected + 9, // 27: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult + 50, // 28: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats + 21, // 29: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions + 7, // 30: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey + 51, // 31: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort + 53, // 32: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry + 9, // 33: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult + 7, // 34: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey + 38, // 35: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable + 56, // 36: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry + 57, // 37: resource.ListRepositoryObjectsResponse.items:type_name -> resource.ListRepositoryObjectsResponse.Item + 9, // 38: resource.ListRepositoryObjectsResponse.error:type_name -> resource.ErrorResult + 58, // 39: resource.CountRepositoryObjectsResponse.items:type_name -> resource.CountRepositoryObjectsResponse.ResourceCount + 9, // 40: resource.CountRepositoryObjectsResponse.error:type_name -> resource.ErrorResult + 4, // 41: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus + 39, // 42: resource.ResourceTable.columns:type_name -> resource.ResourceTableColumnDefinition + 40, // 43: resource.ResourceTable.rows:type_name -> resource.ResourceTableRow + 5, // 44: resource.ResourceTableColumnDefinition.type:type_name -> resource.ResourceTableColumnDefinition.ColumnType + 59, // 45: resource.ResourceTableColumnDefinition.properties:type_name -> resource.ResourceTableColumnDefinition.Properties + 7, // 46: resource.ResourceTableRow.key:type_name -> resource.ResourceKey + 7, // 47: resource.RestoreRequest.key:type_name -> resource.ResourceKey + 9, // 48: resource.RestoreResponse.error:type_name -> resource.ErrorResult + 7, // 49: resource.PutBlobRequest.resource:type_name -> resource.ResourceKey + 6, // 50: resource.PutBlobRequest.method:type_name -> resource.PutBlobRequest.Method + 9, // 51: resource.PutBlobResponse.error:type_name -> resource.ErrorResult + 7, // 52: resource.GetBlobRequest.resource:type_name -> resource.ResourceKey + 9, // 53: resource.GetBlobResponse.error:type_name -> resource.ErrorResult + 7, // 54: resource.BatchResponse.Rejected.key:type_name -> resource.ResourceKey + 3, // 55: resource.BatchResponse.Rejected.action:type_name -> resource.BatchRequest.Action + 52, // 56: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet + 55, // 57: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet + 54, // 58: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet + 7, // 59: resource.ListRepositoryObjectsResponse.Item.object:type_name -> resource.ResourceKey + 18, // 60: resource.ResourceStore.Read:input_type -> resource.ReadRequest + 12, // 61: resource.ResourceStore.Create:input_type -> resource.CreateRequest + 14, // 62: resource.ResourceStore.Update:input_type -> resource.UpdateRequest + 16, // 63: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest + 41, // 64: resource.ResourceStore.Restore:input_type -> resource.RestoreRequest + 22, // 65: resource.ResourceStore.List:input_type -> resource.ListRequest + 24, // 66: resource.ResourceStore.Watch:input_type -> resource.WatchRequest + 26, // 67: resource.BatchStore.BatchProcess:input_type -> resource.BatchRequest + 30, // 68: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest + 28, // 69: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest + 34, // 70: resource.RepositoryIndex.CountRepositoryObjects:input_type -> resource.CountRepositoryObjectsRequest + 32, // 71: resource.RepositoryIndex.ListRepositoryObjects:input_type -> resource.ListRepositoryObjectsRequest + 43, // 72: resource.BlobStore.PutBlob:input_type -> resource.PutBlobRequest + 45, // 73: resource.BlobStore.GetBlob:input_type -> resource.GetBlobRequest + 36, // 74: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest + 19, // 75: resource.ResourceStore.Read:output_type -> resource.ReadResponse + 13, // 76: resource.ResourceStore.Create:output_type -> resource.CreateResponse + 15, // 77: resource.ResourceStore.Update:output_type -> resource.UpdateResponse + 17, // 78: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse + 42, // 79: resource.ResourceStore.Restore:output_type -> resource.RestoreResponse + 23, // 80: resource.ResourceStore.List:output_type -> resource.ListResponse + 25, // 81: resource.ResourceStore.Watch:output_type -> resource.WatchEvent + 27, // 82: resource.BatchStore.BatchProcess:output_type -> resource.BatchResponse + 31, // 83: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse + 29, // 84: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse + 35, // 85: resource.RepositoryIndex.CountRepositoryObjects:output_type -> resource.CountRepositoryObjectsResponse + 33, // 86: resource.RepositoryIndex.ListRepositoryObjects:output_type -> resource.ListRepositoryObjectsResponse + 44, // 87: resource.BlobStore.PutBlob:output_type -> resource.PutBlobResponse + 46, // 88: resource.BlobStore.GetBlob:output_type -> resource.GetBlobResponse + 37, // 89: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse + 75, // [75:90] is the sub-list for method output_type + 60, // [60:75] is the sub-list for method input_type + 60, // [60:60] is the sub-list for extension type_name + 60, // [60:60] is the sub-list for extension extendee + 0, // [0:60] is the sub-list for field type_name } func init() { file_resource_proto_init() } @@ -4338,11 +4770,11 @@ func file_resource_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_resource_proto_rawDesc, - NumEnums: 6, - NumMessages: 49, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_resource_proto_rawDesc), len(file_resource_proto_rawDesc)), + NumEnums: 7, + NumMessages: 53, NumExtensions: 0, - NumServices: 5, + NumServices: 6, }, GoTypes: file_resource_proto_goTypes, DependencyIndexes: file_resource_proto_depIdxs, @@ -4350,7 +4782,6 @@ func file_resource_proto_init() { MessageInfos: file_resource_proto_msgTypes, }.Build() File_resource_proto = out.File - file_resource_proto_rawDesc = nil file_resource_proto_goTypes = nil file_resource_proto_depIdxs = nil } diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index 2624e36dc94..f16f194d360 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -308,7 +308,7 @@ message WatchEvent { // Timestamp the event was sent int64 timestamp = 1; - // Timestamp the event was sent + // The event type Type type = 2; // Resource version for the object @@ -318,6 +318,65 @@ message WatchEvent { Resource previous = 4; } +message BatchRequest { + enum Action { + // will be an error + UNKNOWN = 0; + + // Matches Watch event enum + ADDED = 1; + MODIFIED = 2; + DELETED = 3; + } + + // NOTE everything in the same stream must share the same Namespace/Group/Resource + ResourceKey key = 1; + + // Requested action + Action action = 2; + + // The resource value + bytes value = 3; + + // Hint that a new version will be written on-top of this + string folder = 4; +} + +message BatchResponse { + message Summary { + string namespace = 1; + string group = 2; + string resource = 3; + int64 count = 4; + int64 history = 5; + int64 resource_version = 6; // The max saved RV + + // The previous count + int64 previous_count = 7; + int64 previous_history = 8; + } + + // Collect a few invalid messages + message Rejected { + ResourceKey key = 1; + BatchRequest.Action action = 2; + string error = 3; + } + + // Error details + ErrorResult error = 1; + + // Total events processed + int64 processed = 2; + + // Summary status for the processed values + repeated Summary summary = 3; + + // Rejected + repeated Rejected rejected = 4; +} + + // Get statistics across multiple resources // For these queries, we do not need authorization to see the actual values message ResourceStatsRequest { @@ -754,6 +813,13 @@ service ResourceStore { rpc Watch(WatchRequest) returns (stream WatchEvent); } +service BatchStore { + // Write multiple resources to the same Namespace/Group/Resource + // Events will not be sent until the stream is complete + // Only the *create* permissions is checked + rpc BatchProcess(stream BatchRequest) returns (BatchResponse); +} + // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics service ResourceIndex { diff --git a/pkg/storage/unified/resource/resource_grpc.pb.go b/pkg/storage/unified/resource/resource_grpc.pb.go index cec8f0bfc9c..c8cbb9569a0 100644 --- a/pkg/storage/unified/resource/resource_grpc.pb.go +++ b/pkg/storage/unified/resource/resource_grpc.pb.go @@ -385,6 +385,135 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{ Metadata: "resource.proto", } +const ( + BatchStore_BatchProcess_FullMethodName = "/resource.BatchStore/BatchProcess" +) + +// BatchStoreClient is the client API for BatchStore service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type BatchStoreClient interface { + // Write multiple resources to the same Namespace/Group/Resource + // Events will not be sent until the stream is complete + // Only the *create* permissions is checked + BatchProcess(ctx context.Context, opts ...grpc.CallOption) (BatchStore_BatchProcessClient, error) +} + +type batchStoreClient struct { + cc grpc.ClientConnInterface +} + +func NewBatchStoreClient(cc grpc.ClientConnInterface) BatchStoreClient { + return &batchStoreClient{cc} +} + +func (c *batchStoreClient) BatchProcess(ctx context.Context, opts ...grpc.CallOption) (BatchStore_BatchProcessClient, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &BatchStore_ServiceDesc.Streams[0], BatchStore_BatchProcess_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &batchStoreBatchProcessClient{ClientStream: stream} + return x, nil +} + +type BatchStore_BatchProcessClient interface { + Send(*BatchRequest) error + CloseAndRecv() (*BatchResponse, error) + grpc.ClientStream +} + +type batchStoreBatchProcessClient struct { + grpc.ClientStream +} + +func (x *batchStoreBatchProcessClient) Send(m *BatchRequest) error { + return x.ClientStream.SendMsg(m) +} + +func (x *batchStoreBatchProcessClient) CloseAndRecv() (*BatchResponse, error) { + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + m := new(BatchResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// BatchStoreServer is the server API for BatchStore service. +// All implementations should embed UnimplementedBatchStoreServer +// for forward compatibility +type BatchStoreServer interface { + // Write multiple resources to the same Namespace/Group/Resource + // Events will not be sent until the stream is complete + // Only the *create* permissions is checked + BatchProcess(BatchStore_BatchProcessServer) error +} + +// UnimplementedBatchStoreServer should be embedded to have forward compatible implementations. +type UnimplementedBatchStoreServer struct { +} + +func (UnimplementedBatchStoreServer) BatchProcess(BatchStore_BatchProcessServer) error { + return status.Errorf(codes.Unimplemented, "method BatchProcess not implemented") +} + +// UnsafeBatchStoreServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to BatchStoreServer will +// result in compilation errors. +type UnsafeBatchStoreServer interface { + mustEmbedUnimplementedBatchStoreServer() +} + +func RegisterBatchStoreServer(s grpc.ServiceRegistrar, srv BatchStoreServer) { + s.RegisterService(&BatchStore_ServiceDesc, srv) +} + +func _BatchStore_BatchProcess_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(BatchStoreServer).BatchProcess(&batchStoreBatchProcessServer{ServerStream: stream}) +} + +type BatchStore_BatchProcessServer interface { + SendAndClose(*BatchResponse) error + Recv() (*BatchRequest, error) + grpc.ServerStream +} + +type batchStoreBatchProcessServer struct { + grpc.ServerStream +} + +func (x *batchStoreBatchProcessServer) SendAndClose(m *BatchResponse) error { + return x.ServerStream.SendMsg(m) +} + +func (x *batchStoreBatchProcessServer) Recv() (*BatchRequest, error) { + m := new(BatchRequest) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// BatchStore_ServiceDesc is the grpc.ServiceDesc for BatchStore service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var BatchStore_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "resource.BatchStore", + HandlerType: (*BatchStoreServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "BatchProcess", + Handler: _BatchStore_BatchProcess_Handler, + ClientStreams: true, + }, + }, + Metadata: "resource.proto", +} + const ( ResourceIndex_Search_FullMethodName = "/resource.ResourceIndex/Search" ResourceIndex_GetStats_FullMethodName = "/resource.ResourceIndex/GetStats" diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index 2f32e5cb440..74d89b8bdb3 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -107,6 +107,9 @@ func newSearchSupport(opts SearchOptions, storage StorageBackend, access types.A if opts.Backend == nil { return nil, nil } + if tracer == nil { + return nil, fmt.Errorf("missing tracer") + } if opts.WorkerThreads < 1 { opts.WorkerThreads = 1 @@ -384,6 +387,11 @@ func (s *searchSupport) init(ctx context.Context) error { for { v := <-events + // Skip events during batch updates + if v.PreviousRV < 0 { + continue + } + s.handleEvent(watchctx, v) } }() @@ -496,7 +504,7 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso if idx == nil { idx, _, err = s.build(ctx, key, 10, 0) // unknown size and RV if err != nil { - return nil, err + return nil, fmt.Errorf("error building search index, %w", err) } if idx == nil { return nil, fmt.Errorf("nil index after build") @@ -541,7 +549,8 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size // Convert it to an indexable document doc, err := builder.BuildDocument(ctx, key, iter.ResourceVersion(), iter.Value()) if err != nil { - return err + s.log.Error("error building search document", "key", key.SearchID(), "err", err) + continue } // And finally write it to the index diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 58d555bd429..48989c6808b 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -26,6 +26,7 @@ import ( // ResourceServer implements all gRPC services type ResourceServer interface { ResourceStoreServer + BatchStoreServer ResourceIndexServer RepositoryIndexServer BlobStoreServer @@ -260,7 +261,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { err := s.Init(ctx) if err != nil { - s.log.Error("error initializing resource server", "error", err) + s.log.Error("resource server init failed", "error", err) return nil, err } @@ -314,7 +315,7 @@ func (s *server) Init(ctx context.Context) error { } if s.initErr != nil { - s.log.Error("error initializing resource server", "error", s.initErr) + s.log.Error("error running resource server init", "error", s.initErr) } }) return s.initErr @@ -921,6 +922,12 @@ func (s *server) initWatcher() error { for { // pipe all events v := <-events + + // Skip events during batch updates + if v.PreviousRV < 0 { + continue + } + s.log.Debug("Server. Streaming Event", "type", v.Type, "previousRV", v.PreviousRV, "group", v.Key.Group, "namespace", v.Key.Namespace, "resource", v.Key.Resource, "name", v.Key.Name) s.mostRecentRV.Store(v.ResourceVersion) out <- v diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 1690d129a77..7a7d9990d6a 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -59,6 +59,7 @@ func NewBackend(opts BackendOptions) (Backend, error) { tracer: opts.Tracer, dbProvider: opts.DBProvider, pollingInterval: pollingInterval, + batchLock: &batchLock{running: make(map[string]bool)}, }, nil } @@ -77,6 +78,7 @@ type backend struct { dbProvider db.DBProvider db db.DB dialect sqltemplate.Dialect + batchLock *batchLock // watch streaming //stream chan *resource.WatchEvent @@ -701,7 +703,7 @@ func (b *backend) WatchWriteEvents(ctx context.Context) (<-chan *resource.Writte // Get the latest RV since, err := b.listLatestRVs(ctx) if err != nil { - return nil, fmt.Errorf("get the latest resource version: %w", err) + return nil, fmt.Errorf("watch, get latest resource version: %w", err) } // Start the poller stream := make(chan *resource.WrittenEvent) @@ -713,17 +715,23 @@ func (b *backend) poller(ctx context.Context, since groupResourceRV, stream chan t := time.NewTicker(b.pollingInterval) defer close(stream) defer t.Stop() + isSQLite := b.dialect.DialectName() == "sqlite" for { select { case <-b.done: return case <-t.C: + // Block polling duffing import to avoid database locked issues + if isSQLite && b.batchLock.Active() { + continue + } + ctx, span := b.tracer.Start(ctx, tracePrefix+"poller") // List the latest RVs grv, err := b.listLatestRVs(ctx) if err != nil { - b.log.Error("get the latest resource version", "err", err) + b.log.Error("poller get latest resource version", "err", err) t.Reset(b.pollingInterval) continue } diff --git a/pkg/storage/unified/sql/batch.go b/pkg/storage/unified/sql/batch.go new file mode 100644 index 00000000000..75221c98b32 --- /dev/null +++ b/pkg/storage/unified/sql/batch.go @@ -0,0 +1,338 @@ +package sql + +import ( + "context" + "fmt" + "net/http" + "os" + "sync" + "time" + + "github.com/google/uuid" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/storage/unified/parquet" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/sql/db" + "github.com/grafana/grafana/pkg/storage/unified/sql/dbutil" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" +) + +var ( + _ resource.BatchProcessingBackend = (*backend)(nil) +) + +type batchRV struct { + max int64 + counter int64 +} + +func newBatchRV() *batchRV { + t := time.Now().Truncate(time.Second * 10) + return &batchRV{ + max: (t.UnixMicro() / 10000000) * 10000000, + counter: 0, + } +} + +func (x *batchRV) next(obj metav1.Object) int64 { + ts := obj.GetCreationTimestamp().UnixMicro() + anno := obj.GetAnnotations() + if anno != nil { + v := anno[utils.AnnoKeyUpdatedTimestamp] + t, err := time.Parse(time.RFC3339, v) + if err == nil { + ts = t.UnixMicro() + } + } + if ts > x.max || ts < 10000000 { + ts = x.max + } + x.counter++ + return (ts/10000000)*10000000 + x.counter +} + +type batchLock struct { + running map[string]bool + mu sync.Mutex +} + +func (x *batchLock) Start(keys []*resource.ResourceKey) error { + x.mu.Lock() + defer x.mu.Unlock() + + // First verify that it is not already running + ids := make([]string, len(keys)) + for i, k := range keys { + id := k.BatchID() + if x.running[id] { + return &apierrors.StatusError{ErrStatus: metav1.Status{ + Code: http.StatusPreconditionFailed, + Message: "batch export is already running", + }} + } + ids[i] = id + } + + // Then add the keys to the lock + for _, k := range ids { + x.running[k] = true + } + return nil +} + +func (x *batchLock) Finish(keys []*resource.ResourceKey) { + x.mu.Lock() + defer x.mu.Unlock() + for _, k := range keys { + delete(x.running, k.BatchID()) + } +} + +func (x *batchLock) Active() bool { + x.mu.Lock() + defer x.mu.Unlock() + return len(x.running) > 0 +} + +func (b *backend) ProcessBatch(ctx context.Context, setting resource.BatchSettings, iter resource.BatchRequestIterator) *resource.BatchResponse { + err := b.batchLock.Start(setting.Collection) + if err != nil { + return &resource.BatchResponse{ + Error: resource.AsErrorResult(err), + } + } + defer b.batchLock.Finish(setting.Collection) + + // We may want to first write parquet, then read parquet + if b.dialect.DialectName() == "sqlite" { + file, err := os.CreateTemp("", "grafana-batch-export-*.parquet") + if err != nil { + return &resource.BatchResponse{ + Error: resource.AsErrorResult(err), + } + } + + writer, err := parquet.NewParquetWriter(file) + if err != nil { + return &resource.BatchResponse{ + Error: resource.AsErrorResult(err), + } + } + + // write batch to parquet + rsp := writer.ProcessBatch(ctx, setting, iter) + if rsp.Error != nil { + return rsp + } + + b.log.Info("using parquet buffer", "parquet", file) + + // Replace the iterator with one from parquet + iter, err = parquet.NewParquetReader(file.Name(), 50) + if err != nil { + return &resource.BatchResponse{ + Error: resource.AsErrorResult(err), + } + } + } + + return b.processBatch(ctx, setting, iter) +} + +// internal batch process +func (b *backend) processBatch(ctx context.Context, setting resource.BatchSettings, iter resource.BatchRequestIterator) *resource.BatchResponse { + rsp := &resource.BatchResponse{} + err := b.db.WithTx(ctx, ReadCommitted, func(ctx context.Context, tx db.Tx) error { + rollbackWithError := func(err error) error { + txerr := tx.Rollback() + if txerr != nil { + b.log.Warn("rollback", "error", txerr) + } else { + b.log.Info("rollback") + } + return err + } + batch := &batchWroker{ + ctx: ctx, + tx: tx, + dialect: b.dialect, + logger: logging.FromContext(ctx), + } + + // Calculate the RV based on incoming request timestamps + rv := newBatchRV() + + summaries := make(map[string]*resource.BatchResponse_Summary, len(setting.Collection)*4) + + // First clear everything in the transaction + if setting.RebuildCollection { + for _, key := range setting.Collection { + summary, err := batch.deleteCollection(key) + if err != nil { + return rollbackWithError(err) + } + summaries[key.BatchID()] = summary + rsp.Summary = append(rsp.Summary, summary) + } + } + + obj := &unstructured.Unstructured{} + + // Write each event into the history + for iter.Next() { + if iter.RollbackRequested() { + return rollbackWithError(nil) + } + req := iter.Request() + if req == nil { + return rollbackWithError(fmt.Errorf("missing request")) + } + rsp.Processed++ + + if req.Action == resource.BatchRequest_UNKNOWN { + rsp.Rejected = append(rsp.Rejected, &resource.BatchResponse_Rejected{ + Key: req.Key, + Action: req.Action, + Error: "unknown action", + }) + continue + } + + err := obj.UnmarshalJSON(req.Value) + if err != nil { + rsp.Rejected = append(rsp.Rejected, &resource.BatchResponse_Rejected{ + Key: req.Key, + Action: req.Action, + Error: "unable to unmarshal json", + }) + continue + } + + // Write the event to history + if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryInsert, sqlResourceRequest{ + SQLTemplate: sqltemplate.New(b.dialect), + WriteEvent: resource.WriteEvent{ + Key: req.Key, + Type: resource.WatchEvent_Type(req.Action), + Value: req.Value, + PreviousRV: -1, // Used for WATCH, but we want to skip watch events + }, + Folder: req.Folder, + GUID: uuid.NewString(), + ResourceVersion: rv.next(obj), + }); err != nil { + return rollbackWithError(fmt.Errorf("insert into resource history: %w", err)) + } + } + + // Now update the resource table from history + for _, key := range setting.Collection { + k := fmt.Sprintf("%s/%s/%s", key.Namespace, key.Group, key.Resource) + summary := summaries[k] + if summary == nil { + return rollbackWithError(fmt.Errorf("missing summary key for: %s", k)) + } + + err := batch.syncCollection(key, summary) + if err != nil { + return err + } + + // Make sure the collection RV is above our last written event + _, err = b.resourceVersionAtomicInc(ctx, tx, key) + if err != nil { + b.log.Warn("error increasing RV", "error", err) + } + } + return nil + }) + if err != nil { + rsp.Error = resource.AsErrorResult(err) + } + return rsp +} + +type batchWroker struct { + ctx context.Context + tx db.ContextExecer + dialect sqltemplate.Dialect + logger logging.Logger +} + +// This will remove everything from the `resource` and `resource_history` table for a given namespace/group/resource +func (w *batchWroker) deleteCollection(key *resource.ResourceKey) (*resource.BatchResponse_Summary, error) { + summary := &resource.BatchResponse_Summary{ + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + } + + // First delete history + res, err := dbutil.Exec(w.ctx, w.tx, sqlResourceHistoryDelete, &sqlResourceHistoryDeleteRequest{ + SQLTemplate: sqltemplate.New(w.dialect), + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + }) + if err != nil { + return nil, err + } + + summary.PreviousHistory, err = res.RowsAffected() + if err != nil { + return nil, err + } + + // Next delete the active resource table + res, err = dbutil.Exec(w.ctx, w.tx, sqlResourceDelete, &sqlResourceRequest{ + SQLTemplate: sqltemplate.New(w.dialect), + WriteEvent: resource.WriteEvent{ + Key: key, + }, + }) + if err != nil { + return nil, err + } + summary.PreviousCount, err = res.RowsAffected() + return summary, err +} + +// Copy the latest value from history into the active resource table +func (w *batchWroker) syncCollection(key *resource.ResourceKey, summary *resource.BatchResponse_Summary) error { + w.logger.Info("synchronize collection", "key", key.BatchID()) + _, err := dbutil.Exec(w.ctx, w.tx, sqlResourceInsertFromHistory, &sqlResourceInsertFromHistoryRequest{ + SQLTemplate: sqltemplate.New(w.dialect), + Key: key, + }) + if err != nil { + return err + } + + w.logger.Info("get stats (still in transaction)", "key", key.BatchID()) + rows, err := dbutil.QueryRows(w.ctx, w.tx, sqlResourceStats, &sqlStatsRequest{ + SQLTemplate: sqltemplate.New(w.dialect), + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + }) + if err != nil { + return err + } + if rows != nil { + defer func() { + _ = rows.Close() + }() + } + if rows.Next() { + row := resource.ResourceStats{} + return rows.Scan(&row.Namespace, &row.Group, &row.Resource, + &summary.Count, + &summary.ResourceVersion) + } + return err +} diff --git a/pkg/storage/unified/sql/batch_test.go b/pkg/storage/unified/sql/batch_test.go new file mode 100644 index 00000000000..789f9922a12 --- /dev/null +++ b/pkg/storage/unified/sql/batch_test.go @@ -0,0 +1,24 @@ +package sql + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestBatch(t *testing.T) { + t.Parallel() + + t.Run("rv iterator", func(t *testing.T) { + t.Parallel() + + rv := newBatchRV() + v0 := rv.next(&unstructured.Unstructured{}) + v1 := rv.next(&unstructured.Unstructured{}) + v2 := rv.next(&unstructured.Unstructured{}) + require.True(t, v0 > 1000) + require.Equal(t, int64(1), v1-v0) + require.Equal(t, int64(1), v2-v1) + }) +} diff --git a/pkg/storage/unified/sql/data/resource_insert_from_history.sql b/pkg/storage/unified/sql/data/resource_insert_from_history.sql new file mode 100644 index 00000000000..2a5f434efd3 --- /dev/null +++ b/pkg/storage/unified/sql/data/resource_insert_from_history.sql @@ -0,0 +1,52 @@ +INSERT INTO {{ .Ident "resource" }} +SELECT + kv.{{ .Ident "guid" }}, + kv.{{ .Ident "resource_version" }}, + kv.{{ .Ident "group" }}, + kv.{{ .Ident "resource" }}, + kv.{{ .Ident "namespace" }}, + kv.{{ .Ident "name" }}, + kv.{{ .Ident "value" }}, + kv.{{ .Ident "action" }}, + kv.{{ .Ident "label_set" }}, + kv.{{ .Ident "previous_resource_version" }}, + kv.{{ .Ident "folder" }} +FROM {{ .Ident "resource_history" }} AS kv + INNER JOIN ( + SELECT {{ .Ident "namespace" }}, {{ .Ident "group" }}, {{ .Ident "resource" }}, {{ .Ident "name" }}, max({{ .Ident "resource_version" }}) AS {{ .Ident "resource_version" }} + FROM {{ .Ident "resource_history" }} AS mkv + WHERE 1 = 1 + {{ if .Key.Namespace }} + AND {{ .Ident "namespace" }} = {{ .Arg .Key.Namespace }} + {{ end }} + {{ if .Key.Group }} + AND {{ .Ident "group" }} = {{ .Arg .Key.Group }} + {{ end }} + {{ if .Key.Resource }} + AND {{ .Ident "resource" }} = {{ .Arg .Key.Resource }} + {{ end }} + {{ if .Key.Name }} + AND {{ .Ident "name" }} = {{ .Arg .Key.Name }} + {{ end }} + GROUP BY mkv.{{ .Ident "namespace" }}, mkv.{{ .Ident "group" }}, mkv.{{ .Ident "resource" }}, mkv.{{ .Ident "name" }} + ) AS maxkv + ON maxkv.{{ .Ident "resource_version" }} = kv.{{ .Ident "resource_version" }} + AND maxkv.{{ .Ident "namespace" }} = kv.{{ .Ident "namespace" }} + AND maxkv.{{ .Ident "group" }} = kv.{{ .Ident "group" }} + AND maxkv.{{ .Ident "resource" }} = kv.{{ .Ident "resource" }} + AND maxkv.{{ .Ident "name" }} = kv.{{ .Ident "name" }} + WHERE kv.{{ .Ident "action" }} != 3 + {{ if .Key.Namespace }} + AND kv.{{ .Ident "namespace" }} = {{ .Arg .Key.Namespace }} + {{ end }} + {{ if .Key.Group }} + AND kv.{{ .Ident "group" }} = {{ .Arg .Key.Group }} + {{ end }} + {{ if .Key.Resource }} + AND kv.{{ .Ident "resource" }} = {{ .Arg .Key.Resource }} + {{ end }} + {{ if .Key.Name }} + AND kv.{{ .Ident "name" }} = {{ .Arg .Key.Name }} + {{ end }} + ORDER BY kv.{{ .Ident "resource_version" }} ASC +; diff --git a/pkg/storage/unified/sql/queries.go b/pkg/storage/unified/sql/queries.go index 0641791fa7f..b91b17791f5 100644 --- a/pkg/storage/unified/sql/queries.go +++ b/pkg/storage/unified/sql/queries.go @@ -44,6 +44,7 @@ var ( sqlResourceHistoryPoll = mustTemplate("resource_history_poll.sql") sqlResourceHistoryGet = mustTemplate("resource_history_get.sql") sqlResourceHistoryDelete = mustTemplate("resource_history_delete.sql") + sqlResourceInsertFromHistory = mustTemplate("resource_insert_from_history.sql") // sqlResourceLabelsInsert = mustTemplate("resource_labels_insert.sql") sqlResourceVersionGet = mustTemplate("resource_version_get.sql") @@ -83,6 +84,18 @@ func (r sqlResourceRequest) Validate() error { return nil // TODO } +type sqlResourceInsertFromHistoryRequest struct { + sqltemplate.SQLTemplate + Key *resource.ResourceKey +} + +func (r sqlResourceInsertFromHistoryRequest) Validate() error { + if r.Key == nil { + return fmt.Errorf("missing key") + } + return nil +} + type sqlStatsRequest struct { sqltemplate.SQLTemplate Namespace string diff --git a/pkg/storage/unified/sql/queries_test.go b/pkg/storage/unified/sql/queries_test.go index ee6e0f45537..e14eaef18d5 100644 --- a/pkg/storage/unified/sql/queries_test.go +++ b/pkg/storage/unified/sql/queries_test.go @@ -385,5 +385,18 @@ func TestUnifiedStorageQueries(t *testing.T) { }, }, }, + sqlResourceInsertFromHistory: { + { + Name: "update", + Data: &sqlResourceInsertFromHistoryRequest{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Key: &resource.ResourceKey{ + Namespace: "default", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + }, + }, + }, }}) } diff --git a/pkg/storage/unified/sql/service.go b/pkg/storage/unified/sql/service.go index 6c05f884b27..6ae039d1c7f 100644 --- a/pkg/storage/unified/sql/service.go +++ b/pkg/storage/unified/sql/service.go @@ -126,7 +126,9 @@ func (s *service) start(ctx context.Context) error { srv := s.handler.GetServer() resource.RegisterResourceStoreServer(srv, server) + resource.RegisterBatchStoreServer(srv, server) resource.RegisterResourceIndexServer(srv, server) + resource.RegisterRepositoryIndexServer(srv, server) resource.RegisterBlobStoreServer(srv, server) resource.RegisterDiagnosticsServer(srv, server) grpc_health_v1.RegisterHealthServer(srv, healthService) diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_insert_from_history-update.sql b/pkg/storage/unified/sql/testdata/mysql--resource_insert_from_history-update.sql new file mode 100755 index 00000000000..a177c394bff --- /dev/null +++ b/pkg/storage/unified/sql/testdata/mysql--resource_insert_from_history-update.sql @@ -0,0 +1,34 @@ +INSERT INTO `resource` +SELECT + kv.`guid`, + kv.`resource_version`, + kv.`group`, + kv.`resource`, + kv.`namespace`, + kv.`name`, + kv.`value`, + kv.`action`, + kv.`label_set`, + kv.`previous_resource_version`, + kv.`folder` +FROM `resource_history` AS kv + INNER JOIN ( + SELECT `namespace`, `group`, `resource`, `name`, max(`resource_version`) AS `resource_version` + FROM `resource_history` AS mkv + WHERE 1 = 1 + AND `namespace` = 'default' + AND `group` = 'dashboard.grafana.app' + AND `resource` = 'dashboards' + GROUP BY mkv.`namespace`, mkv.`group`, mkv.`resource`, mkv.`name` + ) AS maxkv + ON maxkv.`resource_version` = kv.`resource_version` + AND maxkv.`namespace` = kv.`namespace` + AND maxkv.`group` = kv.`group` + AND maxkv.`resource` = kv.`resource` + AND maxkv.`name` = kv.`name` + WHERE kv.`action` != 3 + AND kv.`namespace` = 'default' + AND kv.`group` = 'dashboard.grafana.app' + AND kv.`resource` = 'dashboards' + ORDER BY kv.`resource_version` ASC +; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_insert_from_history-update.sql b/pkg/storage/unified/sql/testdata/postgres--resource_insert_from_history-update.sql new file mode 100755 index 00000000000..e6439c98074 --- /dev/null +++ b/pkg/storage/unified/sql/testdata/postgres--resource_insert_from_history-update.sql @@ -0,0 +1,34 @@ +INSERT INTO "resource" +SELECT + kv."guid", + kv."resource_version", + kv."group", + kv."resource", + kv."namespace", + kv."name", + kv."value", + kv."action", + kv."label_set", + kv."previous_resource_version", + kv."folder" +FROM "resource_history" AS kv + INNER JOIN ( + SELECT "namespace", "group", "resource", "name", max("resource_version") AS "resource_version" + FROM "resource_history" AS mkv + WHERE 1 = 1 + AND "namespace" = 'default' + AND "group" = 'dashboard.grafana.app' + AND "resource" = 'dashboards' + GROUP BY mkv."namespace", mkv."group", mkv."resource", mkv."name" + ) AS maxkv + ON maxkv."resource_version" = kv."resource_version" + AND maxkv."namespace" = kv."namespace" + AND maxkv."group" = kv."group" + AND maxkv."resource" = kv."resource" + AND maxkv."name" = kv."name" + WHERE kv."action" != 3 + AND kv."namespace" = 'default' + AND kv."group" = 'dashboard.grafana.app' + AND kv."resource" = 'dashboards' + ORDER BY kv."resource_version" ASC +; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_insert_from_history-update.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_insert_from_history-update.sql new file mode 100755 index 00000000000..e6439c98074 --- /dev/null +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_insert_from_history-update.sql @@ -0,0 +1,34 @@ +INSERT INTO "resource" +SELECT + kv."guid", + kv."resource_version", + kv."group", + kv."resource", + kv."namespace", + kv."name", + kv."value", + kv."action", + kv."label_set", + kv."previous_resource_version", + kv."folder" +FROM "resource_history" AS kv + INNER JOIN ( + SELECT "namespace", "group", "resource", "name", max("resource_version") AS "resource_version" + FROM "resource_history" AS mkv + WHERE 1 = 1 + AND "namespace" = 'default' + AND "group" = 'dashboard.grafana.app' + AND "resource" = 'dashboards' + GROUP BY mkv."namespace", mkv."group", mkv."resource", mkv."name" + ) AS maxkv + ON maxkv."resource_version" = kv."resource_version" + AND maxkv."namespace" = kv."namespace" + AND maxkv."group" = kv."group" + AND maxkv."resource" = kv."resource" + AND maxkv."name" = kv."name" + WHERE kv."action" != 3 + AND kv."namespace" = 'default' + AND kv."group" = 'dashboard.grafana.app' + AND kv."resource" = 'dashboards' + ORDER BY kv."resource_version" ASC +; From ccb442558fdea77c97cfb58d8d45834a3079d902 Mon Sep 17 00:00:00 2001 From: Charandas <542168+charandas@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:00:15 -0800 Subject: [PATCH 506/894] chore: fix dockerfile for local development (#99721) --- Dockerfile | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index e3472f9ab19..d1757581938 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,24 +62,24 @@ COPY go.* ./ COPY .bingo .bingo # Include vendored dependencies -COPY pkg/util/xorm/go.* pkg/util/xorm/ -COPY pkg/apiserver/go.* pkg/apiserver/ -COPY pkg/apimachinery/go.* pkg/apimachinery/ -COPY pkg/build/go.* pkg/build/ -COPY pkg/build/wire/go.* pkg/build/wire/ -COPY pkg/promlib/go.* pkg/promlib/ -COPY pkg/storage/unified/resource/go.* pkg/storage/unified/resource/ -COPY pkg/storage/unified/apistore/go.* pkg/storage/unified/apistore/ -COPY pkg/semconv/go.* pkg/semconv/ -COPY pkg/aggregator/go.* pkg/aggregator/ -COPY apps/playlist/go.* apps/playlist/ -COPY apps/investigation/go.* apps/investigation/ -COPY apps/advisor/go.* apps/advisor/ +COPY pkg/util/xorm pkg/util/xorm +COPY pkg/apiserver pkg/apiserver +COPY pkg/apimachinery pkg/apimachinery +COPY pkg/build pkg/build +COPY pkg/build/wire pkg/build/wire +COPY pkg/promlib pkg/promlib +COPY pkg/storage/unified/resource pkg/storage/unified/resource +COPY pkg/storage/unified/apistore pkg/storage/unified/apistore +COPY pkg/semconv pkg/semconv +COPY pkg/aggregator pkg/aggregator +COPY apps/playlist apps/playlist +COPY apps/investigation apps/investigation +COPY apps/advisor apps/advisor COPY apps apps COPY kindsv2 kindsv2 -COPY apps/alerting/notifications/go.* apps/alerting/notifications/ -COPY pkg/codegen/go.* pkg/codegen/ -COPY pkg/plugins/codegen/go.* pkg/plugins/codegen/ +COPY apps/alerting/notifications apps/alerting/notifications +COPY pkg/codegen pkg/codegen +COPY pkg/plugins/codegen pkg/plugins/codegen RUN go mod download RUN if [[ "$BINGO" = "true" ]]; then \ From ab74852fc9ec852b1b8e4dc5f750c476f705a64f Mon Sep 17 00:00:00 2001 From: Staton Hysell <101748563+staton-hyse11@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:16:19 -0500 Subject: [PATCH 507/894] PanelEdit: Align chevron direction (#100386) Adjusted the collapsable section chevron + point down when closed + point up when expanded --- .../dashboard/components/PanelEditor/OptionsPaneCategory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx index 2540fe59a60..8e284c59e11 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx @@ -131,7 +131,7 @@ export const OptionsPaneCategory = React.memo( variant="secondary" aria-expanded={isExpanded} className={styles.toggleButton} - icon={isExpanded ? 'angle-down' : 'angle-up'} + icon={isExpanded ? 'angle-up' : 'angle-down'} onClick={onToggle} />
    From df84d928e2d5d9e4e8d2ba4aed1c0c104b5f2e26 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 11 Feb 2025 12:14:25 -0700 Subject: [PATCH 508/894] K8s: Folders: Fix legacy search (#100393) --- pkg/api/dashboard_test.go | 5 +- pkg/api/folder_bench_test.go | 3 +- pkg/registry/apis/dashboard/search.go | 10 +- pkg/registry/apis/dashboard/search_test.go | 67 ++- .../apis/dashboard/v0alpha1/register.go | 2 +- .../accesscontrol/accesscontrol_test.go | 3 +- .../annotationsimpl/annotations_test.go | 5 +- pkg/services/apiserver/client/client.go | 36 +- pkg/services/apiserver/client/client_mock.go | 9 + .../dashboards/service/dashboard_service.go | 4 +- .../dashboard_service_integration_test.go | 16 +- .../service/service_test.go | 4 +- .../dashboardversion/dashverimpl/dashver.go | 3 +- pkg/services/folder/folderimpl/folder.go | 33 +- pkg/services/folder/folderimpl/folder_test.go | 10 +- .../folderimpl/folder_unifiedstorage.go | 97 +-- .../folderimpl/folder_unifiedstorage_test.go | 550 ++++++++---------- .../folder/folderimpl/unifiedstore.go | 183 +----- .../libraryelements/libraryelements_test.go | 8 +- .../librarypanels/librarypanels_test.go | 5 +- pkg/services/ngalert/testutil/testutil.go | 3 +- .../publicdashboards/api/query_test.go | 3 +- .../publicdashboards/service/service_test.go | 3 +- pkg/services/quota/quotaimpl/quota_test.go | 3 +- pkg/storage/unified/resource/search_client.go | 10 +- 25 files changed, 415 insertions(+), 660 deletions(-) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index a08deefb4bb..36a329e5faa 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -30,6 +30,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/actest" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + "github.com/grafana/grafana/pkg/services/apiserver/client" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -835,7 +836,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr if dashboardService == nil { dashboardService, err = service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, - ac, folderSvc, fStore, nil, nil, nil, nil, quotaService, nil, nil, + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, ) require.NoError(t, err) dashboardService.(dashboards.PermissionsRegistrationService).RegisterDashboardPermissions(dashboardPermissions) @@ -843,7 +844,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr dashboardProvisioningService, err := service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, - ac, folderSvc, fStore, nil, nil, nil, nil, quotaService, nil, nil, + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, ) require.NoError(t, err) diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index d6645c1b945..42fc5d52d55 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" @@ -472,7 +473,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl( sc.cfg, dashStore, folderStore, features, folderPermissions, ac, - folderServiceWithFlagOn, fStore, nil, nil, nil, nil, quotaSrv, nil, nil, + folderServiceWithFlagOn, fStore, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil, ) require.NoError(b, err) diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 824243692a1..c4ad9532066 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -1,6 +1,7 @@ package dashboard import ( + "context" "encoding/json" "net/http" "net/url" @@ -9,6 +10,7 @@ import ( "strconv" "strings" + "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/storage/unified/search" "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -32,12 +34,12 @@ import ( // The DTO returns everything the UI needs in a single request type SearchHandler struct { log log.Logger - client resource.ResourceIndexClient + client func(context.Context) resource.ResourceIndexClient tracer trace.Tracer } -func NewSearchHandler(client resource.ResourceIndexClient, tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient) *SearchHandler { - searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, client, legacyDashboardSearcher) +func NewSearchHandler(tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient) *SearchHandler { + searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, unified.GetResourceClient, legacyDashboardSearcher) return &SearchHandler{ client: searchClient, log: log.New("grafana-apiserver.dashboards.search"), @@ -339,7 +341,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { searchRequest.Options.Fields = append(searchRequest.Options.Fields, namesFilter...) } - result, err := s.client.Search(ctx, searchRequest) + result, err := s.client(ctx).Search(ctx, searchRequest) if err != nil { errhttp.Write(ctx, err, w) return diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index d48a6e7a802..91af78295a3 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -23,6 +23,7 @@ import ( func TestSearchFallback(t *testing.T) { t.Run("should hit legacy search handler on mode 0", func(t *testing.T) { mockClient := &MockClient{} + mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -30,7 +31,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode0}, }, } - searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -49,6 +51,7 @@ func TestSearchFallback(t *testing.T) { t.Run("should hit legacy search handler on mode 1", func(t *testing.T) { mockClient := &MockClient{} + mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -56,7 +59,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode1}, }, } - searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -75,6 +79,7 @@ func TestSearchFallback(t *testing.T) { t.Run("should hit legacy search handler on mode 2", func(t *testing.T) { mockClient := &MockClient{} + mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -82,7 +87,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode2}, }, } - searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -101,6 +107,7 @@ func TestSearchFallback(t *testing.T) { t.Run("should hit unified storage search handler on mode 3", func(t *testing.T) { mockClient := &MockClient{} + mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -108,7 +115,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode3}, }, } - searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -127,6 +135,7 @@ func TestSearchFallback(t *testing.T) { t.Run("should hit unified storage search handler on mode 4", func(t *testing.T) { mockClient := &MockClient{} + mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -134,7 +143,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode4}, }, } - searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -153,6 +163,7 @@ func TestSearchFallback(t *testing.T) { t.Run("should hit unified storage search handler on mode 5", func(t *testing.T) { mockClient := &MockClient{} + mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -160,7 +171,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode5}, }, } - searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -185,7 +197,7 @@ func TestSearchHandler(t *testing.T) { // Initialize the search handler with the mock client searchHandler := SearchHandler{ log: log.New("test", "test"), - client: mockClient, + client: func(context.Context) resource.ResourceIndexClient { return mockClient }, tracer: tracing.NewNoopTracerService(), } @@ -271,6 +283,7 @@ func TestSearchHandler(t *testing.T) { // MockClient implements the ResourceIndexClient interface for testing type MockClient struct { resource.ResourceIndexClient + resource.ResourceIndex // Capture the last SearchRequest for assertions LastSearchRequest *resource.ResourceSearchRequest @@ -330,7 +343,45 @@ func (m *MockClient) Search(ctx context.Context, in *resource.ResourceSearchRequ }, }, nil } - func (m *MockClient) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) { return nil, nil } +func (m *MockClient) CountRepositoryObjects(ctx context.Context, in *resource.CountRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.CountRepositoryObjectsResponse, error) { + return nil, nil +} +func (m *MockClient) Watch(ctx context.Context, in *resource.WatchRequest, opts ...grpc.CallOption) (resource.ResourceStore_WatchClient, error) { + return nil, nil +} +func (m *MockClient) Delete(ctx context.Context, in *resource.DeleteRequest, opts ...grpc.CallOption) (*resource.DeleteResponse, error) { + return nil, nil +} +func (m *MockClient) Create(ctx context.Context, in *resource.CreateRequest, opts ...grpc.CallOption) (*resource.CreateResponse, error) { + return nil, nil +} +func (m *MockClient) Update(ctx context.Context, in *resource.UpdateRequest, opts ...grpc.CallOption) (*resource.UpdateResponse, error) { + return nil, nil +} +func (m *MockClient) Read(ctx context.Context, in *resource.ReadRequest, opts ...grpc.CallOption) (*resource.ReadResponse, error) { + return nil, nil +} +func (m *MockClient) Restore(ctx context.Context, in *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) { + return nil, nil +} +func (m *MockClient) GetBlob(ctx context.Context, in *resource.GetBlobRequest, opts ...grpc.CallOption) (*resource.GetBlobResponse, error) { + return nil, nil +} +func (m *MockClient) PutBlob(ctx context.Context, in *resource.PutBlobRequest, opts ...grpc.CallOption) (*resource.PutBlobResponse, error) { + return nil, nil +} +func (m *MockClient) List(ctx context.Context, in *resource.ListRequest, opts ...grpc.CallOption) (*resource.ListResponse, error) { + return nil, nil +} +func (m *MockClient) ListRepositoryObjects(ctx context.Context, in *resource.ListRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.ListRepositoryObjectsResponse, error) { + return nil, nil +} +func (m *MockClient) IsHealthy(ctx context.Context, in *resource.HealthCheckRequest, opts ...grpc.CallOption) (*resource.HealthCheckResponse, error) { + return nil, nil +} +func (m *MockClient) BatchProcess(ctx context.Context, opts ...grpc.CallOption) (resource.BatchStore_BatchProcessClient, error) { + return nil, nil +} diff --git a/pkg/registry/apis/dashboard/v0alpha1/register.go b/pkg/registry/apis/dashboard/v0alpha1/register.go index c75b159961f..c051bb7595f 100644 --- a/pkg/registry/apis/dashboard/v0alpha1/register.go +++ b/pkg/registry/apis/dashboard/v0alpha1/register.go @@ -82,7 +82,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, features: features, accessControl: accessControl, unified: unified, - search: dashboard.NewSearchHandler(unified, tracing, cfg, legacyDashboardSearcher), + search: dashboard.NewSearchHandler(tracing, cfg, legacyDashboardSearcher), legacy: &dashboard.DashboardStorage{ Resource: dashboardv0alpha1.DashboardResourceInfo, diff --git a/pkg/services/annotations/accesscontrol/accesscontrol_test.go b/pkg/services/annotations/accesscontrol/accesscontrol_test.go index 2e982273e28..4070d1d6b5f 100644 --- a/pkg/services/annotations/accesscontrol/accesscontrol_test.go +++ b/pkg/services/annotations/accesscontrol/accesscontrol_test.go @@ -16,6 +16,7 @@ import ( accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/testutil" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardsservice "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -51,7 +52,7 @@ func TestIntegrationAuthorize(t *testing.T) { fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, nil, nil, nil, quotatest.New(false, nil), nil, nil) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil) require.NoError(t, err) dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index 555ebbe4e88..0ac44989ec8 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -19,6 +19,7 @@ import ( accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/testutil" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardsservice "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -63,7 +64,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, nil, nil, nil, quotatest.New(false, nil), nil, nil) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil) require.NoError(t, err) dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) repo := ProvideService(sql, cfg, features, tagService, tracing.InitializeTracerForTest(), ruleStore, dashSvc) @@ -246,7 +247,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sql, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, features, accesscontrolmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, nil, nil, nil, quotatest.New(false, nil), nil, nil) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil) require.NoError(t, err) dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) cfg.AnnotationMaximumTagsLength = 60 diff --git a/pkg/services/apiserver/client/client.go b/pkg/services/apiserver/client/client.go index 9aaf573b7c8..efeee1c573d 100644 --- a/pkg/services/apiserver/client/client.go +++ b/pkg/services/apiserver/client/client.go @@ -12,15 +12,16 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher" - "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/storage/unified/resource" k8sUser "k8s.io/apiserver/pkg/authentication/user" k8sRequest "k8s.io/apiserver/pkg/endpoints/request" @@ -42,22 +43,25 @@ type K8sHandler interface { var _ K8sHandler = (*k8sHandler)(nil) type k8sHandler struct { - namespacer request.NamespaceMapper - gvr schema.GroupVersionResource - restConfigProvider apiserver.RestConfigProvider - searcher resource.ResourceIndexClient - userService user.Service + namespacer request.NamespaceMapper + gvr schema.GroupVersionResource + restConfig func(context.Context) *rest.Config + searcher func(context.Context) resource.ResourceIndexClient + userService user.Service } -func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient, dashStore dashboards.Store, userSvc user.Service) K8sHandler { +func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, + restConfig func(context.Context) *rest.Config, dashStore dashboards.Store, userSvc user.Service) K8sHandler { legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore) - searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, searcher, legacySearcher) + key := gvr.Resource + "." + gvr.Group // the unified storage key in the config.ini is resource + group + searchClient := resource.NewSearchClient(cfg, key, unified.GetResourceClient, legacySearcher) + return &k8sHandler{ - namespacer: namespacer, - gvr: gvr, - restConfigProvider: restConfigProvider, - searcher: searchClient, - userService: userSvc, + namespacer: namespacer, + gvr: gvr, + restConfig: restConfig, + searcher: searchClient, + userService: userSvc, } } @@ -187,12 +191,12 @@ func (h *k8sHandler) Search(ctx context.Context, orgID int64, in *resource.Resou } } - return h.searcher.Search(ctx, in) + return h.searcher(ctx).Search(ctx, in) } func (h *k8sHandler) GetStats(ctx context.Context, orgID int64) (*resource.ResourceStatsResponse, error) { // goes directly through grpc, so doesn't need the new context - return h.searcher.GetStats(ctx, &resource.ResourceStatsRequest{ + return h.searcher(ctx).GetStats(ctx, &resource.ResourceStatsRequest{ Namespace: h.GetNamespace(orgID), Kinds: []string{ h.gvr.Group + "/" + h.gvr.Resource, @@ -223,7 +227,7 @@ func (h *k8sHandler) GetUserFromMeta(ctx context.Context, userMeta string) (*use } func (h *k8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) { - cfg := h.restConfigProvider.GetRestConfig(ctx) + cfg := h.restConfig(ctx) if cfg == nil { return nil, false } diff --git a/pkg/services/apiserver/client/client_mock.go b/pkg/services/apiserver/client/client_mock.go index b5e2fc2eecb..57b6c4183f8 100644 --- a/pkg/services/apiserver/client/client_mock.go +++ b/pkg/services/apiserver/client/client_mock.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/rest" ) var _ K8sHandler = (*MockK8sHandler)(nil) @@ -88,3 +89,11 @@ func (m *MockK8sHandler) GetUserFromMeta(ctx context.Context, userMeta string) ( } return args.Get(0).(*user.User), args.Error(1) } + +type MockTestRestConfig struct { + cfg *rest.Config +} + +func (r MockTestRestConfig) GetRestConfig(ctx context.Context) *rest.Config { + return r.cfg +} diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 9508f9711e6..3964922fcda 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -93,10 +93,10 @@ func ProvideDashboardServiceImpl( cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore folder.FolderStore, features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, ac accesscontrol.AccessControl, folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer, - restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, + restConfigProvider apiserver.RestConfigProvider, userService user.Service, quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper, ) (*DashboardServiceImpl, error) { - k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore, userService) + k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashboardStore, userService) dashSvc := &DashboardServiceImpl{ cfg: cfg, diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index b2deeba68fe..470ba022383 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -904,8 +905,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc folderService, folder.NewFakeStore(), nil, - nil, - nil, + client.MockTestRestConfig{}, nil, quotaService, nil, @@ -993,8 +993,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt folderService, folder.NewFakeStore(), nil, - nil, - nil, + client.MockTestRestConfig{}, nil, quotaService, nil, @@ -1040,8 +1039,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto folderService, folder.NewFakeStore(), nil, - nil, - nil, + client.MockTestRestConfig{}, nil, quotaService, nil, @@ -1106,8 +1104,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string folderService, folder.NewFakeStore(), nil, - nil, - nil, + client.MockTestRestConfig{}, nil, quotaService, nil, @@ -1179,8 +1176,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da folderService, folder.NewFakeStore(), nil, - nil, - nil, + client.MockTestRestConfig{}, nil, quotaService, nil, diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index 48db9813f85..b125e159ed4 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -12,6 +12,7 @@ import ( dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/infra/db" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" dashdb "github.com/grafana/grafana/pkg/services/dashboards/database" dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -110,8 +111,7 @@ func TestValidateDashboardExists(t *testing.T) { foldertest.NewFakeService(), folder.NewFakeStore(), nil, - nil, - nil, + client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 98406b6a7b4..8b83d379f5a 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -52,8 +52,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.Dash cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), - restConfigProvider, - unified, + restConfigProvider.GetRestConfig, dashboardStore, userService, ), diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 9ac7c8a2e1e..0b4dc311fa4 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/dskit/concurrency" "github.com/grafana/grafana/pkg/apimachinery/identity" + dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" @@ -27,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" @@ -42,7 +44,6 @@ import ( "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/util" ) @@ -57,7 +58,8 @@ type Service struct { dashboardFolderStore folder.FolderStore features featuremgmt.FeatureToggles accessControl accesscontrol.AccessControl - k8sclient folderK8sHandler + k8sclient client.K8sHandler + dashboardK8sClient client.K8sHandler publicDashboardService publicdashboards.ServiceWrapper // bus is currently used to publish event in case of folder full path change. // For example when a folder is moved to another folder or when a folder is renamed. @@ -106,13 +108,14 @@ func ProvideService( ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(srv)) if features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) { - k8sHandler := &foldk8sHandler{ - gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(), - namespacer: request.GetNamespaceMapper(cfg), - cfg: cfg, - restConfigProvider: apiserver.GetRestConfig, - recourceClientProvider: unified.GetResourceClient, - } + k8sHandler := client.NewK8sHandler( + cfg, + request.GetNamespaceMapper(cfg), + v0alpha1.FolderResourceInfo.GroupVersionResource(), + apiserver.GetRestConfig, + dashboardStore, + userService, + ) unifiedStore := ProvideUnifiedStore(k8sHandler, userService) @@ -120,6 +123,18 @@ func ProvideService( srv.k8sclient = k8sHandler } + if features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { + dashHandler := client.NewK8sHandler( + cfg, + request.GetNamespaceMapper(cfg), + dashboardalpha1.DashboardResourceInfo.GroupVersionResource(), + apiserver.GetRestConfig, + dashboardStore, + userService, + ) + srv.dashboardK8sClient = dashHandler + } + return srv } diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 988fd4c5d95..87b8787f8fb 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -494,7 +495,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { }) publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) - dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, nil, nil, nil, quotaService, nil, publicDashboardFakeService) + dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService) require.NoError(t, err) dashSrv.RegisterDashboardPermissions(dashboardPermissions) @@ -580,7 +581,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff, - folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, nil, nil, nil, quotaService, nil, publicDashboardFakeService) + folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService) require.NoError(t, err) dashSrv.RegisterDashboardPermissions(dashboardPermissions) alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b) @@ -723,7 +724,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { tc.service.store = nestedFolderStore publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) - dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service, tc.service.store, nil, nil, nil, nil, quotaService, nil, publicDashboardFakeService) + dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service, tc.service.store, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService) require.NoError(t, err) dashSrv.RegisterDashboardPermissions(dashboardPermissions) @@ -1510,8 +1511,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { serviceWithFlagOn, nestedFolderStore, nil, - nil, - nil, + client.MockTestRestConfig{}, nil, quotaService, nil, diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index 203aa4f37ab..94a19aa17b5 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -10,20 +10,15 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "golang.org/x/exp/slices" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/selection" - "k8s.io/client-go/dynamic" - clientrest "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - dashboardv0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" @@ -33,31 +28,12 @@ import ( "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/store/entity" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// interface to allow for testing -type folderK8sHandler interface { - getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) - getDashboardClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) - getNamespace(orgID int64) string - getSearcher(ctx context.Context) resource.ResourceClient -} - -var _ folderK8sHandler = (*foldk8sHandler)(nil) - -type foldk8sHandler struct { - cfg *setting.Cfg - namespacer request.NamespaceMapper - gvr schema.GroupVersionResource - restConfigProvider func(ctx context.Context) *clientrest.Config - recourceClientProvider func(ctx context.Context) resource.ResourceClient -} - func (s *Service) getFoldersFromApiServer(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) { if q.SignedInUser == nil { return nil, folder.ErrBadRequest.Errorf("missing signed in user") @@ -189,7 +165,7 @@ func (s *Service) searchFoldersFromApiServer(ctx context.Context, query folder.S request := &resource.ResourceSearchRequest{ Options: &resource.ListOptions{ Key: &resource.ResourceKey{ - Namespace: s.k8sclient.getNamespace(query.OrgID), + Namespace: s.k8sclient.GetNamespace(query.OrgID), Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, }, @@ -228,9 +204,7 @@ func (s *Service) searchFoldersFromApiServer(ctx context.Context, query folder.S request.Limit = query.Limit } - client := s.k8sclient.getSearcher(ctx) - - res, err := client.Search(ctx, request) + res, err := s.k8sclient.Search(ctx, query.OrgID, request) if err != nil { return nil, err } @@ -279,7 +253,7 @@ func (s *Service) getFolderByIDFromApiServer(ctx context.Context, id int64, orgI } folderkey := &resource.ResourceKey{ - Namespace: s.k8sclient.getNamespace(orgID), + Namespace: s.k8sclient.GetNamespace(orgID), Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, } @@ -298,9 +272,7 @@ func (s *Service) getFolderByIDFromApiServer(ctx context.Context, id int64, orgI }, Limit: 100000} - client := s.k8sclient.getSearcher(ctx) - - res, err := client.Search(ctx, request) + res, err := s.k8sclient.Search(ctx, orgID, request) if err != nil { return nil, err } @@ -334,7 +306,7 @@ func (s *Service) getFolderByTitleFromApiServer(ctx context.Context, orgID int64 } folderkey := &resource.ResourceKey{ - Namespace: s.k8sclient.getNamespace(orgID), + Namespace: s.k8sclient.GetNamespace(orgID), Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, } @@ -362,9 +334,7 @@ func (s *Service) getFolderByTitleFromApiServer(ctx context.Context, orgID int64 request.Options.Fields = append(request.Options.Fields, req...) } - client := s.k8sclient.getSearcher(ctx) - - res, err := client.Search(ctx, request) + res, err := s.k8sclient.Search(ctx, orgID, request) if err != nil { return nil, err } @@ -696,14 +666,8 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol // we cannot use the dashboard service directly due to circular dependencies, // so either use the search client if the feature is enabled or use the dashboard store if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { - dashboardKey := &resource.ResourceKey{ - Namespace: s.k8sclient.getNamespace(cmd.OrgID), - Group: dashboardv0.DashboardResourceInfo.GroupVersionResource().Group, - Resource: dashboardv0.DashboardResourceInfo.GroupVersionResource().Resource, - } request := &resource.ResourceSearchRequest{ Options: &resource.ListOptions{ - Key: dashboardKey, Labels: []*resource.Requirement{}, Fields: []*resource.Requirement{ { @@ -715,8 +679,7 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol }, Limit: 100000} - client := s.k8sclient.getSearcher(ctx) - res, err := client.Search(ctx, request) + res, err := s.dashboardK8sClient.Search(ctx, cmd.OrgID, request) if err != nil { return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err) } @@ -726,13 +689,9 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err) } dashboardUIDs = make([]string, len(hits.Hits)) - k8sDeleteClient, created := s.k8sclient.getDashboardClient(ctx, cmd.OrgID) - if !created { - return folder.ErrInternal.Errorf("failed to create client to get dashboards") - } for i, dashboard := range hits.Hits { dashboardUIDs[i] = dashboard.Name - err = k8sDeleteClient.Delete(ctx, dashboard.Name, metav1.DeleteOptions{}) + err = s.dashboardK8sClient.Delete(ctx, dashboard.Name, cmd.OrgID, metav1.DeleteOptions{}) if err != nil { return folder.ErrInternal.Errorf("failed to delete child dashboard: %w", err) } @@ -989,43 +948,3 @@ func (s *Service) getDescendantCountsFromApiServer(ctx context.Context, q *folde } return countsMap, nil } - -// ----------------------------------------------------------------------------------------- -// Folder k8s functions -// ----------------------------------------------------------------------------------------- - -func (fk8s *foldk8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) { - cfg := fk8s.restConfigProvider(ctx) - if cfg == nil { - return nil, false - } - - dyn, err := dynamic.NewForConfig(cfg) - if err != nil { - return nil, false - } - - return dyn.Resource(fk8s.gvr).Namespace(fk8s.getNamespace(orgID)), true -} - -func (fk8s *foldk8sHandler) getDashboardClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) { - cfg := fk8s.restConfigProvider(ctx) - if cfg == nil { - return nil, false - } - - dyn, err := dynamic.NewForConfig(cfg) - if err != nil { - return nil, false - } - - return dyn.Resource(dashboardv0.DashboardResourceInfo.GroupVersionResource()).Namespace(fk8s.getNamespace(orgID)), true -} - -func (fk8s *foldk8sHandler) getNamespace(orgID int64) string { - return fk8s.namespacer(orgID) -} - -func (fk8s *foldk8sHandler) getSearcher(ctx context.Context) resource.ResourceClient { - return fk8s.recourceClientProvider(ctx) -} diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 4421b441113..8a7e99a991c 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -12,9 +12,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "google.golang.org/grpc" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/dynamic" + "k8s.io/apimachinery/pkg/selection" clientrest "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -28,8 +26,10 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" + dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" @@ -173,23 +173,17 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { Host: folderApiServerMock.URL, } - f := func(ctx context.Context) resource.ResourceClient { - return resourceClientMock{} - } - - k8sHandler := &foldk8sHandler{ - gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(), - namespacer: request.GetNamespaceMapper(cfg), - cfg: cfg, - restConfigProvider: restCfgProvider.GetRestConfig, - recourceClientProvider: f, - } - userService := &usertest.FakeUserService{ ExpectedUser: &user.User{}, } - unifiedStore := ProvideUnifiedStore(k8sHandler, userService) + featuresArr := []any{ + featuremgmt.FlagKubernetesFoldersServiceV2} + features := featuremgmt.WithFeatures(featuresArr...) + + dashboardStore := dashboards.NewFakeDashboardStore(t) + k8sCli := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.FolderResourceInfo.GroupVersionResource(), restCfgProvider.GetRestConfig, dashboardStore, userService) + unifiedStore := ProvideUnifiedStore(k8sCli, userService) ctx := context.Background() usr := &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{ @@ -209,10 +203,6 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, } - featuresArr := []any{ - featuremgmt.FlagKubernetesFoldersServiceV2} - features := featuremgmt.WithFeatures(featuresArr...) - dashboardStore := dashboards.NewFakeDashboardStore(t) publicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) folderService := &Service{ @@ -224,7 +214,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { registry: make(map[string]folder.RegistryService), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), - k8sclient: k8sHandler, + k8sclient: k8sCli, dashboardStore: dashboardStore, publicDashboardService: publicDashboardService, } @@ -341,7 +331,6 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { NewTitle: &title, SignedInUser: usr, } - reqResult, err := folderService.Update(ctx, req) require.NoError(t, err) require.Equal(t, title, reqResult.Title) @@ -358,7 +347,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { }) t.Run("When deleting folder by uid should not return access denied error - ForceDeleteRules false", func(t *testing.T) { - dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil) + dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once() publicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) err := folderService.Delete(ctx, &folder.DeleteFolderCommand{ @@ -428,6 +417,13 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { }) t.Run("When get folder by ID and uid is an empty string should return folder by id", func(t *testing.T) { + dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{ + { + IsFolder: true, + ID: fooFolder.ID, // nolint:staticcheck + UID: fooFolder.UID, + }, + }, nil).Once() id := int64(123) emptyString := "" query := &folder.GetFolderQuery{ @@ -443,6 +439,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { }) t.Run("When get folder by non existing ID should return not found error", func(t *testing.T) { + dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once() id := int64(111111) query := &folder.GetFolderQuery{ ID: &id, @@ -456,6 +453,13 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { }) t.Run("When get folder by Title should return folder", func(t *testing.T) { + dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{ + { + IsFolder: true, + ID: fooFolder.ID, // nolint:staticcheck + UID: fooFolder.UID, + }, + }, nil).Once() title := "foo" query := &folder.GetFolderQuery{ Title: &title, @@ -469,6 +473,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { }) t.Run("When get folder by non existing Title should return not found error", func(t *testing.T) { + dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once() title := "does not exists" query := &folder.GetFolderQuery{ Title: &title, @@ -506,287 +511,81 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { }) } -type resourceClientMock struct{} - -func (r resourceClientMock) Read(ctx context.Context, in *resource.ReadRequest, opts ...grpc.CallOption) (*resource.ReadResponse, error) { - return nil, nil -} -func (r resourceClientMock) Create(ctx context.Context, in *resource.CreateRequest, opts ...grpc.CallOption) (*resource.CreateResponse, error) { - return nil, nil -} -func (r resourceClientMock) Update(ctx context.Context, in *resource.UpdateRequest, opts ...grpc.CallOption) (*resource.UpdateResponse, error) { - return nil, nil -} -func (r resourceClientMock) Delete(ctx context.Context, in *resource.DeleteRequest, opts ...grpc.CallOption) (*resource.DeleteResponse, error) { - return nil, nil -} -func (r resourceClientMock) Restore(ctx context.Context, in *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) { - return nil, nil -} -func (r resourceClientMock) List(ctx context.Context, in *resource.ListRequest, opts ...grpc.CallOption) (*resource.ListResponse, error) { - return nil, nil -} -func (r resourceClientMock) Watch(ctx context.Context, in *resource.WatchRequest, opts ...grpc.CallOption) (resource.ResourceStore_WatchClient, error) { - return nil, nil -} -func (r resourceClientMock) BatchProcess(ctx context.Context, opts ...grpc.CallOption) (resource.BatchStore_BatchProcessClient, error) { - return nil, nil -} -func (r resourceClientMock) Search(ctx context.Context, in *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) { - if len(in.Options.Labels) > 0 && - in.Options.Labels[0].Key == utils.LabelKeyDeprecatedInternalID && - in.Options.Labels[0].Operator == "in" && - len(in.Options.Labels[0].Values) > 0 && - in.Options.Labels[0].Values[0] == "123" { - return &resource.ResourceSearchResponse{ - Results: &resource.ResourceTable{ - Columns: []*resource.ResourceTableColumnDefinition{ - { - Name: "_id", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: "title", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: "folder", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - }, - Rows: []*resource.ResourceTableRow{ - { - Key: &resource.ResourceKey{ - Name: "foo", - Resource: "folders", - }, - Cells: [][]byte{ - []byte("123"), - []byte("folder1"), - []byte(""), - }, - }, - }, - }, - TotalHits: 1, - }, nil - } - - if len(in.Options.Fields) > 0 && - in.Options.Fields[0].Key == resource.SEARCH_FIELD_TITLE_PHRASE && - in.Options.Fields[0].Operator == "in" && - len(in.Options.Fields[0].Values) > 0 && - in.Options.Fields[0].Values[0] == "foo" { - return &resource.ResourceSearchResponse{ - Results: &resource.ResourceTable{ - Columns: []*resource.ResourceTableColumnDefinition{ - { - Name: "_id", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: "title", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: "folder", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - }, - Rows: []*resource.ResourceTableRow{ - { - Key: &resource.ResourceKey{ - Name: "foo", - Resource: "folders", - }, - Cells: [][]byte{ - []byte("123"), - []byte("folder1"), - []byte(""), - }, - }, - }, - }, - TotalHits: 1, - }, nil - } - - if in.Query == "*test*" { - return &resource.ResourceSearchResponse{ - Results: &resource.ResourceTable{ - Columns: []*resource.ResourceTableColumnDefinition{ - { - Name: "_id", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: "title", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: "folder", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - }, - Rows: []*resource.ResourceTableRow{ - { - Key: &resource.ResourceKey{ - Name: "uid", - Resource: "folders", - }, - Cells: [][]byte{ - []byte("123"), - []byte("testing-123"), - []byte("parent-uid"), - }, - }, - }, - }, - TotalHits: 1, - }, nil - } - - if len(in.Options.Fields) > 0 && - in.Options.Fields[0].Key == resource.SEARCH_FIELD_NAME && - in.Options.Fields[0].Operator == "in" && - len(in.Options.Fields[0].Values) > 0 { - rows := []*resource.ResourceTableRow{} - for i, row := range in.Options.Fields[0].Values { - rows = append(rows, &resource.ResourceTableRow{ - Key: &resource.ResourceKey{ - Name: row, - Resource: "folders", - }, - Cells: [][]byte{ - []byte(fmt.Sprintf("%d", i)), // set legacy id as the row id - []byte(fmt.Sprintf("folder%d", i)), // set title as folder + row id - []byte(""), - }, - }) - } - return &resource.ResourceSearchResponse{ - Results: &resource.ResourceTable{ - Columns: []*resource.ResourceTableColumnDefinition{ - { - Name: "_id", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: "title", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: "folder", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - }, - Rows: rows, - }, - TotalHits: int64(len(rows)), - }, nil - } - - if len(in.Options.Fields) > 0 && - in.Options.Fields[0].Key == resource.SEARCH_FIELD_FOLDER && - in.Options.Fields[0].Operator == "in" && - len(in.Options.Fields[0].Values) > 0 { - rows := []*resource.ResourceTableRow{} - for i, row := range in.Options.Fields[0].Values { - rows = append(rows, &resource.ResourceTableRow{ - Key: &resource.ResourceKey{ - Name: row, - Resource: "folders", - }, - Cells: [][]byte{ - []byte(fmt.Sprintf("%d", i)), // set legacy id as the row id - []byte(fmt.Sprintf("folder%d", i)), // set title as folder + row id - []byte(""), - }, - }) - } - return &resource.ResourceSearchResponse{ - Results: &resource.ResourceTable{ - Columns: []*resource.ResourceTableColumnDefinition{ - { - Name: "_id", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: "title", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: "folder", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - }, - Rows: rows, - }, - TotalHits: int64(len(rows)), - }, nil - } - - // not found - return &resource.ResourceSearchResponse{ - Results: &resource.ResourceTable{}, - }, nil -} -func (r resourceClientMock) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) { - return nil, nil -} -func (r resourceClientMock) CountRepositoryObjects(ctx context.Context, in *resource.CountRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.CountRepositoryObjectsResponse, error) { - return nil, nil -} -func (r resourceClientMock) ListRepositoryObjects(ctx context.Context, in *resource.ListRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.ListRepositoryObjectsResponse, error) { - return nil, nil -} -func (r resourceClientMock) PutBlob(ctx context.Context, in *resource.PutBlobRequest, opts ...grpc.CallOption) (*resource.PutBlobResponse, error) { - return nil, nil -} -func (r resourceClientMock) GetBlob(ctx context.Context, in *resource.GetBlobRequest, opts ...grpc.CallOption) (*resource.GetBlobResponse, error) { - return nil, nil -} -func (r resourceClientMock) IsHealthy(ctx context.Context, in *resource.HealthCheckRequest, opts ...grpc.CallOption) (*resource.HealthCheckResponse, error) { - return nil, nil -} - -type mockFoldersK8sCli struct { - mock.Mock - searcher resourceClientMock -} - -func (m *mockFoldersK8sCli) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) { - args := m.Called(ctx, orgID) - return args.Get(0).(dynamic.ResourceInterface), args.Bool(1) -} - -func (m *mockFoldersK8sCli) getDashboardClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) { - args := m.Called(ctx, orgID) - return args.Get(0).(dynamic.ResourceInterface), args.Bool(1) -} - -func (m *mockFoldersK8sCli) getNamespace(orgID int64) string { - if orgID == 1 { - return "default" - } - return fmt.Sprintf("orgs-%d", orgID) -} - -func (m *mockFoldersK8sCli) getSearcher(ctx context.Context) resource.ResourceClient { - return m.searcher -} - func TestSearchFoldersFromApiServer(t *testing.T) { - fakeK8sClient := new(mockFoldersK8sCli) + fakeK8sClient := new(client.MockK8sHandler) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanSaveValue: true, + CanViewValue: true, + }) + folderStore := folder.NewFakeStore() + folderStore.ExpectedFolder = &folder.Folder{ + UID: "parent-uid", + ID: 2, + Title: "parent title", + } service := Service{ - k8sclient: fakeK8sClient, - features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2), + k8sclient: fakeK8sClient, + features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2), + unifiedStore: folderStore, } - fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient) user := &user.SignedInUser{OrgID: 1} ctx := identity.WithRequester(context.Background(), user) + fakeK8sClient.On("GetNamespace", mock.Anything, mock.Anything).Return("default") - t.Run("Should search by uids if provided", func(t *testing.T) { + t.Run("Should call search with uids, if provided", func(t *testing.T) { + fakeK8sClient.On("Search", mock.Anything, int64(1), &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: "default", + Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, + Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + }, + Fields: []*resource.Requirement{ + { + Key: resource.SEARCH_FIELD_NAME, + Operator: string(selection.In), + Values: []string{"uid1", "uid2"}, // should only search by uid since it is provided + }, + }, + Labels: []*resource.Requirement{}, + }, + Limit: 100000}).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid1", + Resource: "folder", + }, + Cells: [][]byte{ + []byte("folder0"), + []byte(""), + }, + }, + { + Key: &resource.ResourceKey{ + Name: "uid2", + Resource: "folder", + }, + Cells: [][]byte{ + []byte("folder1"), + []byte(""), + }, + }, + }, + }, + TotalHits: 2, + }, nil).Once() query := folder.SearchFoldersQuery{ UIDs: []string{"uid1", "uid2"}, IDs: []int64{1, 2}, // will ignore these because uid is passed in @@ -821,16 +620,60 @@ func TestSearchFoldersFromApiServer(t *testing.T) { }, } require.Equal(t, expectedResult, result) + fakeK8sClient.AssertExpectations(t) }) - t.Run("Search by ID if uids are not provided", func(t *testing.T) { + t.Run("Should call search by ID if uids are not provided", func(t *testing.T) { query := folder.SearchFoldersQuery{ IDs: []int64{123}, SignedInUser: user, } + fakeK8sClient.On("Search", mock.Anything, int64(1), &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: "default", + Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, + Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + }, + Fields: []*resource.Requirement{}, + Labels: []*resource.Requirement{ + { + Key: utils.LabelKeyDeprecatedInternalID, + Operator: string(selection.In), + Values: []string{"123"}, + }, + }, + }, + Limit: 100000}).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "foo", + Resource: "folder", + }, + Cells: [][]byte{ + []byte("folder1"), + []byte(""), + }, + }, + }, + }, + TotalHits: 1, + }, nil).Once() + result, err := service.searchFoldersFromApiServer(ctx, query) require.NoError(t, err) - expectedResult := model.HitList{ { UID: "foo", @@ -844,6 +687,7 @@ func TestSearchFoldersFromApiServer(t *testing.T) { }, } require.Equal(t, expectedResult, result) + fakeK8sClient.AssertExpectations(t) }) t.Run("Search by title, wildcard should be added to search request (won't match in search mock if not)", func(t *testing.T) { @@ -855,10 +699,45 @@ func TestSearchFoldersFromApiServer(t *testing.T) { Title: "parent title", } service.unifiedStore = fakeFolderStore - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanSaveValue: true, - CanViewValue: true, - }) + fakeK8sClient.On("Search", mock.Anything, int64(1), &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: "default", + Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, + Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + }, + Fields: []*resource.Requirement{}, + Labels: []*resource.Requirement{}, + }, + Query: "*test*", + Fields: dashboardsearch.IncludeFields, + Limit: 100000}).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid", + Resource: "folder", + }, + Cells: [][]byte{ + []byte("testing-123"), + []byte("parent-uid"), + }, + }, + }, + }, + TotalHits: 1, + }, nil).Once() query := folder.SearchFoldersQuery{ Title: "test", @@ -881,33 +760,26 @@ func TestSearchFoldersFromApiServer(t *testing.T) { }, } require.Equal(t, expectedResult, result) + fakeK8sClient.AssertExpectations(t) }) } -type mockDashboardCli struct { - mock.Mock - dynamic.ResourceInterface -} - -func (c *mockDashboardCli) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error { - args := c.Called(ctx, name, options) - return args.Error(0) -} - func TestDeleteFoldersFromApiServer(t *testing.T) { - fakeK8sClient := new(mockFoldersK8sCli) + fakeK8sClient := new(client.MockK8sHandler) + fakeK8sClient.On("GetNamespace", mock.Anything, mock.Anything).Return("default") + dashboardK8sclient := new(client.MockK8sHandler) fakeFolderStore := folder.NewFakeStore() dashboardStore := dashboards.NewFakeDashboardStore(t) publicDashboardFakeService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) service := Service{ k8sclient: fakeK8sClient, + dashboardK8sClient: dashboardK8sclient, unifiedStore: fakeFolderStore, dashboardStore: dashboardStore, publicDashboardService: publicDashboardFakeService, registry: make(map[string]folder.RegistryService), features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2), } - fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient) user := &user.SignedInUser{OrgID: 1} ctx := identity.WithRequester(context.Background(), user) guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ @@ -969,19 +841,53 @@ func TestDeleteFoldersFromApiServer(t *testing.T) { service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagKubernetesCliDashboards) t.Run("Should delete dashboards and public dashboards within the folder through k8s if the ff is enabled", func(t *testing.T) { - dashboardK8sCli := mockDashboardCli{} - dashboardK8sCli.On("Delete", mock.Anything, "uid1", mock.Anything, mock.Anything).Return(nil).Once() - fakeK8sClient.On("getDashboardClient", mock.Anything, mock.Anything).Return(&dashboardK8sCli, true) - fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient) publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"uid1"}).Return(nil).Once() + dashboardK8sclient.On("Delete", mock.Anything, "uid1", int64(1), mock.Anything).Return(nil).Once() + dashboardK8sclient.On("Search", mock.Anything, int64(1), &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Labels: []*resource.Requirement{}, + Fields: []*resource.Requirement{ + { + Key: resource.SEARCH_FIELD_FOLDER, + Operator: string(selection.In), + Values: []string{"uid1"}, + }, + }, + }, + Limit: 100000}).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid1", + Resource: "folder", + }, + Cells: [][]byte{ + []byte("folder1"), + []byte(""), + }, + }, + }, + }, + TotalHits: 1, + }, nil).Once() err := service.deleteFromApiServer(ctx, &folder.DeleteFolderCommand{ UID: "uid1", OrgID: 1, SignedInUser: user, }) require.NoError(t, err) - dashboardStore.AssertExpectations(t) publicDashboardFakeService.AssertExpectations(t) - dashboardK8sCli.AssertExpectations(t) + dashboardK8sclient.AssertExpectations(t) }) } diff --git a/pkg/services/folder/folderimpl/unifiedstore.go b/pkg/services/folder/folderimpl/unifiedstore.go index 481ab6c704d..c46f9ada849 100644 --- a/pkg/services/folder/folderimpl/unifiedstore.go +++ b/pkg/services/folder/folderimpl/unifiedstore.go @@ -4,20 +4,17 @@ import ( "context" "fmt" "strings" - "time" apierrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - k8sUser "k8s.io/apiserver/pkg/authentication/user" - k8sRequest "k8s.io/apiserver/pkg/endpoints/request" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/user" @@ -26,14 +23,14 @@ import ( type FolderUnifiedStoreImpl struct { log log.Logger - k8sclient folderK8sHandler + k8sclient client.K8sHandler userService user.Service } // sqlStore implements the store interface. var _ folder.Store = (*FolderUnifiedStoreImpl)(nil) -func ProvideUnifiedStore(k8sHandler *foldk8sHandler, userService user.Service) *FolderUnifiedStoreImpl { +func ProvideUnifiedStore(k8sHandler client.K8sHandler, userService user.Service) *FolderUnifiedStoreImpl { return &FolderUnifiedStoreImpl{ k8sclient: k8sHandler, log: log.New("folder-store"), @@ -42,23 +39,11 @@ func ProvideUnifiedStore(k8sHandler *foldk8sHandler, userService user.Service) * } func (ss *FolderUnifiedStoreImpl) Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error) { - newCtx, cancel, err := ss.getK8sContext(ctx) - if err != nil { - return nil, err - } else if cancel != nil { - defer cancel() - } - - client, ok := ss.k8sclient.getClient(newCtx, cmd.OrgID) - if !ok { - return nil, nil - } - obj, err := internalfolders.LegacyCreateCommandToUnstructured(&cmd) if err != nil { return nil, err } - out, err := client.Create(newCtx, obj, v1.CreateOptions{}) + out, err := ss.k8sclient.Create(ctx, obj, cmd.OrgID) if err != nil { return nil, err } @@ -72,20 +57,8 @@ func (ss *FolderUnifiedStoreImpl) Create(ctx context.Context, cmd folder.CreateF } func (ss *FolderUnifiedStoreImpl) Delete(ctx context.Context, UIDs []string, orgID int64) error { - newCtx, cancel, err := ss.getK8sContext(ctx) - if err != nil { - return err - } else if cancel != nil { - defer cancel() - } - - client, ok := ss.k8sclient.getClient(newCtx, orgID) - if !ok { - return nil - } - for _, uid := range UIDs { - err = client.Delete(newCtx, uid, v1.DeleteOptions{}) + err := ss.k8sclient.Delete(ctx, uid, orgID, v1.DeleteOptions{}) if err != nil { return err } @@ -95,19 +68,7 @@ func (ss *FolderUnifiedStoreImpl) Delete(ctx context.Context, UIDs []string, org } func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateFolderCommand) (*folder.Folder, error) { - newCtx, cancel, err := ss.getK8sContext(ctx) - if err != nil { - return nil, err - } else if cancel != nil { - defer cancel() - } - - client, ok := ss.k8sclient.getClient(newCtx, cmd.OrgID) - if !ok { - return nil, nil - } - - obj, err := client.Get(ctx, cmd.UID, v1.GetOptions{}) + obj, err := ss.k8sclient.Get(ctx, cmd.UID, cmd.OrgID, v1.GetOptions{}) if err != nil { return nil, err } @@ -133,7 +94,7 @@ func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateF meta.SetFolder(*cmd.NewParentUID) } - out, err := client.Update(ctx, updated, v1.UpdateOptions{}) + out, err := ss.k8sclient.Update(ctx, updated, cmd.OrgID) if err != nil { return nil, err } @@ -160,21 +121,7 @@ func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateF // // The full path of C is "A/B\/C". func (ss *FolderUnifiedStoreImpl) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) { - // create a new context - prevents issues when the request stems from the k8s api itself - // otherwise the context goes through the handlers twice and causes issues - newCtx, cancel, err := ss.getK8sContext(ctx) - if err != nil { - return nil, err - } else if cancel != nil { - defer cancel() - } - - client, ok := ss.k8sclient.getClient(newCtx, q.OrgID) - if !ok { - return nil, nil - } - - out, err := client.Get(newCtx, *q.UID, v1.GetOptions{}) + out, err := ss.k8sclient.Get(ctx, *q.UID, q.OrgID, v1.GetOptions{}) if err != nil && !apierrors.IsNotFound(err) { return nil, err } else if err != nil || out == nil { @@ -185,26 +132,12 @@ func (ss *FolderUnifiedStoreImpl) Get(ctx context.Context, q folder.GetFolderQue } func (ss *FolderUnifiedStoreImpl) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) { - // create a new context - prevents issues when the request stems from the k8s api itself - // otherwise the context goes through the handlers twice and causes issues - newCtx, cancel, err := ss.getK8sContext(ctx) - if err != nil { - return nil, err - } else if cancel != nil { - defer cancel() - } - - client, ok := ss.k8sclient.getClient(newCtx, q.OrgID) - if !ok { - return nil, nil - } - hits := []*folder.Folder{} parentUid := q.UID for parentUid != "" { - out, err := client.Get(newCtx, parentUid, v1.GetOptions{}) + out, err := ss.k8sclient.Get(ctx, parentUid, q.OrgID, v1.GetOptions{}) if err != nil { return nil, err } @@ -226,21 +159,7 @@ func (ss *FolderUnifiedStoreImpl) GetParents(ctx context.Context, q folder.GetPa } func (ss *FolderUnifiedStoreImpl) GetChildren(ctx context.Context, q folder.GetChildrenQuery) ([]*folder.Folder, error) { - // create a new context - prevents issues when the request stems from the k8s api itself - // otherwise the context goes through the handlers twice and causes issues - newCtx, cancel, err := ss.getK8sContext(ctx) - if err != nil { - return nil, err - } else if cancel != nil { - defer cancel() - } - - client, ok := ss.k8sclient.getClient(newCtx, q.OrgID) - if !ok { - return nil, nil - } - - out, err := client.List(newCtx, v1.ListOptions{}) + out, err := ss.k8sclient.List(ctx, q.OrgID, v1.ListOptions{}) if err != nil { return nil, err } @@ -328,19 +247,7 @@ func (ss *FolderUnifiedStoreImpl) GetHeight(ctx context.Context, foldrUID string // The full path UIDs of B is "uid1/uid2". // The full path UIDs of A is "uid1". func (ss *FolderUnifiedStoreImpl) GetFolders(ctx context.Context, q folder.GetFoldersFromStoreQuery) ([]*folder.Folder, error) { - newCtx, cancel, err := ss.getK8sContext(ctx) - if err != nil { - return nil, err - } else if cancel != nil { - defer cancel() - } - - client, ok := ss.k8sclient.getClient(newCtx, q.OrgID) - if !ok { - return nil, nil - } - - out, err := client.List(newCtx, v1.ListOptions{}) + out, err := ss.k8sclient.List(ctx, q.OrgID, v1.ListOptions{}) if err != nil { return nil, err } @@ -394,21 +301,7 @@ func (ss *FolderUnifiedStoreImpl) GetFolders(ctx context.Context, q folder.GetFo } func (ss *FolderUnifiedStoreImpl) GetDescendants(ctx context.Context, orgID int64, ancestor_uid string) ([]*folder.Folder, error) { - // create a new context - prevents issues when the request stems from the k8s api itself - // otherwise the context goes through the handlers twice and causes issues - newCtx, cancel, err := ss.getK8sContext(ctx) - if err != nil { - return nil, err - } else if cancel != nil { - defer cancel() - } - - client, ok := ss.k8sclient.getClient(newCtx, orgID) - if !ok { - return nil, nil - } - - out, err := client.List(newCtx, v1.ListOptions{}) + out, err := ss.k8sclient.List(ctx, orgID, v1.ListOptions{}) if err != nil { return nil, err } @@ -458,21 +351,7 @@ func getDescendants(nodes map[string]*folder.Folder, tree map[string]map[string] } func (ss *FolderUnifiedStoreImpl) CountFolderContent(ctx context.Context, orgID int64, ancestor_uid string) (folder.DescendantCounts, error) { - // create a new context - prevents issues when the request stems from the k8s api itself - // otherwise the context goes through the handlers twice and causes issues - newCtx, cancel, err := ss.getK8sContext(ctx) - if err != nil { - return nil, err - } else if cancel != nil { - defer cancel() - } - - client, ok := ss.k8sclient.getClient(newCtx, orgID) - if !ok { - return nil, nil - } - - counts, err := client.Get(newCtx, ancestor_uid, v1.GetOptions{}, "counts") + counts, err := ss.k8sclient.Get(ctx, ancestor_uid, orgID, v1.GetOptions{}, "counts") if err != nil { return nil, err } @@ -502,42 +381,6 @@ func toFolderLegacyCounts(u *unstructured.Unstructured) (*folder.DescendantCount return &out, nil } -func (ss *FolderUnifiedStoreImpl) getK8sContext(ctx context.Context) (context.Context, context.CancelFunc, error) { - requester, requesterErr := identity.GetRequester(ctx) - if requesterErr != nil { - return nil, nil, requesterErr - } - - user, exists := k8sRequest.UserFrom(ctx) - if !exists { - // add in k8s user if not there yet - var ok bool - user, ok = requester.(k8sUser.Info) - if !ok { - return nil, nil, fmt.Errorf("could not convert user to k8s user") - } - } - - newCtx := k8sRequest.WithUser(context.Background(), user) - newCtx = log.WithContextualAttributes(newCtx, log.FromContext(ctx)) - // TODO: after GLSA token workflow is removed, make this return early - // and move the else below to be unconditional - if requesterErr == nil { - newCtxWithRequester := identity.WithRequester(newCtx, requester) - newCtx = newCtxWithRequester - } - - // inherit the deadline from the original context, if it exists - deadline, ok := ctx.Deadline() - if ok { - var newCancel context.CancelFunc - newCtx, newCancel = context.WithTimeout(newCtx, time.Until(deadline)) - return newCtx, newCancel, nil - } - - return newCtx, nil, nil -} - func computeFullPath(parents []*folder.Folder) (string, string) { fullpath := make([]string, len(parents)) fullpathUIDs := make([]string, len(parents)) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 7ea77dd22ff..8fddbc1e1f2 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -25,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/apiserver/client" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -355,8 +356,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash folderSvc, folder.NewFakeStore(), nil, - nil, - nil, + client.MockTestRestConfig{}, nil, quotaService, nil, @@ -453,7 +453,7 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena cfg, dashboardStore, folderStore, features, folderPermissions, ac, folderSvc, fStore, - nil, nil, nil, nil, quotaService, nil, nil, + nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, ) require.NoError(t, svcErr) dashboardService.RegisterDashboardPermissions(dashboardPermissions) @@ -526,7 +526,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo cfg, dashboardStore, folderStore, features, folderPermissions, ac, folderSvc, fStore, - nil, nil, nil, nil, quotaService, nil, nil, + nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, ) require.NoError(t, dashSvcErr) dashService.RegisterDashboardPermissions(dashboardPermissions) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index f9e9c8c8d75..75980513f1b 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -735,7 +736,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash cfg, dashboardStore, folderStore, features, acmock.NewMockedPermissionsService(), ac, foldertest.NewFakeService(), folder.NewFakeStore(), - nil, nil, nil, nil, quotaService, nil, nil, + nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, ) require.NoError(t, err) service.RegisterDashboardPermissions(dashPermissionService) @@ -833,7 +834,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo cfg, dashStore, folderStore, features, acmock.NewMockedPermissionsService(), ac, folderSvc, folder.NewFakeStore(), - nil, nil, nil, nil, quotaService, nil, nil, + nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, ) require.NoError(t, err) dashService.RegisterDashboardPermissions(dashPermissionService) diff --git a/pkg/services/ngalert/testutil/testutil.go b/pkg/services/ngalert/testutil/testutil.go index 7aa8c1d45b3..b7175b5dca6 100644 --- a/pkg/services/ngalert/testutil/testutil.go +++ b/pkg/services/ngalert/testutil/testutil.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -62,7 +63,7 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa cfg, dashboardStore, fs, features, folderPermissions, ac, foldertest.NewFakeService(), folder.NewFakeStore(), - nil, nil, nil, nil, quotaService, nil, nil, + nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, ) require.NoError(tb, err) dashboardService.RegisterDashboardPermissions(dashboardPermissions) diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index d0a87802ab8..e81d34c6d37 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -25,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -325,7 +326,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) dashService, err := service.ProvideDashboardServiceImpl( cfg, dashboardStoreService, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), ac, - foldertest.NewFakeService(), folder.NewFakeStore(), nil, nil, nil, nil, quotatest.New(false, nil), nil, nil, + foldertest.NewFakeService(), folder.NewFakeStore(), nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, ) require.NoError(t, err) dashService.RegisterDashboardPermissions(dashPermissionService) diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index bec987a72a8..6605c391488 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -1398,7 +1399,7 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, testDB, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) - dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, nil, nil, nil, quotatest.New(false, nil), nil, nil) + dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil) require.NoError(t, err) dashboardService.RegisterDashboardPermissions(&actest.FakePermissionsService{}) fakeGuardian := &guardian.FakeDashboardGuardian{ diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 5cf4a3acabf..5f70f869e3a 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/services/annotations/annotationstest" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" + "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authimpl" "github.com/grafana/grafana/pkg/services/dashboards" @@ -496,7 +497,7 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) dashService, err := dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, nil, nil, nil, quotaService, nil, nil) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil) require.NoError(t, err) dashService.RegisterDashboardPermissions(acmock.NewMockedPermissionsService()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) diff --git a/pkg/storage/unified/resource/search_client.go b/pkg/storage/unified/resource/search_client.go index 43e0faa7b1e..8615a8cbed2 100644 --- a/pkg/storage/unified/resource/search_client.go +++ b/pkg/storage/unified/resource/search_client.go @@ -1,20 +1,22 @@ package resource import ( + "context" + "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/setting" ) -func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient ResourceIndexClient, legacyClient ResourceIndexClient) ResourceIndexClient { +func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient func(context.Context) ResourceClient, legacyClient ResourceIndexClient) func(context.Context) ResourceIndexClient { config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey] if !ok { - return legacyClient + return func(ctx context.Context) ResourceIndexClient { return legacyClient } } switch config.DualWriterMode { case rest.Mode0, rest.Mode1, rest.Mode2: - return legacyClient + return func(ctx context.Context) ResourceIndexClient { return legacyClient } default: - return unifiedClient + return func(ctx context.Context) ResourceIndexClient { return unifiedClient(ctx) } } } From d199c33d7e0bc815ac456c3350e1bd45261ea0fd Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 11 Feb 2025 20:25:52 +0100 Subject: [PATCH 509/894] Log Context: Fix bug where variables are not replaced in dashboards (#100433) * Log Context: Fix bug where variables are not replaced in dashboards * add objects to act as `row` and `options` * run prettier * fix lint --- packages/grafana-data/src/types/logs.ts | 9 ++- .../loki/LogContextProvider.test.ts | 57 +++++++++++++++++-- .../datasource/loki/LogContextProvider.ts | 17 +++++- .../app/plugins/datasource/loki/datasource.ts | 14 ++++- .../app/plugins/panel/logs/LogsPanel.test.tsx | 2 +- public/app/plugins/panel/logs/LogsPanel.tsx | 8 ++- 6 files changed, 95 insertions(+), 12 deletions(-) diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index 67a194202a7..bfd54b0ec6f 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -4,6 +4,7 @@ import { DataQuery, LogsSortOrder } from '@grafana/schema'; import { BusEventWithPayload } from '../events/types'; +import { ScopedVars } from './ScopedVars'; import { KeyValue, Labels } from './data'; import { DataFrame } from './dataFrame'; import { DataQueryRequest, DataQueryResponse, DataSourceApi, QueryFixAction, QueryFixType } from './datasource'; @@ -134,6 +135,7 @@ export enum LogsDedupDescription { export interface LogRowContextOptions { direction?: LogRowContextQueryDirection; limit?: number; + scopedVars?: ScopedVars; } export enum LogRowContextQueryDirection { @@ -172,7 +174,12 @@ export interface DataSourceWithLogsContextSupport void, origQuery?: TQuery): React.ReactNode; + getLogRowContextUi?( + row: LogRowModel, + runContextQuery?: () => void, + origQuery?: TQuery, + scopedVars?: ScopedVars + ): React.ReactNode; } export const hasLogsContextSupport = (datasource: unknown): datasource is DataSourceWithLogsContextSupport => { diff --git a/public/app/plugins/datasource/loki/LogContextProvider.test.ts b/public/app/plugins/datasource/loki/LogContextProvider.test.ts index 08c4643d475..dcd86744b47 100644 --- a/public/app/plugins/datasource/loki/LogContextProvider.test.ts +++ b/public/app/plugins/datasource/loki/LogContextProvider.test.ts @@ -1,4 +1,5 @@ import { of } from 'rxjs'; +import { initTemplateSrv } from 'test/helpers/initTemplateSrv'; import { DataQueryResponse, @@ -8,6 +9,7 @@ import { createDataFrame, dateTime, } from '@grafana/data'; +import { setTemplateSrv } from '@grafana/runtime'; import LokiLanguageProvider from './LanguageProvider'; import { @@ -24,10 +26,6 @@ const defaultLanguageProviderMock = { getLabelKeys: jest.fn(() => ['bar', 'xyz']), } as unknown as LokiLanguageProvider; -const defaultDatasourceMock = createLokiDatasource(); -defaultDatasourceMock.query = jest.fn(() => of({ data: [] } as DataQueryResponse)); -defaultDatasourceMock.languageProvider = defaultLanguageProviderMock; - const defaultLogRow = { rowIndex: 0, dataFrame: createDataFrame({ @@ -68,6 +66,11 @@ const frameWithoutTypes = { describe('LogContextProvider', () => { let logContextProvider: LogContextProvider; beforeEach(() => { + const templateSrv = initTemplateSrv('key', [{ type: 'query', name: 'foo', current: { value: 'baz' } }]); + setTemplateSrv(templateSrv); + const defaultDatasourceMock = createLokiDatasource(templateSrv); + defaultDatasourceMock.query = jest.fn(() => of({ data: [] } as DataQueryResponse)); + defaultDatasourceMock.languageProvider = defaultLanguageProviderMock; logContextProvider = new LogContextProvider(defaultDatasourceMock); }); @@ -107,6 +110,32 @@ describe('LogContextProvider', () => { expect(logContextProvider.cachedContextFilters).toHaveLength(1); }); + it('should replace variables before getInitContextFilters', async () => { + logContextProvider.getInitContextFilters = jest.fn().mockResolvedValue({ + contextFilters: [{ value: 'baz', enabled: true, nonIndexed: false, label: 'bar' }], + preservedFiltersApplied: false, + }); + + expect(logContextProvider.cachedContextFilters).toHaveLength(0); + await logContextProvider.getLogRowContext( + defaultLogRow, + { + limit: 10, + direction: LogRowContextQueryDirection.Backward, + scopedVars: { test: { value: 'baz', text: 'baz' } }, + }, + { + expr: '{bar="$test"}', + refId: 'A', + } + ); + expect(logContextProvider.getInitContextFilters).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ expr: '{bar="baz"}', refId: 'A' }), + expect.anything() + ); + }); + it('should not call getInitContextFilters if cachedContextFilters', async () => { logContextProvider.getInitContextFilters = jest .fn() @@ -140,6 +169,26 @@ describe('LogContextProvider', () => { expect(logContextProvider.getInitContextFilters).toHaveBeenCalled(); }); + it('should replace variables before getInitContextFilters', async () => { + logContextProvider.getInitContextFilters = jest.fn().mockResolvedValue({ + contextFilters: [{ value: 'baz', enabled: true, nonIndexed: false, label: 'bar' }], + preservedFiltersApplied: false, + }); + + const query = await logContextProvider.getLogRowContextQuery( + defaultLogRow, + { + limit: 10, + direction: LogRowContextQueryDirection.Backward, + }, + { + expr: '{bar="$test"}', + refId: 'A', + } + ); + expect(query.expr).toBe('{bar="baz"}'); + }); + it('should also call getInitContextFilters if cacheFilters is not set', async () => { logContextProvider.getInitContextFilters = jest.fn().mockResolvedValue({ contextFilters: [{ value: 'baz', enabled: true, nonIndexed: false, label: 'bar' }], diff --git a/public/app/plugins/datasource/loki/LogContextProvider.ts b/public/app/plugins/datasource/loki/LogContextProvider.ts index 4fa2da27792..00873a8d920 100644 --- a/public/app/plugins/datasource/loki/LogContextProvider.ts +++ b/public/app/plugins/datasource/loki/LogContextProvider.ts @@ -14,6 +14,7 @@ import { LogRowContextQueryDirection, LogRowContextOptions, dateTime, + ScopedVars, } from '@grafana/data'; import { LabelParser, LabelFilter, LineFilters, PipelineStage, Logfmt, Json } from '@grafana/lezer-logql'; @@ -79,6 +80,9 @@ export class LogContextProvider { origQuery?: LokiQuery, cacheFilters = true ): Promise => { + if (origQuery && options?.scopedVars) { + origQuery = this.datasource.applyTemplateVariables(origQuery, options?.scopedVars); + } const { query } = await this.getQueryAndRange(row, options, origQuery, cacheFilters); if (!cacheFilters) { @@ -94,6 +98,9 @@ export class LogContextProvider { options?: LogRowContextOptions, origQuery?: LokiQuery ): Promise<{ data: DataFrame[] }> => { + if (origQuery && options?.scopedVars) { + origQuery = this.datasource.applyTemplateVariables(origQuery, options?.scopedVars); + } const direction = (options && options.direction) || LogRowContextQueryDirection.Backward; const { query, range } = await this.getQueryAndRange(row, options, origQuery); @@ -185,7 +192,15 @@ export class LogContextProvider { }; } - getLogRowContextUi(row: LogRowModel, runContextQuery?: () => void, origQuery?: LokiQuery): React.ReactNode { + getLogRowContextUi( + row: LogRowModel, + runContextQuery?: () => void, + origQuery?: LokiQuery, + scopedVars?: ScopedVars + ): React.ReactNode { + if (origQuery && scopedVars) { + origQuery = this.datasource.applyTemplateVariables(origQuery, scopedVars); + } const updateFilter = (contextFilters: ContextFilter[]) => { this.cachedContextFilters = contextFilters; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 3ceade42594..66c9236ae27 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -1014,8 +1014,18 @@ export class LokiDatasource * Part of `DataSourceWithLogsContextSupport`, used to retrieve the log context UI for the provided log row and original query. * @returns A React component or element representing the log context UI for the log row. */ - getLogRowContextUi(row: LogRowModel, runContextQuery: () => void, origQuery: DataQuery): React.ReactNode { - return this.logContextProvider.getLogRowContextUi(row, runContextQuery, getLokiQueryFromDataQuery(origQuery)); + getLogRowContextUi( + row: LogRowModel, + runContextQuery: () => void, + origQuery: DataQuery, + scopedVars?: ScopedVars + ): React.ReactNode { + return this.logContextProvider.getLogRowContextUi( + row, + runContextQuery, + getLokiQueryFromDataQuery(origQuery), + scopedVars + ); } /** diff --git a/public/app/plugins/panel/logs/LogsPanel.test.tsx b/public/app/plugins/panel/logs/LogsPanel.test.tsx index 76c04ca75a7..7def6ca6cb5 100644 --- a/public/app/plugins/panel/logs/LogsPanel.test.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.test.tsx @@ -414,7 +414,7 @@ describe('LogsPanel', () => { await userEvent.click(screen.getByLabelText(/show context/i)); const getRowContextCb = logRowContextModalMock.mock.calls[0][0].getRowContext; - getRowContextCb(); + getRowContextCb({}, {}); expect(showContextDs.getLogRowContext).toBeCalled(); }); }); diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index 33043b913eb..51ae908d9b5 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -232,9 +232,11 @@ export const LogsPanel = ({ return Promise.resolve({ data: [] }); } + options.scopedVars = panelData.request?.scopedVars; + return dataSource.getLogRowContext(row, options, query); }, - [panelData.request?.targets, dataSourcesMap] + [panelData.request?.targets, panelData.request?.scopedVars, dataSourcesMap] ); const getLogRowContextUi = useCallback( @@ -257,9 +259,9 @@ export const LogsPanel = ({ return <>; } - return dataSource.getLogRowContextUi(origRow, runContextQuery, query); + return dataSource.getLogRowContextUi(origRow, runContextQuery, query, panelData.request?.scopedVars); }, - [panelData.request?.targets, dataSourcesMap] + [panelData.request?.targets, panelData.request?.scopedVars, dataSourcesMap] ); // Important to memoize stuff here, as panel rerenders a lot for example when resizing. From 9b37e9249aac66b4b63fcfd1d51f4d2fd5e6271a Mon Sep 17 00:00:00 2001 From: jackyin <648588267@qq.com> Date: Wed, 12 Feb 2025 14:16:01 +0800 Subject: [PATCH 510/894] Dashboard: Folder move unexpected behavior (#100394) * Dashboard: Folder move unexpected behavior * format --- .../features/browse-dashboards/api/browseDashboardsAPI.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index e14cae0ef31..93a0df6d8c4 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -72,7 +72,10 @@ export const browseDashboardsAPI = createApi({ baseQuery: createBaseQuery({ baseURL: '/api' }), endpoints: (builder) => ({ listFolders: builder.query({ - providesTags: (result) => result?.map((folder) => ({ type: 'getFolder', id: folder.uid })) ?? [], + providesTags: (result) => + result && result.length > 0 + ? result.map((folder) => ({ type: 'getFolder', id: folder.uid })) + : [{ type: 'getFolder', id: 'EMPTY_RESULT' }], query: ({ parentUid, limit, page, permission }) => ({ url: '/folders', params: { parentUid, limit, page, permission }, @@ -87,6 +90,7 @@ export const browseDashboardsAPI = createApi({ // create a new folder newFolder: builder.mutation({ + invalidatesTags: ['getFolder'], query: ({ title, parentUid }) => ({ method: 'POST', url: '/folders', @@ -276,6 +280,7 @@ export const browseDashboardsAPI = createApi({ // delete *multiple* items (folders and dashboards). used in the delete modal. deleteItems: builder.mutation({ + invalidatesTags: ['getFolder'], queryFn: async ({ selectedItems }, _api, _extraOptions, baseQuery) => { const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]); const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]); From 9593e51da7c05ed548f11d24ac4c667c51dbe6bb Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 12 Feb 2025 08:13:21 +0100 Subject: [PATCH 511/894] Alerting: conversion API structure (#100258) --- .../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 | 14 + pkg/services/ngalert/api/api.go | 6 + .../ngalert/api/api_convert_prometheus.go | 36 ++ pkg/services/ngalert/api/authorization.go | 28 ++ .../ngalert/api/authorization_test.go | 2 +- .../generated_base_api_convert_prometheus.go | 136 +++++++ .../ngalert/api/prometheus_conversion.go | 56 +++ pkg/services/ngalert/api/tooling/api.json | 79 ++++- .../definitions/convert_prometheus_api.go | 139 ++++++++ pkg/services/ngalert/api/tooling/post.json | 332 +++++++++++++++++- pkg/services/ngalert/api/tooling/spec.json | 332 +++++++++++++++++- .../templates/controller-api.mustache | 9 + public/api-merged.json | 79 ++++- public/openapi3.json | 79 ++++- 18 files changed, 1333 insertions(+), 8 deletions(-) create mode 100644 pkg/services/ngalert/api/api_convert_prometheus.go create mode 100644 pkg/services/ngalert/api/generated_base_api_convert_prometheus.go create mode 100644 pkg/services/ngalert/api/prometheus_conversion.go create mode 100644 pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index cbe09d02910..8e955663673 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -253,6 +253,7 @@ export interface FeatureToggles { exploreMetricsUseExternalAppPlugin?: boolean; datasourceConnectionsTab?: boolean; fetchRulesUsingPost?: boolean; + alertingConversionAPI?: boolean; alertingAlertmanagerExtraDedupStage?: boolean; alertingAlertmanagerExtraDedupStageStopPipeline?: boolean; newLogsPanel?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 81dda206467..879c3153378 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1760,6 +1760,14 @@ var ( HideFromAdminPage: true, HideFromDocs: true, }, + { + Name: "alertingConversionAPI", + Description: "Enable the alerting conversion API", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromAdminPage: true, + HideFromDocs: true, + }, { Name: "alertingAlertmanagerExtraDedupStage", Description: "enables extra deduplication stage in alertmanager that checks that timestamps of the pipeline and the current state are matching", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d21b4c4fded..a3694fa49ae 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -234,6 +234,7 @@ elasticsearchImprovedParsing,experimental,@grafana/aws-datasources,false,false,f exploreMetricsUseExternalAppPlugin,experimental,@grafana/observability-metrics,false,true,true datasourceConnectionsTab,experimental,@grafana/plugins-platform-backend,false,false,true fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false +alertingConversionAPI,experimental,@grafana/alerting-squad,false,false,false alertingAlertmanagerExtraDedupStage,experimental,@grafana/alerting-squad,false,true,false alertingAlertmanagerExtraDedupStageStopPipeline,experimental,@grafana/alerting-squad,false,true,false newLogsPanel,experimental,@grafana/observability-logs,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index bedb497d4f5..d2f0868a839 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -947,6 +947,10 @@ const ( // Use a POST request to list rules by passing down the namespaces user has access to FlagFetchRulesUsingPost = "fetchRulesUsingPost" + // FlagAlertingConversionAPI + // Enable the alerting conversion API + FlagAlertingConversionAPI = "alertingConversionAPI" + // FlagAlertingAlertmanagerExtraDedupStage // enables extra deduplication stage in alertmanager that checks that timestamps of the pipeline and the current state are matching FlagAlertingAlertmanagerExtraDedupStage = "alertingAlertmanagerExtraDedupStage" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index db65ff4fdb8..0cdcc21124f 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -215,6 +215,20 @@ "frontend": true } }, + { + "metadata": { + "name": "alertingConversionAPI", + "resourceVersion": "1739207762746", + "creationTimestamp": "2025-02-10T17:16:02Z" + }, + "spec": { + "description": "Enable the alerting conversion API", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "alertingDisableSendAlertsExternal", diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 811edf6e893..8973ca6cce7 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -185,4 +185,10 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { receiverService: api.ReceiverService, muteTimingService: api.MuteTimings, }), m) + + if api.FeatureManager.IsEnabledGlobally(featuremgmt.FlagAlertingConversionAPI) { + api.RegisterConvertPrometheusApiEndpoints(NewConvertPrometheusApi(&ConvertPrometheusSrv{ + logger: logger, + }), m) + } } diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go new file mode 100644 index 00000000000..babe4f1bca4 --- /dev/null +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -0,0 +1,36 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/infra/log" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" +) + +type ConvertPrometheusSrv struct { + logger log.Logger +} + +func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.ReqContext) response.Response { + return response.Error(501, "Not implemented", nil) +} + +func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response { + return response.Error(501, "Not implemented", nil) +} + +func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { + return response.Error(501, "Not implemented", nil) +} + +func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response { + return response.Error(501, "Not implemented", nil) +} + +func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { + return response.Error(501, "Not implemented", nil) +} + +func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, prometheusGroup apimodels.PrometheusRuleGroup) response.Response { + return response.Error(501, "Not implemented", nil) +} diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index ac5aacfd93d..155f3e0131e 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -123,6 +123,34 @@ func (api *API) authorize(method, path string) web.Handler { case http.MethodPost + "/api/v1/rule/test/{DatasourceUID}": eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) + // convert/prometheus API paths + case http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}", + http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}": + eval = ac.EvalAll( + ac.EvalPermission(ac.ActionAlertingRuleRead), + ac.EvalPermission(dashboards.ActionFoldersRead), + ) + + case http.MethodGet + "/api/convert/prometheus/config/v1/rules": + eval = ac.EvalPermission(ac.ActionAlertingRuleRead) + + case http.MethodPost + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}": + eval = ac.EvalAll( + ac.EvalPermission(dashboards.ActionFoldersWrite), + ac.EvalPermission(ac.ActionAlertingRuleRead), + ac.EvalPermission(ac.ActionAlertingRuleUpdate), + ac.EvalPermission(ac.ActionAlertingRuleCreate), + ac.EvalPermission(ac.ActionAlertingRuleDelete), + ) + + case http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}", + http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}": + eval = ac.EvalAll( + ac.EvalPermission(ac.ActionAlertingRuleDelete), + ac.EvalPermission(ac.ActionAlertingRuleRead), + ac.EvalPermission(dashboards.ActionFoldersRead), + ) + // Alert Instances and Silences // Silences for Grafana paths. diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index 1f83e8750cf..13b0f24c36c 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -41,7 +41,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 60) + require.Len(t, paths, 63) ac := acmock.New() api := &API{AccessControl: ac, FeatureManager: featuremgmt.WithFeatures()} diff --git a/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go new file mode 100644 index 00000000000..3f2e0566295 --- /dev/null +++ b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go @@ -0,0 +1,136 @@ +/*Package api contains base API implementation of unified alerting + * + *Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + * + *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. + */ +package api + +import ( + "net/http" + + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/middleware/requestmeta" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/ngalert/metrics" + "github.com/grafana/grafana/pkg/web" +) + +type ConvertPrometheusApi interface { + RouteConvertPrometheusDeleteNamespace(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusDeleteRuleGroup(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusGetRuleGroup(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusGetRules(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusPostRuleGroup(*contextmodel.ReqContext) response.Response +} + +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteNamespace(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + return f.handleRouteConvertPrometheusDeleteNamespace(ctx, namespaceTitleParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteRuleGroup(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + groupParam := web.Params(ctx.Req)[":Group"] + return f.handleRouteConvertPrometheusDeleteRuleGroup(ctx, namespaceTitleParam, groupParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetNamespace(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + return f.handleRouteConvertPrometheusGetNamespace(ctx, namespaceTitleParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetRuleGroup(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + groupParam := web.Params(ctx.Req)[":Group"] + return f.handleRouteConvertPrometheusGetRuleGroup(ctx, namespaceTitleParam, groupParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetRules(ctx *contextmodel.ReqContext) response.Response { + return f.handleRouteConvertPrometheusGetRules(ctx) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusPostRuleGroup(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + return f.handleRouteConvertPrometheusPostRuleGroup(ctx, namespaceTitleParam) +} + +func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi, m *metrics.API) { + api.RouteRegister.Group("", func(group routing.RouteRegister) { + group.Delete( + toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodDelete, "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), + metrics.Instrument( + http.MethodDelete, + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}", + api.Hooks.Wrap(srv.RouteConvertPrometheusDeleteNamespace), + m, + ), + ) + group.Delete( + toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodDelete, "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}"), + metrics.Instrument( + http.MethodDelete, + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}", + api.Hooks.Wrap(srv.RouteConvertPrometheusDeleteRuleGroup), + m, + ), + ) + group.Get( + toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), + metrics.Instrument( + http.MethodGet, + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}", + api.Hooks.Wrap(srv.RouteConvertPrometheusGetNamespace), + m, + ), + ) + group.Get( + toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}"), + metrics.Instrument( + http.MethodGet, + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}", + api.Hooks.Wrap(srv.RouteConvertPrometheusGetRuleGroup), + m, + ), + ) + group.Get( + toMacaronPath("/api/convert/prometheus/config/v1/rules"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/convert/prometheus/config/v1/rules"), + metrics.Instrument( + http.MethodGet, + "/api/convert/prometheus/config/v1/rules", + api.Hooks.Wrap(srv.RouteConvertPrometheusGetRules), + m, + ), + ) + group.Post( + toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodPost, "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), + metrics.Instrument( + http.MethodPost, + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}", + api.Hooks.Wrap(srv.RouteConvertPrometheusPostRuleGroup), + m, + ), + ) + }, middleware.ReqSignedIn) +} diff --git a/pkg/services/ngalert/api/prometheus_conversion.go b/pkg/services/ngalert/api/prometheus_conversion.go new file mode 100644 index 00000000000..354f6de2a38 --- /dev/null +++ b/pkg/services/ngalert/api/prometheus_conversion.go @@ -0,0 +1,56 @@ +package api + +import ( + "io" + + "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/api/response" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" +) + +type ConvertPrometheusApiHandler struct { + svc *ConvertPrometheusSrv +} + +func NewConvertPrometheusApi(svc *ConvertPrometheusSrv) *ConvertPrometheusApiHandler { + return &ConvertPrometheusApiHandler{ + svc: svc, + } +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetRules(ctx *contextmodel.ReqContext) response.Response { + return f.svc.RouteConvertPrometheusGetRules(ctx) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusDeleteNamespace(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response { + return f.svc.RouteConvertPrometheusDeleteNamespace(ctx, namespaceTitle) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusDeleteRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { + return f.svc.RouteConvertPrometheusDeleteRuleGroup(ctx, namespaceTitle, group) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetNamespace(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response { + return f.svc.RouteConvertPrometheusGetNamespace(ctx, namespaceTitle) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { + return f.svc.RouteConvertPrometheusGetRuleGroup(ctx, namespaceTitle, group) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response { + body, err := io.ReadAll(ctx.Req.Body) + if err != nil { + return errorToResponse(err) + } + defer func() { _ = ctx.Req.Body.Close() }() + + var promGroup apimodels.PrometheusRuleGroup + if err := yaml.Unmarshal(body, &promGroup); err != nil { + return errorToResponse(err) + } + + return f.svc.RouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle, promGroup) +} diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 25f7b2ff8c3..7ed5ae04ba0 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -734,6 +734,20 @@ }, "type": "array" }, + "ConvertPrometheusResponse": { + "properties": { + "error": { + "type": "string" + }, + "errorType": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, "CounterResetHint": { "description": "or alternatively that we are dealing with a gauge histogram, where counter resets do not apply.", "format": "uint8", @@ -2940,6 +2954,70 @@ }, "type": "object" }, + "PrometheusNamespace": { + "properties": { + "Body": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/PrometheusRuleGroup" + }, + "type": "array" + }, + "description": "in: body", + "type": "object" + } + }, + "type": "object" + }, + "PrometheusRule": { + "properties": { + "Alert": { + "type": "string" + }, + "Annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Expr": { + "type": "string" + }, + "For": { + "type": "string" + }, + "KeepFiringFor": { + "type": "string" + }, + "Labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Record": { + "type": "string" + } + }, + "type": "object" + }, + "PrometheusRuleGroup": { + "properties": { + "Interval": { + "$ref": "#/definitions/Duration" + }, + "Name": { + "type": "string" + }, + "Rules": { + "items": { + "$ref": "#/definitions/PrometheusRule" + }, + "type": "array" + } + }, + "type": "object" + }, "Provenance": { "type": "string" }, @@ -4692,7 +4770,6 @@ "type": "object" }, "alertGroups": { - "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup", "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 new file mode 100644 index 00000000000..b2bae42eacb --- /dev/null +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -0,0 +1,139 @@ +package definitions + +import ( + "github.com/prometheus/common/model" +) + +// swagger:route GET /convert/prometheus/config/v1/rules convert_prometheus RouteConvertPrometheusGetRules +// +// Gets all namespaces with their rule groups in Prometheus format. +// +// Produces: +// - application/json +// +// Responses: +// 200: PrometheusNamespace +// 403: ForbiddenError +// 404: NotFound + +// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusGetNamespace +// +// Gets rules in prometheus format for a given namespace. +// +// Produces: +// - application/json +// +// Responses: +// 200: PrometheusNamespace +// 403: ForbiddenError +// 404: NotFound + +// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusGetRuleGroup +// +// Gets a rule group in Prometheus format. +// +// Produces: +// - application/json +// +// Responses: +// 200: PrometheusRuleGroup +// 403: ForbiddenError +// 404: NotFound + +// swagger:route POST /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusPostRuleGroup +// +// Creates or updates a rule group in Prometheus format. +// +// Consumes: +// - application/yaml +// +// Produces: +// - application/json +// +// Responses: +// 202: ConvertPrometheusResponse +// 403: ForbiddenError +// +// Extensions: +// x-raw-request: true + +// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusDeleteNamespace +// +// Deletes all rule groups in the given namespace. +// +// Produces: +// - application/json +// +// Responses: +// 202: ConvertPrometheusResponse +// 403: ForbiddenError + +// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusDeleteRuleGroup +// +// Deletes a rule group in Prometheus format. +// +// Produces: +// - application/json +// +// Responses: +// 202: ConvertPrometheusResponse +// 403: ForbiddenError + +// swagger:parameters RouteConvertPrometheusPostRuleGroup +type RouteConvertPrometheusPostRuleGroupParams struct { + // in: path + NamespaceTitle string + // in: header + DatasourceUID string `json:"x-datasource-uid"` + // in: header + RecordingRulesPaused bool `json:"x-recording-rules-paused"` + // in: header + AlertRulesPaused bool `json:"x-alert-rules-paused"` + // in:body + Body PrometheusRuleGroup +} + +// swagger:model +type PrometheusNamespace struct { + // in: body + Body map[string][]PrometheusRuleGroup +} + +// swagger:model +type PrometheusRuleGroup struct { + Name string `yaml:"name"` + Interval model.Duration `yaml:"interval"` + Rules []PrometheusRule `yaml:"rules"` +} + +// swagger:model +type PrometheusRule struct { + Alert string `yaml:"alert,omitempty"` + Expr string `yaml:"expr"` + For *model.Duration `yaml:"for,omitempty"` + KeepFiringFor *model.Duration `yaml:"keep_firing_for,omitempty"` + Labels map[string]string `yaml:"labels,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty"` + Record string `yaml:"record,omitempty"` +} + +// swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusGetRuleGroup +type RouteConvertPrometheusDeleteRuleGroupParams struct { + // in: path + NamespaceTitle string + // in: path + Group string +} + +// swagger:parameters RouteConvertPrometheusDeleteNamespace RouteConvertPrometheusGetNamespace +type RouteConvertPrometheusDeleteNamespaceParams struct { + // in: path + NamespaceTitle string +} + +// swagger:model +type ConvertPrometheusResponse struct { + Status string `json:"status"` + ErrorType string `json:"errorType"` + Error string `json:"error"` +} diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index e8c4c20d008..26e062391cf 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -734,6 +734,20 @@ }, "type": "array" }, + "ConvertPrometheusResponse": { + "properties": { + "error": { + "type": "string" + }, + "errorType": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, "CounterResetHint": { "description": "or alternatively that we are dealing with a gauge histogram, where counter resets do not apply.", "format": "uint8", @@ -2940,6 +2954,70 @@ }, "type": "object" }, + "PrometheusNamespace": { + "properties": { + "Body": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/PrometheusRuleGroup" + }, + "type": "array" + }, + "description": "in: body", + "type": "object" + } + }, + "type": "object" + }, + "PrometheusRule": { + "properties": { + "Alert": { + "type": "string" + }, + "Annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Expr": { + "type": "string" + }, + "For": { + "type": "string" + }, + "KeepFiringFor": { + "type": "string" + }, + "Labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Record": { + "type": "string" + } + }, + "type": "object" + }, + "PrometheusRuleGroup": { + "properties": { + "Interval": { + "$ref": "#/definitions/Duration" + }, + "Name": { + "type": "string" + }, + "Rules": { + "items": { + "$ref": "#/definitions/PrometheusRule" + }, + "type": "array" + } + }, + "type": "object" + }, "Provenance": { "type": "string" }, @@ -4854,7 +4932,6 @@ "type": "object" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert", "type": "object" @@ -4979,7 +5056,6 @@ "type": "object" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence", "type": "object" @@ -6322,6 +6398,252 @@ ] } }, + "/convert/prometheus/config/v1/rules": { + "get": { + "operationId": "RouteConvertPrometheusGetRules", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "summary": "Gets all namespaces with their rule groups in Prometheus format.", + "tags": [ + "convert_prometheus" + ] + } + }, + "/convert/prometheus/config/v1/rules/{NamespaceTitle}": { + "delete": { + "operationId": "RouteConvertPrometheusDeleteNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Deletes all rule groups in the given namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusGetNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "summary": "Gets rules in prometheus format for a given namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "post": { + "consumes": [ + "application/yaml" + ], + "operationId": "RouteConvertPrometheusPostRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "x-datasource-uid", + "type": "string" + }, + { + "in": "header", + "name": "x-recording-rules-paused", + "type": "boolean" + }, + { + "in": "header", + "name": "x-alert-rules-paused", + "type": "boolean" + }, + { + "in": "body", + "name": "Body", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Creates or updates a rule group in Prometheus format.", + "tags": [ + "convert_prometheus" + ], + "x-raw-request": "true" + } + }, + "/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}": { + "delete": { + "operationId": "RouteConvertPrometheusDeleteRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Deletes a rule group in Prometheus format.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusGetRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "PrometheusRuleGroup", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "summary": "Gets a rule group in Prometheus format.", + "tags": [ + "convert_prometheus" + ] + } + }, "/prometheus/grafana/api/v1/alerts": { "get": { "description": "gets the current alerts", @@ -6886,6 +7208,12 @@ "schema": { "$ref": "#/definitions/ForbiddenError" } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } } }, "tags": [ diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index abe3364c501..f42d1f7c408 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1102,6 +1102,252 @@ } } }, + "/convert/prometheus/config/v1/rules": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets all namespaces with their rule groups in Prometheus format.", + "operationId": "RouteConvertPrometheusGetRules", + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } + } + }, + "/convert/prometheus/config/v1/rules/{NamespaceTitle}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets rules in prometheus format for a given namespace.", + "operationId": "RouteConvertPrometheusGetNamespace", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } + }, + "post": { + "consumes": [ + "application/yaml" + ], + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Creates or updates a rule group in Prometheus format.", + "operationId": "RouteConvertPrometheusPostRuleGroup", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "x-datasource-uid", + "in": "header" + }, + { + "type": "boolean", + "name": "x-recording-rules-paused", + "in": "header" + }, + { + "type": "boolean", + "name": "x-alert-rules-paused", + "in": "header" + }, + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + } + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "x-raw-request": "true" + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Deletes all rule groups in the given namespace.", + "operationId": "RouteConvertPrometheusDeleteNamespace", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + } + } + }, + "/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets a rule group in Prometheus format.", + "operationId": "RouteConvertPrometheusGetRuleGroup", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "PrometheusRuleGroup", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Deletes a rule group in Prometheus format.", + "operationId": "RouteConvertPrometheusDeleteRuleGroup", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + } + } + }, "/prometheus/grafana/api/v1/alerts": { "get": { "description": "gets the current alerts", @@ -1633,6 +1879,12 @@ "schema": { "$ref": "#/definitions/ForbiddenError" } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } } } }, @@ -4420,6 +4672,20 @@ "$ref": "#/definitions/EmbeddedContactPoint" } }, + "ConvertPrometheusResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "errorType": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "CounterResetHint": { "description": "or alternatively that we are dealing with a gauge histogram, where counter resets do not apply.", "type": "integer", @@ -6628,6 +6894,70 @@ } } }, + "PrometheusNamespace": { + "type": "object", + "properties": { + "Body": { + "description": "in: body", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + } + } + } + }, + "PrometheusRule": { + "type": "object", + "properties": { + "Alert": { + "type": "string" + }, + "Annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Expr": { + "type": "string" + }, + "For": { + "type": "string" + }, + "KeepFiringFor": { + "type": "string" + }, + "Labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Record": { + "type": "string" + } + } + }, + "PrometheusRuleGroup": { + "type": "object", + "properties": { + "Interval": { + "$ref": "#/definitions/Duration" + }, + "Name": { + "type": "string" + }, + "Rules": { + "type": "array", + "items": { + "$ref": "#/definitions/PrometheusRule" + } + } + } + }, "Provenance": { "type": "string" }, @@ -8542,7 +8872,6 @@ } }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "type": "array", "items": { "type": "object", @@ -8667,7 +8996,6 @@ } }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "type": "object", diff --git a/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache b/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache index f9902d265c7..635bcc68287 100644 --- a/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache +++ b/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache @@ -20,6 +20,14 @@ type {{classname}} interface { {{#operation}} } {{#operations}}{{#operation}} +{{#vendorExtensions.x-raw-request}} +func (f *{{classname}}Handler) {{nickname}}(ctx *contextmodel.ReqContext) response.Response { {{#hasPathParams}} + // Parse Path Parameters{{/hasPathParams}}{{#pathParams}} + {{paramName}}Param := web.Params(ctx.Req)[":{{baseName}}"]{{/pathParams}} + return f.handle{{nickname}}(ctx{{#pathParams}}, {{paramName}}Param{{/pathParams}}) +} +{{/vendorExtensions.x-raw-request}} +{{^vendorExtensions.x-raw-request}} func (f *{{classname}}Handler) {{nickname}}(ctx *contextmodel.ReqContext) response.Response { {{#hasPathParams}} // Parse Path Parameters{{/hasPathParams}}{{#pathParams}} {{paramName}}Param := web.Params(ctx.Req)[":{{baseName}}"]{{/pathParams}} @@ -31,6 +39,7 @@ func (f *{{classname}}Handler) {{nickname}}(ctx *contextmodel.ReqContext) respon } {{/bodyParams}}return f.handle{{nickname}}(ctx{{#bodyParams}}, conf{{/bodyParams}}{{#pathParams}}, {{paramName}}Param{{/pathParams}}) } +{{/vendorExtensions.x-raw-request}} {{/operation}}{{/operations}} func (api *API) Register{{classname}}Endpoints(srv {{classname}}, m *metrics.API) { diff --git a/public/api-merged.json b/public/api-merged.json index 3e2239131d4..04ffb5ef505 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -13882,6 +13882,20 @@ "$ref": "#/definitions/EmbeddedContactPoint" } }, + "ConvertPrometheusResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "errorType": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "CookiePreferences": { "type": "object", "properties": { @@ -18520,6 +18534,21 @@ } } }, + "PrometheusNamespace": { + "type": "object", + "properties": { + "Body": { + "description": "in: body", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + } + } + } + }, "PrometheusRemoteWriteTargetJSON": { "type": "object", "properties": { @@ -18534,6 +18563,55 @@ } } }, + "PrometheusRule": { + "type": "object", + "properties": { + "Alert": { + "type": "string" + }, + "Annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Expr": { + "type": "string" + }, + "For": { + "type": "string" + }, + "KeepFiringFor": { + "type": "string" + }, + "Labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Record": { + "type": "string" + } + } + }, + "PrometheusRuleGroup": { + "type": "object", + "properties": { + "Interval": { + "$ref": "#/definitions/Duration" + }, + "Name": { + "type": "string" + }, + "Rules": { + "type": "array", + "items": { + "$ref": "#/definitions/PrometheusRule" + } + } + } + }, "Provenance": { "type": "string" }, @@ -22483,7 +22561,6 @@ } }, "alertGroups": { - "description": "AlertGroups alert groups", "type": "array", "items": { "type": "object", diff --git a/public/openapi3.json b/public/openapi3.json index 89612da9cf1..3aa7a775761 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -3957,6 +3957,20 @@ }, "type": "array" }, + "ConvertPrometheusResponse": { + "properties": { + "error": { + "type": "string" + }, + "errorType": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, "CookiePreferences": { "properties": { "analytics": {}, @@ -8595,6 +8609,21 @@ }, "type": "object" }, + "PrometheusNamespace": { + "properties": { + "Body": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/PrometheusRuleGroup" + }, + "type": "array" + }, + "description": "in: body", + "type": "object" + } + }, + "type": "object" + }, "PrometheusRemoteWriteTargetJSON": { "properties": { "data_source_uid": { @@ -8609,6 +8638,55 @@ }, "type": "object" }, + "PrometheusRule": { + "properties": { + "Alert": { + "type": "string" + }, + "Annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Expr": { + "type": "string" + }, + "For": { + "type": "string" + }, + "KeepFiringFor": { + "type": "string" + }, + "Labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Record": { + "type": "string" + } + }, + "type": "object" + }, + "PrometheusRuleGroup": { + "properties": { + "Interval": { + "$ref": "#/components/schemas/Duration" + }, + "Name": { + "type": "string" + }, + "Rules": { + "items": { + "$ref": "#/components/schemas/PrometheusRule" + }, + "type": "array" + } + }, + "type": "object" + }, "Provenance": { "type": "string" }, @@ -12557,7 +12635,6 @@ "type": "object" }, "alertGroups": { - "description": "AlertGroups alert groups", "items": { "$ref": "#/components/schemas/alertGroup" }, From aca024bcbbef54c9a0689bfabf083eedbaa69a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Wed, 12 Feb 2025 09:59:21 +0100 Subject: [PATCH 512/894] fix(unified-storage): use dedicated mocks per storage type (#100389) --- pkg/apiserver/rest/dualwriter_mode1_test.go | 322 ++++++++------- pkg/apiserver/rest/dualwriter_mode2_test.go | 418 +++++++++++--------- pkg/apiserver/rest/dualwriter_mode3_test.go | 231 ++++++----- pkg/apiserver/rest/dualwriter_test.go | 21 +- pkg/apiserver/rest/storage_mocks_test.go | 65 ++- 5 files changed, 568 insertions(+), 489 deletions(-) diff --git a/pkg/apiserver/rest/dualwriter_mode1_test.go b/pkg/apiserver/rest/dualwriter_mode1_test.go index fdae37d76ca..e5504e4df4e 100644 --- a/pkg/apiserver/rest/dualwriter_mode1_test.go +++ b/pkg/apiserver/rest/dualwriter_mode1_test.go @@ -7,8 +7,8 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/meta" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -62,16 +62,15 @@ func TestMode1_Create(t *testing.T) { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, tt.input) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m) + tt.setupStorageFn(us.Mock) } dw := NewDualWriter(Mode1, ls, us, p, kind) @@ -79,14 +78,14 @@ func TestMode1_Create(t *testing.T) { obj, err := dw.Create(context.Background(), tt.input, func(context.Context, runtime.Object) error { return nil }, &metav1.CreateOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } acc, err := meta.Accessor(obj) - assert.NoError(t, err) - assert.Equal(t, acc.GetResourceVersion(), "1") - assert.NotEqual(t, obj, anotherObj) + require.NoError(t, err) + require.Equal(t, acc.GetResourceVersion(), "1") + require.NotEqual(t, obj, anotherObj) }) } } @@ -125,16 +124,15 @@ func TestMode1_CreateOnUnifiedStorage(t *testing.T) { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m) + tt.setupLegacyFn(ls.Mock) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m) + tt.setupStorageFn(us.Mock) } ctx := context.TODO() @@ -144,7 +142,7 @@ func TestMode1_CreateOnUnifiedStorage(t *testing.T) { dw := NewDualWriter(Mode1, ls, us, p, kind) err := dw.(*DualWriterMode1).createOnUnifiedStorage(ctx, func(context.Context, runtime.Object) error { return nil }, tt.input, &metav1.CreateOptions{}) - assert.NoError(t, err) + require.NoError(t, err) }) } } @@ -154,14 +152,12 @@ func TestMode1_Get(t *testing.T) { setupLegacyFn func(m *mock.Mock, name string) setupStorageFn func(m *mock.Mock, name string) name string - input string wantErr bool } tests := []testCase{ { - name: "get an object only in the legacy store", - input: "foo", + name: "should succeed when getting an object from LegacyStorage", setupLegacyFn: func(m *mock.Mock, name string) { m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) }, @@ -170,44 +166,56 @@ func TestMode1_Get(t *testing.T) { }, }, { - name: "error when getting an object in the legacy store fails", - input: "object-fail", + name: "should error when getting an object from LegacyStorage fails", setupLegacyFn: func(m *mock.Mock, name string) { m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error")) }, + setupStorageFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) + }, wantErr: true, }, + { + name: "should not error when getting an object from UnifiedStorage fails", + setupLegacyFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) + }, + setupStorageFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error")) + }, + }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, name) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, name) } dw := NewDualWriter(Mode1, ls, us, p, kind) - obj, err := dw.Get(context.Background(), tt.input, &metav1.GetOptions{}) + obj, err := dw.Get(context.Background(), name, &metav1.GetOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } us.AssertNotCalled(t, "Get", context.Background(), tt.name, &metav1.GetOptions{}) - assert.Equal(t, obj, exampleObj) - assert.NotEqual(t, obj, anotherObj) + require.Equal(t, obj, exampleObj) + require.NotEqual(t, obj, anotherObj) }) } } @@ -221,41 +229,39 @@ func TestMode1_GetFromUnifiedStorage(t *testing.T) { setupStorageFn func(m *mock.Mock, name string) ctx *context.Context name string - input string } tests := []testCase{ { - name: "Get from unified storage", - input: "foo", + name: "should succeed when getting an object from UnifiedStorage", setupStorageFn: func(m *mock.Mock, name string) { m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) }, }, { - name: "Get from unified storage works even if parent context is canceled", - input: "foo", - ctx: &ctxCanceled, + name: "should succeed when getting an object from UnifiedStorage even if parent context is canceled", + ctx: &ctxCanceled, setupStorageFn: func(m *mock.Mock, name string) { m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) }, }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, name) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, name) } ctx := context.TODO() @@ -264,8 +270,8 @@ func TestMode1_GetFromUnifiedStorage(t *testing.T) { } dw := NewDualWriter(Mode1, ls, us, p, kind) - err := dw.(*DualWriterMode1).getFromUnifiedStorage(ctx, exampleObj, tt.input, &metav1.GetOptions{}) - assert.NoError(t, err) + err := dw.(*DualWriterMode1).getFromUnifiedStorage(ctx, exampleObj, name, &metav1.GetOptions{}) + require.NoError(t, err) }) } } @@ -280,28 +286,39 @@ func TestMode1_List(t *testing.T) { tests := []testCase{ { - name: "error when listing an object in the legacy store is not implemented", + name: "should error when listing from LegacyStorage fails", setupLegacyFn: func(m *mock.Mock) { - m.On("List", mock.Anything, mock.Anything).Return(&example.PodList{}, errors.New("error")) + m.On("List", mock.Anything, mock.Anything).Return(nil, errors.New("error")) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(&example.PodList{}, nil) + }, + wantErr: true, + }, + { + name: "should not error when listing from UnifiedStorage fails", + setupLegacyFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(&example.PodList{}, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(nil, errors.New("error")) }, }, - // TODO: legacy list is missing } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m) + tt.setupLegacyFn(ls.Mock) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m) + tt.setupStorageFn(us.Mock) } dw := NewDualWriter(Mode1, ls, us, p, kind) @@ -309,7 +326,7 @@ func TestMode1_List(t *testing.T) { _, err := dw.List(context.Background(), &metainternalversion.ListOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } }) @@ -329,13 +346,16 @@ func TestMode1_ListFromUnifiedStorage(t *testing.T) { tests := []testCase{ { - name: "list from unified storage", + name: "should succeed when listing from UnifiedStorage", setupStorageFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(anotherList, nil) }, + setupLegacyFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(anotherList, nil) + }, }, { - name: "list from unified storage works even if parent context is canceled", + name: "should succeed when listing from UnifiedStorage even if parent context is canceled", ctx: &ctxCanceled, setupStorageFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(anotherList, nil) @@ -347,16 +367,15 @@ func TestMode1_ListFromUnifiedStorage(t *testing.T) { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m) + tt.setupLegacyFn(ls.Mock) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m) + tt.setupStorageFn(us.Mock) } ctx := context.TODO() @@ -367,7 +386,7 @@ func TestMode1_ListFromUnifiedStorage(t *testing.T) { dw := NewDualWriter(Mode1, ls, us, p, kind) err := dw.(*DualWriterMode1).listFromUnifiedStorage(ctx, &metainternalversion.ListOptions{}, anotherList) - assert.NoError(t, err) + require.NoError(t, err) }) } } @@ -377,56 +396,69 @@ func TestMode1_Delete(t *testing.T) { setupLegacyFn func(m *mock.Mock, name string) setupStorageFn func(m *mock.Mock, name string) name string - input string wantErr bool } tests := []testCase{ { - name: "deleting an object in the legacy store", - input: "foo", + name: "should succeed when deleting an object from LegacyStorage", setupLegacyFn: func(m *mock.Mock, name string) { m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, + setupStorageFn: func(m *mock.Mock, name string) { + m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, }, { - name: "error when deleting an object in the legacy store", - input: "object-fail", + name: "should error when deleting an object from LegacyStorage fails", setupLegacyFn: func(m *mock.Mock, name string) { m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) }, + setupStorageFn: func(m *mock.Mock, name string) { + m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, wantErr: true, }, + { + name: "should not error when deleting an object from UnifiedStorage fails", + setupLegacyFn: func(m *mock.Mock, name string) { + m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + setupStorageFn: func(m *mock.Mock, name string) { + m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) + }, + }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, name) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, name) } dw := NewDualWriter(Mode1, ls, us, p, kind) - obj, _, err := dw.Delete(context.Background(), tt.input, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) + obj, _, err := dw.Delete(context.Background(), name, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - us.AssertNotCalled(t, "Delete", context.Background(), tt.input, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) - assert.Equal(t, obj, exampleObj) - assert.NotEqual(t, obj, anotherObj) + us.AssertNotCalled(t, "Delete", context.Background(), name, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) + require.Equal(t, obj, exampleObj) + require.NotEqual(t, obj, anotherObj) }) } } @@ -440,39 +472,39 @@ func TestMode1_DeleteFromUnifiedStorage(t *testing.T) { setupLegacyFn func(m *mock.Mock, name string) setupStorageFn func(m *mock.Mock, name string) name string - input string } tests := []testCase{ { - name: "Delete from unified storage", - setupStorageFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + name: "should succeed when deleting an object from UnifiedStorage", + setupStorageFn: func(m *mock.Mock, name string) { + m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, }, { - name: "Delete from unified storage works even if parent context is canceled", + name: "should succeed when deleting an object from UnifiedStorage even if parent context is canceled", ctx: &ctxCanceled, - setupStorageFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + setupStorageFn: func(m *mock.Mock, name string) { + m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, name) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, name) } ctx := context.TODO() @@ -482,8 +514,8 @@ func TestMode1_DeleteFromUnifiedStorage(t *testing.T) { dw := NewDualWriter(Mode1, ls, us, p, kind) - err := dw.(*DualWriterMode1).deleteFromUnifiedStorage(ctx, exampleObj, tt.input, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) - assert.NoError(t, err) + err := dw.(*DualWriterMode1).deleteFromUnifiedStorage(ctx, exampleObj, name, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) + require.NoError(t, err) }) } } @@ -499,36 +531,51 @@ func TestMode1_DeleteCollection(t *testing.T) { tests := []testCase{ { - name: "deleting a collection in the legacy store", + name: "should succeed when deleting a collection from LegacyStorage", input: &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}}, setupLegacyFn: func(m *mock.Mock, input *metav1.DeleteOptions) { m.On("DeleteCollection", mock.Anything, mock.Anything, input, mock.Anything).Return(exampleObj, nil) }, + setupStorageFn: func(m *mock.Mock, input *metav1.DeleteOptions) { + m.On("DeleteCollection", mock.Anything, mock.Anything, input, mock.Anything).Return(exampleObj, nil) + }, }, { - name: "error deleting a collection in the legacy store", + name: "should error when deleting a collection from LegacyStorage fails", input: &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: "fail"}}, setupLegacyFn: func(m *mock.Mock, input *metav1.DeleteOptions) { m.On("DeleteCollection", mock.Anything, mock.Anything, input, mock.Anything).Return(nil, errors.New("error")) }, + setupStorageFn: func(m *mock.Mock, input *metav1.DeleteOptions) { + m.On("DeleteCollection", mock.Anything, mock.Anything, input, mock.Anything).Return(exampleObj, nil) + }, wantErr: true, }, + { + name: "should not error when deleting a collection from UnifiedStorage fails", + input: &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}}, + setupLegacyFn: func(m *mock.Mock, input *metav1.DeleteOptions) { + m.On("DeleteCollection", mock.Anything, mock.Anything, input, mock.Anything).Return(exampleObj, nil) + }, + setupStorageFn: func(m *mock.Mock, input *metav1.DeleteOptions) { + m.On("DeleteCollection", mock.Anything, mock.Anything, input, mock.Anything).Return(nil, errors.New("error")) + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, tt.input) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, tt.input) } dw := NewDualWriter(Mode1, ls, us, p, kind) @@ -536,13 +583,13 @@ func TestMode1_DeleteCollection(t *testing.T) { obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, tt.input, &metainternalversion.ListOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } us.AssertNotCalled(t, "DeleteCollection", context.Background(), tt.input, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) - assert.Equal(t, obj, exampleObj) - assert.NotEqual(t, obj, anotherObj) + require.Equal(t, obj, exampleObj) + require.NotEqual(t, obj, anotherObj) }) } } @@ -561,14 +608,14 @@ func TestMode1_DeleteCollectionFromUnifiedStorage(t *testing.T) { tests := []testCase{ { - name: "Delete Collection from unified storage", + name: "should succeed when deleting a collection from UnifiedStorage", input: &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}}, setupStorageFn: func(m *mock.Mock) { m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, nil) }, }, { - name: "Delete Collection from unified storage works even if parent context is canceled", + name: "should succeed when deleting a collection from UnifiedStorage even if parent context is canceled", input: &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}}, ctx: &ctxCanceled, setupStorageFn: func(m *mock.Mock) { @@ -581,16 +628,15 @@ func TestMode1_DeleteCollectionFromUnifiedStorage(t *testing.T) { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m) + tt.setupLegacyFn(ls.Mock) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m) + tt.setupStorageFn(us.Mock) } ctx := context.TODO() @@ -601,7 +647,7 @@ func TestMode1_DeleteCollectionFromUnifiedStorage(t *testing.T) { dw := NewDualWriter(Mode1, ls, us, p, kind) err := dw.(*DualWriterMode1).deleteCollectionFromUnifiedStorage(ctx, exampleObj, func(ctx context.Context, obj runtime.Object) error { return nil }, tt.input, &metainternalversion.ListOptions{}) - assert.NoError(t, err) + require.NoError(t, err) }) } } @@ -611,14 +657,12 @@ func TestMode1_Update(t *testing.T) { setupLegacyFn func(m *mock.Mock, input string) setupStorageFn func(m *mock.Mock, input string) name string - input string wantErr bool } tests := []testCase{ { - name: "update an object in legacy", - input: "foo", + name: "should succeed when updating an object in LegacyStorage", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, @@ -627,8 +671,7 @@ func TestMode1_Update(t *testing.T) { }, }, { - name: "error updating an object in legacy", - input: "object-fail", + name: "should error when updating an object in LegacyStorage fails", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) }, @@ -637,35 +680,45 @@ func TestMode1_Update(t *testing.T) { }, wantErr: true, }, + { + name: "should not error when updating an object in UnifiedStorage fails", + setupLegacyFn: func(m *mock.Mock, input string) { + m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + setupStorageFn: func(m *mock.Mock, input string) { + m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) + }, + }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, name) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, name) } dw := NewDualWriter(Mode1, ls, us, p, kind) - obj, _, err := dw.Update(context.Background(), tt.input, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) + obj, _, err := dw.Update(context.Background(), name, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, obj, exampleObj) - assert.NotEqual(t, obj, anotherObj) + require.Equal(t, obj, exampleObj) + require.NotEqual(t, obj, anotherObj) }) } } @@ -680,13 +733,11 @@ func TestMode1_UpdateOnUnifiedStorage(t *testing.T) { setupStorageFn func(m *mock.Mock, input string) setupGetFn func(m *mock.Mock, input string) name string - input string } tests := []testCase{ { - name: "Update on unified storage", - input: "foo", + name: "should succeed when updating an object on UnifiedStorage", setupStorageFn: func(m *mock.Mock, input string) { m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(anotherObj, false, nil) }, @@ -695,9 +746,8 @@ func TestMode1_UpdateOnUnifiedStorage(t *testing.T) { }, }, { - name: "Update on unified storage works even if parent context is canceled", - ctx: &ctxCanceled, - input: "foo", + name: "should succeed when updating an object on UnifiedStorage even if parent context is canceled", + ctx: &ctxCanceled, setupStorageFn: func(m *mock.Mock, input string) { m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(anotherObj, false, nil) }, @@ -707,24 +757,26 @@ func TestMode1_UpdateOnUnifiedStorage(t *testing.T) { }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, name) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, name) } if tt.setupGetFn != nil { - tt.setupGetFn(m, tt.input) + tt.setupGetFn(ls.Mock, name) + tt.setupGetFn(us.Mock, name) } ctx := context.TODO() @@ -734,8 +786,8 @@ func TestMode1_UpdateOnUnifiedStorage(t *testing.T) { dw := NewDualWriter(Mode1, ls, us, p, kind) - err := dw.(*DualWriterMode1).updateOnUnifiedStorageMode1(ctx, exampleObj, tt.input, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) - assert.NoError(t, err) + err := dw.(*DualWriterMode1).updateOnUnifiedStorageMode1(ctx, exampleObj, name, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) + require.NoError(t, err) }) } } diff --git a/pkg/apiserver/rest/dualwriter_mode2_test.go b/pkg/apiserver/rest/dualwriter_mode2_test.go index 4d150ac2ad7..6e67ac0fe59 100644 --- a/pkg/apiserver/rest/dualwriter_mode2_test.go +++ b/pkg/apiserver/rest/dualwriter_mode2_test.go @@ -29,21 +29,25 @@ func TestMode2_Create(t *testing.T) { tests := []testCase{ { - name: "creating an object in both the LegacyStorage and Storage", + name: "should create an object in both the LegacyStorage and Storage", input: exampleObj, setupLegacyFn: func(m *mock.Mock, input runtime.Object) { - m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(exampleObj, nil) + m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, nil) }, - setupStorageFn: func(m *mock.Mock, input runtime.Object) { - m.On("Create", mock.Anything, exampleObj, mock.Anything, mock.Anything).Return(exampleObj, nil) + setupStorageFn: func(m *mock.Mock, _ runtime.Object) { + // We don't use the input here, as the input is transformed before being passed to unified storage. + m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(exampleObj, nil) }, }, { - name: "error when creating object in the legacy store fails", + name: "should return an error when creating an object in the LegacyStorage fails", input: failingObj, setupLegacyFn: func(m *mock.Mock, input runtime.Object) { m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, errors.New("error")) }, + setupStorageFn: func(m *mock.Mock, input runtime.Object) { + m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, nil) + }, wantErr: true, }, } @@ -52,16 +56,15 @@ func TestMode2_Create(t *testing.T) { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, tt.input) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, tt.input) } dw := NewDualWriter(Mode2, ls, us, p, kind) @@ -89,7 +92,7 @@ func TestMode2_Get(t *testing.T) { tests := []testCase{ { - name: "getting an object from storage", + name: "should get an object from both the LegacyStorage and Storage", input: "foo", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Get", mock.Anything, input, mock.Anything).Return(exampleObj, nil) @@ -99,7 +102,7 @@ func TestMode2_Get(t *testing.T) { }, }, { - name: "object not present in storage but present in legacy store", + name: "should return an error when getting an object from the Storage fails", input: "foo", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Get", mock.Anything, input, mock.Anything).Return(exampleObj, nil) @@ -107,9 +110,22 @@ func TestMode2_Get(t *testing.T) { setupStorageFn: func(m *mock.Mock, input string) { m.On("Get", mock.Anything, input, mock.Anything).Return(nil, errors.New("error")) }, + wantErr: true, }, { - name: "error when getting object in both stores fails", + name: "should not error when object is not found in the Storage but found in the LegacyStorage", + input: "foo", + setupLegacyFn: func(m *mock.Mock, input string) { + m.On("Get", mock.Anything, input, mock.Anything).Return(exampleObj, nil) + }, + setupStorageFn: func(m *mock.Mock, input string) { + m.On("Get", mock.Anything, input, mock.Anything).Return(nil, apierrors.NewNotFound( + schema.GroupResource{Group: "", Resource: "pods"}, "not-found")) + }, + wantErr: true, + }, + { + name: "should return an error when getting an object from both the LegacyStorage and Storage fails", input: "object-fail", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Get", mock.Anything, input, mock.Anything).Return(nil, errors.New("error")) @@ -125,16 +141,15 @@ func TestMode2_Get(t *testing.T) { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, tt.input) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, tt.input) } dw := NewDualWriter(Mode2, ls, us, p, kind) @@ -163,7 +178,7 @@ func TestMode2_List(t *testing.T) { tests := []testCase{ { - name: "object present in both Storage and LegacyStorage", + name: "should return a list of objects from both the LegacyStorage and Storage", inputLegacy: exampleOption, setupLegacyFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(exampleList, nil) @@ -172,168 +187,25 @@ func TestMode2_List(t *testing.T) { m.On("List", mock.Anything, mock.Anything).Return(anotherList, nil) }, }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) - s := (Storage)(nil) - m := &mock.Mock{} - - ls := legacyStoreMock{m, l} - us := storageMock{m, s} - - if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m) - } - if tt.setupStorageFn != nil { - tt.setupStorageFn(m) - } - - dw := NewDualWriter(Mode2, ls, us, p, kind) - - obj, err := dw.List(context.Background(), &metainternalversion.ListOptions{}) - - if tt.wantErr { - require.Error(t, err) - return - } - require.Equal(t, exampleList, obj) - }) - } -} - -func TestMode2_Delete(t *testing.T) { - type testCase struct { - setupLegacyFn func(m *mock.Mock, input string) - setupStorageFn func(m *mock.Mock, input string) - name string - input string - wantErr bool - } - tests := - []testCase{ { - name: "delete in legacy and storage", - input: "foo", - setupLegacyFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) - }, - setupStorageFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) - }, - }, - { - name: "object delete in legacy not found, but found in storage", - input: "foo", - setupLegacyFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, "not-found-legacy", mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "not-found")) - }, - setupStorageFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) - }, - }, - { - name: " object delete in storage not found, but found in legacy", - input: "foo", - setupLegacyFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) - }, - setupStorageFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, "not-found-storage", mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "not-found")) - }, - }, - { - name: " object not found in both", - input: "object-fail", - setupLegacyFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "not-found")) - }, - setupStorageFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "not-found")) - }, - wantErr: true, - }, - { - name: " object delete error", - input: "object-fail", - setupLegacyFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) - }, - setupStorageFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) - s := (Storage)(nil) - m := &mock.Mock{} - - ls := legacyStoreMock{m, l} - us := storageMock{m, s} - - if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) - } - if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) - } - - dw := NewDualWriter(Mode2, ls, us, p, kind) - - obj, _, err := dw.Delete(context.Background(), tt.input, func(context.Context, runtime.Object) error { return nil }, &metav1.DeleteOptions{}) - - if tt.wantErr { - require.Error(t, err) - return - } - - require.Equal(t, obj, exampleObj) - require.NotEqual(t, obj, anotherObj) - }) - } -} - -func TestMode2_DeleteCollection(t *testing.T) { - type testCase struct { - setupLegacyFn func(m *mock.Mock) - setupStorageFn func(m *mock.Mock) - name string - input string - wantErr bool - } - tests := - []testCase{ - { - name: "deleting a collection in both stores", - input: "foo", + name: "should return an error when listing objects from the LegacyStorage fails", + inputLegacy: exampleOption, setupLegacyFn: func(m *mock.Mock) { - m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleList, nil) + m.On("List", mock.Anything, mock.Anything).Return(nil, errors.New("error")) }, setupStorageFn: func(m *mock.Mock) { - m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleList, nil) - }, - }, - { - name: "error deleting a collection in the storage when legacy store is successful", - input: "fail", - setupLegacyFn: func(m *mock.Mock) { - m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, nil) - }, - setupStorageFn: func(m *mock.Mock) { - m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error")) + m.On("List", mock.Anything, mock.Anything).Return(anotherList, nil) }, wantErr: true, }, { - name: "deleting a collection when error in legacy store", - input: "fail", + name: "should return an error when listing objects from the Storage fails", + inputLegacy: exampleOption, setupLegacyFn: func(m *mock.Mock) { - m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error")) + m.On("List", mock.Anything, mock.Anything).Return(exampleList, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(nil, errors.New("error")) }, wantErr: true, }, @@ -356,7 +228,177 @@ func TestMode2_DeleteCollection(t *testing.T) { dw := NewDualWriter(Mode2, ls, us, p, kind) - obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: tt.input}}, &metainternalversion.ListOptions{}) + obj, err := dw.List(context.Background(), &metainternalversion.ListOptions{}) + + if tt.wantErr { + require.Error(t, err) + return + } + require.Equal(t, exampleList, obj) + }) + } +} + +func TestMode2_Delete(t *testing.T) { + type testCase struct { + setupLegacyFn func(m *mock.Mock, input string) + setupStorageFn func(m *mock.Mock, input string) + name string + wantErr bool + } + tests := + []testCase{ + { + name: "should delete an object from both the LegacyStorage and Storage", + setupLegacyFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + setupStorageFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + }, + { + name: "should return an error when deleting an object from the LegacyStorage fails", + setupLegacyFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) + }, + setupStorageFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + wantErr: true, + }, + { + name: "should return an error when deleting an object from the Storage fails", + setupLegacyFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + setupStorageFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) + }, + wantErr: true, + }, + { + name: "should return an error when the object is not found in LegacyStorage", + setupLegacyFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, + apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, input)) + }, + setupStorageFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + wantErr: true, + }, + { + name: "should not return an error when the object is not found in Storage", + setupLegacyFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + setupStorageFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, + apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, input)) + }, + }, + { + name: "should return an error when deleting an object from both the LegacyStorage and Storage fails", + setupLegacyFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) + }, + setupStorageFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) + }, + wantErr: true, + }, + } + + name := "foo" + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := (LegacyStorage)(nil) + s := (Storage)(nil) + + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} + + if tt.setupLegacyFn != nil { + tt.setupLegacyFn(ls.Mock, name) + } + if tt.setupStorageFn != nil { + tt.setupStorageFn(us.Mock, name) + } + + dw := NewDualWriter(Mode2, ls, us, p, kind) + + obj, _, err := dw.Delete(context.Background(), name, func(context.Context, runtime.Object) error { return nil }, &metav1.DeleteOptions{}) + + if tt.wantErr { + require.Error(t, err) + return + } + + require.Equal(t, obj, exampleObj) + require.NotEqual(t, obj, anotherObj) + }) + } +} + +func TestMode2_DeleteCollection(t *testing.T) { + type testCase struct { + setupLegacyFn func(m *mock.Mock) + setupStorageFn func(m *mock.Mock) + name string + wantErr bool + } + tests := + []testCase{ + { + name: "should delete a collection from both the LegacyStorage and Storage", + setupLegacyFn: func(m *mock.Mock) { + m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleList, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleList, nil) + }, + }, + { + name: "should return an error when deleting a collection from the Storage fails", + setupLegacyFn: func(m *mock.Mock) { + m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleList, nil) + }, + setupStorageFn: func(m *mock.Mock) { + m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error")) + }, + wantErr: true, + }, + { + name: "should return an error when deleting a collection from the LegacyStorage fails", + setupLegacyFn: func(m *mock.Mock) { + m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error")) + }, + wantErr: true, + }, + } + + name := "foo" + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := (LegacyStorage)(nil) + s := (Storage)(nil) + + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} + + if tt.setupLegacyFn != nil { + tt.setupLegacyFn(ls.Mock) + } + if tt.setupStorageFn != nil { + tt.setupStorageFn(us.Mock) + } + + dw := NewDualWriter(Mode2, ls, us, p, kind) + + obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: name}}, &metainternalversion.ListOptions{}) if tt.wantErr { require.Error(t, err) @@ -373,14 +415,12 @@ func TestMode2_Update(t *testing.T) { setupLegacyFn func(m *mock.Mock, input string) setupStorageFn func(m *mock.Mock, input string) name string - input string wantErr bool } tests := []testCase{ { - name: "update an object in both stores", - input: "foo", + name: "should succeed when updating an object in both the LegacyStorage and Storage is successful", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, @@ -390,34 +430,44 @@ func TestMode2_Update(t *testing.T) { expectedObj: exampleObj, }, { - name: "error updating legacy store", - input: "object-fail", + name: "should return an error when updating the LegacyStorage fails", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) }, wantErr: true, }, + { + name: "should return an error when updating the Storage fails", + setupLegacyFn: func(m *mock.Mock, input string) { + m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + }, + setupStorageFn: func(m *mock.Mock, input string) { + m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) + }, + wantErr: true, + }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, name) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, name) } dw := NewDualWriter(Mode2, ls, us, p, kind) - obj, _, err := dw.Update(context.Background(), tt.input, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) + obj, _, err := dw.Update(context.Background(), name, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) if tt.wantErr { require.Error(t, err) diff --git a/pkg/apiserver/rest/dualwriter_mode3_test.go b/pkg/apiserver/rest/dualwriter_mode3_test.go index d5ddbe04a88..bc8635b2fd7 100644 --- a/pkg/apiserver/rest/dualwriter_mode3_test.go +++ b/pkg/apiserver/rest/dualwriter_mode3_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -26,17 +26,18 @@ func TestMode3_Create(t *testing.T) { tests := []testCase{ { - name: "creating an object in both the LegacyStorage and Storage", + name: "should succeed when creating an object in both the LegacyStorage and Storage", input: exampleObj, setupLegacyFn: func(m *mock.Mock, input runtime.Object) { - m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(exampleObj, nil).Once() + m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, nil).Once() }, - setupStorageFn: func(m *mock.Mock, input runtime.Object) { - m.On("Create", mock.Anything, exampleObj, mock.Anything, mock.Anything).Return(exampleObj, nil).Once() + setupStorageFn: func(m *mock.Mock, _ runtime.Object) { + // We don't use the input here, as the input is transformed before being passed to unified storage. + m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(exampleObj, nil).Once() }, }, { - name: "error when creating object in the legacy store fails", + name: "should return an error when creating an object in the legacy store fails", input: failingObj, setupLegacyFn: func(m *mock.Mock, input runtime.Object) { m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, errors.New("error")).Once() @@ -44,14 +45,15 @@ func TestMode3_Create(t *testing.T) { wantErr: true, }, { - name: "error when creating object in the unistore fails - legacy delete should be called", + name: "should return an error when creating an object in the unified store fails and delete from LegacyStorage", input: exampleObj, setupLegacyFn: func(m *mock.Mock, input runtime.Object) { m.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, true, nil).Once() - m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(exampleObj, nil).Once() + m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, nil).Once() }, - setupStorageFn: func(m *mock.Mock, input runtime.Object) { - m.On("Create", mock.Anything, exampleObj, mock.Anything, mock.Anything).Return(exampleObj, errors.New("error")).Once() + setupStorageFn: func(m *mock.Mock, _ runtime.Object) { + // We don't use the input here, as the input is transformed before being passed to unified storage. + m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(nil, errors.New("error")).Once() }, wantErr: true, }, @@ -61,16 +63,15 @@ func TestMode3_Create(t *testing.T) { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, tt.input) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, tt.input) } dw := NewDualWriter(Mode3, ls, us, p, kind) @@ -78,66 +79,83 @@ func TestMode3_Create(t *testing.T) { obj, err := dw.Create(context.Background(), tt.input, createFn, &metav1.CreateOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, exampleObj, obj) + require.Equal(t, exampleObj, obj) }) } } func TestMode3_Get(t *testing.T) { type testCase struct { + setupLegacyFn func(m *mock.Mock, name string) setupStorageFn func(m *mock.Mock, name string) name string - input string wantErr bool } tests := []testCase{ { - name: "get an object only in unified store", - input: "foo", + name: "should succeed when getting an object from both stores", + setupLegacyFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) + }, setupStorageFn: func(m *mock.Mock, name string) { m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) }, }, { - name: "error when getting an object in the unified store fails", - input: "object-fail", + name: "should return an error when getting an object in the unified store fails", + setupLegacyFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) + }, setupStorageFn: func(m *mock.Mock, name string) { m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error")) }, wantErr: true, }, + { + name: "should succeed when getting an object in the LegacyStorage fails", + setupLegacyFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error")) + }, + setupStorageFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) + }, + }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} + if tt.setupLegacyFn != nil { + tt.setupLegacyFn(ls.Mock, name) + } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, name) } p := prometheus.NewRegistry() dw := NewDualWriter(Mode3, ls, us, p, kind) - obj, err := dw.Get(context.Background(), tt.input, &metav1.GetOptions{}) + obj, err := dw.Get(context.Background(), name, &metav1.GetOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, obj, exampleObj) - assert.NotEqual(t, obj, anotherObj) + require.Equal(t, obj, exampleObj) + require.NotEqual(t, obj, anotherObj) }) } } @@ -150,38 +168,36 @@ func TestMode1_GetFromLegacyStorage(t *testing.T) { setupLegacyFn func(m *mock.Mock, name string) ctx *context.Context name string - input string } tests := []testCase{ { - name: "Get from legacy storage", - input: "foo", + name: "should succeed when getting an object from the LegacyStorage", setupLegacyFn: func(m *mock.Mock, name string) { m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) }, }, { - name: "Get from legacy storage works even if parent context is canceled", - input: "foo", - ctx: &ctxCanceled, + name: "should succeed when getting an object from the LegacyStorage even if parent context is canceled", + ctx: &ctxCanceled, setupLegacyFn: func(m *mock.Mock, name string) { m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) }, }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, name) } ctx := context.TODO() @@ -190,8 +206,8 @@ func TestMode1_GetFromLegacyStorage(t *testing.T) { } dw := NewDualWriter(Mode3, ls, us, p, kind) - err := dw.(*DualWriterMode3).getFromLegacyStorage(ctx, exampleObj, tt.input, &metav1.GetOptions{}) - assert.NoError(t, err) + err := dw.(*DualWriterMode3).getFromLegacyStorage(ctx, exampleObj, name, &metav1.GetOptions{}) + require.NoError(t, err) }) } } @@ -200,22 +216,19 @@ func TestMode3_List(t *testing.T) { type testCase struct { setupStorageFn func(m *mock.Mock, options *metainternalversion.ListOptions) name string - options *metainternalversion.ListOptions wantErr bool } tests := []testCase{ { - name: "error when listing an object in the unified store is not implemented", - options: &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "fail"}}, + name: "should return an error when listing an object in the UnifiedStorage is failing", setupStorageFn: func(m *mock.Mock, options *metainternalversion.ListOptions) { m.On("List", mock.Anything, options).Return(nil, errors.New("error")) }, wantErr: true, }, { - name: "list objects in the unified store", - options: &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}}, + name: "should succeed when listing objects in the UnifiedStorage is successful", setupStorageFn: func(m *mock.Mock, options *metainternalversion.ListOptions) { m.On("List", mock.Anything, options).Return(exampleList, nil) }, @@ -226,26 +239,25 @@ func TestMode3_List(t *testing.T) { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.options) + tt.setupStorageFn(us.Mock, &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}}) } dw := NewDualWriter(Mode3, ls, us, p, kind) - res, err := dw.List(context.Background(), tt.options) + res, err := dw.List(context.Background(), &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, exampleList, res) - assert.NotEqual(t, anotherList, res) + require.Equal(t, exampleList, res) + require.NotEqual(t, anotherList, res) }) } } @@ -255,14 +267,12 @@ func TestMode3_Delete(t *testing.T) { setupLegacyFn func(m *mock.Mock, input string) setupStorageFn func(m *mock.Mock, input string) name string - input string wantErr bool } tests := []testCase{ { - name: "delete in legacy and storage", - input: "foo", + name: "should succeed when deleting an object in both stores", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, @@ -271,39 +281,25 @@ func TestMode3_Delete(t *testing.T) { }, }, { - name: "object delete in legacy not found, but found in storage", - input: "foo", + name: "should succeed when deleting an object in the LegacyStorage is not found, but found in the UnifiedStorage", setupLegacyFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, "not-found-legacy", mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "not-found")) + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, input)) }, setupStorageFn: func(m *mock.Mock, input string) { m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, }, { - name: " object delete in storage not found, but found in legacy", - input: "foo", + name: "should succeed when deleting an object in the UnifiedStorage is not found in the LegacyStorage", setupLegacyFn: func(m *mock.Mock, input string) { + m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, input)) + }, + setupStorageFn: func(m *mock.Mock, input string) { m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, - setupStorageFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, "not-found-storage", mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "not-found")) - }, }, { - name: " object not found in both", - input: "object-fail", - setupLegacyFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "not-found")) - }, - setupStorageFn: func(m *mock.Mock, input string) { - m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "not-found")) - }, - wantErr: true, - }, - { - name: " object delete error", - input: "object-fail", + name: "should return an error when deleting an object in the LegacyStorage and UnifiedStorage is failing", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Delete", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")) }, @@ -314,33 +310,34 @@ func TestMode3_Delete(t *testing.T) { }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, name) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, name) } dw := NewDualWriter(Mode3, ls, us, p, kind) - obj, _, err := dw.Delete(context.Background(), tt.input, func(context.Context, runtime.Object) error { return nil }, &metav1.DeleteOptions{}) + obj, _, err := dw.Delete(context.Background(), name, func(context.Context, runtime.Object) error { return nil }, &metav1.DeleteOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, obj, exampleObj) - assert.NotEqual(t, obj, anotherObj) + require.Equal(t, obj, exampleObj) + require.NotEqual(t, obj, anotherObj) }) } } @@ -350,14 +347,12 @@ func TestMode3_DeleteCollection(t *testing.T) { setupLegacyFn func(m *mock.Mock) setupStorageFn func(m *mock.Mock) name string - input string wantErr bool } tests := []testCase{ { - name: "deleting a collection in both stores", - input: "foo", + name: "should succeed when deleting a collection in both stores", setupLegacyFn: func(m *mock.Mock) { m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleList, nil) }, @@ -366,8 +361,7 @@ func TestMode3_DeleteCollection(t *testing.T) { }, }, { - name: "error deleting a collection in the storage when legacy store is successful", - input: "foo", + name: "should return an error when deleting a collection in the storage fails and LegacyStorage is successful", setupLegacyFn: func(m *mock.Mock) { m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, nil) }, @@ -377,40 +371,40 @@ func TestMode3_DeleteCollection(t *testing.T) { wantErr: true, }, { - name: "error deleting a collection legacy store", - input: "fail", + name: "should return an error when deleting a collection in the LegacyStorage fails", setupLegacyFn: func(m *mock.Mock) { - m.On("DeleteCollection", mock.Anything, mock.Anything, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: "fail"}}, mock.Anything).Return(nil, errors.New("error")) + m.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error")) }, wantErr: true, }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m) + tt.setupLegacyFn(ls.Mock) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m) + tt.setupStorageFn(us.Mock) } dw := NewDualWriter(Mode3, ls, us, p, kind) - obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: tt.input}}, &metainternalversion.ListOptions{}) + obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: name}}, &metainternalversion.ListOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, exampleList, obj) + require.Equal(t, exampleList, obj) }) } } @@ -421,14 +415,12 @@ func TestMode3_Update(t *testing.T) { setupLegacyFn func(m *mock.Mock, input string) setupStorageFn func(m *mock.Mock, input string) name string - input string wantErr bool } tests := []testCase{ { - name: "update an object in both stores", - input: "foo", + name: "should succeed when updating an object in both stores", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil).Once() }, @@ -438,16 +430,14 @@ func TestMode3_Update(t *testing.T) { expectedObj: exampleObj, }, { - name: "error updating legacy store", - input: "object-fail", + name: "should return an error when updating an object in the LegacyStorage fails", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, false, errors.New("error")).Once() }, wantErr: true, }, { - name: "error updating unistore", - input: "object-fail", + name: "should return an error when updating an object in the UnifiedStorage fails", setupLegacyFn: func(m *mock.Mock, input string) { m.On("Update", mock.Anything, input, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil).Once() }, @@ -458,33 +448,34 @@ func TestMode3_Update(t *testing.T) { }, } + name := "foo" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + ls := legacyStoreMock{&mock.Mock{}, l} + us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { - tt.setupLegacyFn(m, tt.input) + tt.setupLegacyFn(ls.Mock, name) } if tt.setupStorageFn != nil { - tt.setupStorageFn(m, tt.input) + tt.setupStorageFn(us.Mock, name) } dw := NewDualWriter(Mode3, ls, us, p, kind) - obj, _, err := dw.Update(context.Background(), tt.input, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) + obj, _, err := dw.Update(context.Background(), name, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.Equal(t, tt.expectedObj, obj) - assert.NotEqual(t, anotherObj, obj) + require.Equal(t, tt.expectedObj, obj) + require.NotEqual(t, anotherObj, obj) }) } } diff --git a/pkg/apiserver/rest/dualwriter_test.go b/pkg/apiserver/rest/dualwriter_test.go index e299ae91e3f..44f969351c1 100644 --- a/pkg/apiserver/rest/dualwriter_test.go +++ b/pkg/apiserver/rest/dualwriter_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/apis/example" @@ -57,13 +57,16 @@ func TestSetDualWritingMode(t *testing.T) { for _, tt := range tests { l := (LegacyStorage)(nil) s := (Storage)(nil) - m := &mock.Mock{} - m.On("List", mock.Anything, mock.Anything).Return(exampleList, nil) - m.On("List", mock.Anything, mock.Anything).Return(anotherList, nil) + sm := &mock.Mock{} + sm.On("List", mock.Anything, mock.Anything).Return(anotherList, nil) + sm.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + sm.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) + us := storageMock{sm, s} - ls := legacyStoreMock{m, l} - us := storageMock{m, s} + lm := &mock.Mock{} + lm.On("List", mock.Anything, mock.Anything).Return(exampleList, nil) + ls := legacyStoreMock{lm, l} dwMode, err := SetDualWritingMode(context.Background(), tt.kvStore, &SyncerConfig{ LegacyStorage: ls, @@ -77,8 +80,8 @@ func TestSetDualWritingMode(t *testing.T) { DataSyncerRecordsLimit: 1000, DataSyncerInterval: time.Hour, }) - assert.NoError(t, err) - assert.Equal(t, tt.expectedMode, dwMode) + require.NoError(t, err) + require.Equal(t, tt.expectedMode, dwMode) } } @@ -121,7 +124,7 @@ func TestCompare(t *testing.T) { } for _, tt := range testCase { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, Compare(tt.input1, tt.input2)) + require.Equal(t, tt.expected, Compare(tt.input1, tt.input2)) }) } } diff --git a/pkg/apiserver/rest/storage_mocks_test.go b/pkg/apiserver/rest/storage_mocks_test.go index 5e818b99560..582bcdc067c 100644 --- a/pkg/apiserver/rest/storage_mocks_test.go +++ b/pkg/apiserver/rest/storage_mocks_test.go @@ -5,7 +5,6 @@ import ( "errors" "github.com/stretchr/testify/mock" - "k8s.io/apimachinery/pkg/api/meta" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -30,8 +29,8 @@ func (m legacyStoreMock) Get(ctx context.Context, name string, options *metav1.G } args := m.Called(ctx, name, options) - if name == "object-fail" { - return nil, args.Error(1) + if err := args.Get(1); err != nil { + return nil, err.(error) } return args.Get(0).(runtime.Object), args.Error(1) } @@ -44,13 +43,8 @@ func (m legacyStoreMock) Create(ctx context.Context, obj runtime.Object, createV } args := m.Called(ctx, obj, createValidation, options) - acc, err := meta.Accessor(obj) - if err != nil { - return nil, args.Error(1) - } - name := acc.GetName() - if name == "object-fail" { - return nil, args.Error(1) + if err := args.Get(1); err != nil { + return nil, err.(error) } return args.Get(0).(runtime.Object), args.Error(1) } @@ -63,8 +57,8 @@ func (m legacyStoreMock) List(ctx context.Context, options *metainternalversion. } args := m.Called(ctx, options) - if options.Kind == "fail" { - return nil, args.Error(1) + if err := args.Get(1); err != nil { + return nil, err.(error) } return args.Get(0).(runtime.Object), args.Error(1) } @@ -80,8 +74,8 @@ func (m legacyStoreMock) Update(ctx context.Context, name string, objInfo rest.U default: } args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) - if name == "object-fail" { - return nil, false, args.Error(2) + if err := args.Get(2); err != nil { + return nil, false, err.(error) } return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) } @@ -94,11 +88,8 @@ func (m legacyStoreMock) Delete(ctx context.Context, name string, deleteValidati } args := m.Called(ctx, name, deleteValidation, options) - if name == "object-fail" { - return nil, false, args.Error(2) - } - if name == "not-found-legacy" { - return nil, false, args.Error(2) + if err := args.Get(2); err != nil { + return nil, false, err.(error) } return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) } @@ -110,8 +101,8 @@ func (m legacyStoreMock) DeleteCollection(ctx context.Context, deleteValidation default: } args := m.Called(ctx, deleteValidation, options, listOptions) - if options.Kind == "fail" { - return nil, args.Error(1) + if err := args.Get(1); err != nil { + return nil, err.(error) } return args.Get(0).(runtime.Object), args.Error(1) } @@ -125,11 +116,8 @@ func (m storageMock) Get(ctx context.Context, name string, options *metav1.GetOp } args := m.Called(ctx, name, options) - if name == "object-fail" { - return nil, args.Error(1) - } - if name == "not-found" { - return nil, args.Error(1) + if err := args.Get(1); err != nil { + return nil, err.(error) } return args.Get(0).(runtime.Object), args.Error(1) } @@ -142,13 +130,8 @@ func (m storageMock) Create(ctx context.Context, obj runtime.Object, createValid } args := m.Called(ctx, obj, createValidation, options) - acc, err := meta.Accessor(obj) - if err != nil { - return nil, args.Error(1) - } - name := acc.GetName() - if name == "object-fail" { - return nil, args.Error(1) + if err := args.Get(1); err != nil { + return nil, err.(error) } return args.Get(0).(runtime.Object), args.Error(1) } @@ -161,8 +144,8 @@ func (m storageMock) List(ctx context.Context, options *metainternalversion.List } args := m.Called(ctx, options) - if options.Kind == "fail" { - return nil, args.Error(1) + if err := args.Get(1); err != nil { + return nil, err.(error) } return args.Get(0).(runtime.Object), args.Error(1) } @@ -179,8 +162,8 @@ func (m storageMock) Update(ctx context.Context, name string, objInfo rest.Updat } args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) - if name == "object-fail" { - return nil, false, args.Error(2) + if err := args.Get(2); err != nil { + return nil, false, err.(error) } return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) } @@ -193,8 +176,8 @@ func (m storageMock) Delete(ctx context.Context, name string, deleteValidation r } args := m.Called(ctx, name, deleteValidation, options) - if name == "object-fail" { - return nil, false, args.Error(2) + if err := args.Get(2); err != nil { + return nil, false, err.(error) } return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) } @@ -207,8 +190,8 @@ func (m storageMock) DeleteCollection(ctx context.Context, deleteValidation rest } args := m.Called(ctx, deleteValidation, options, listOptions) - if options.Kind == "fail" { - return nil, args.Error(1) + if err := args.Get(1); err != nil { + return nil, err.(error) } return args.Get(0).(runtime.Object), args.Error(1) } From a0701a42f1f290e3eb94cc4067e6b5e93e75b056 Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Wed, 12 Feb 2025 10:11:52 +0100 Subject: [PATCH 513/894] APIServer: Propagate a new context with limited information (#100374) * APIServer: Propagate a new context with limited information * APIServer: Remove error return * APIServer: Test that context propagation does fork * APIServer: Fix golangci-lint lints * chore: make update-workspace --- .../responsewriter/responsewriter.go | 81 +++++++++++++++++++ .../responsewriter/responsewriter_test.go | 40 +++++++++ pkg/apiserver/go.mod | 5 +- pkg/apiserver/go.sum | 2 + 4 files changed, 127 insertions(+), 1 deletion(-) diff --git a/pkg/apiserver/endpoints/responsewriter/responsewriter.go b/pkg/apiserver/endpoints/responsewriter/responsewriter.go index 7bba579c9ea..a076340d7bb 100644 --- a/pkg/apiserver/endpoints/responsewriter/responsewriter.go +++ b/pkg/apiserver/endpoints/responsewriter/responsewriter.go @@ -2,13 +2,19 @@ package responsewriter import ( "bufio" + "context" "errors" "fmt" "io" "net/http" "sync/atomic" + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/responsewriter" + "k8s.io/component-base/tracing" "k8s.io/klog/v2" ) @@ -27,6 +33,13 @@ func WrapHandler(handler http.Handler) func(req *http.Request) (*http.Response, // so the client will be responsible for closing the response body. //nolint:bodyclose return func(req *http.Request) (*http.Response, error) { + ctx, cancel, err := createLimitedContext(req) + if err != nil { + return nil, err + } + defer cancel() + req = req.WithContext(ctx) // returns a shallow copy, so we can't do it as part of the adapter. + w := NewAdapter(req) go func() { handler.ServeHTTP(w, req) @@ -39,6 +52,74 @@ func WrapHandler(handler http.Handler) func(req *http.Request) (*http.Response, } } +// createLimitedContext creates a new context based on the the req's. +// It contains vital information such as a logger for the driver of the request, a user for auth, tracing, and deadlines. It propagates the parent's cancellation. +func createLimitedContext(req *http.Request) (context.Context, context.CancelFunc, error) { + refCtx := req.Context() + newCtx := context.Background() + + if ns, ok := request.NamespaceFrom(refCtx); ok { + newCtx = request.WithNamespace(newCtx, ns) + } + if signal := request.ServerShutdownSignalFrom(refCtx); signal != nil { + newCtx = request.WithServerShutdownSignal(newCtx, signal) + } + + requester, _ := identity.GetRequester(refCtx) + if requester != nil { + newCtx = identity.WithRequester(newCtx, requester) + } + + usr, ok := request.UserFrom(refCtx) + if !ok && requester != nil { + // add in k8s user if not there yet + var ok bool + usr, ok = requester.(user.Info) + if !ok { + return nil, nil, fmt.Errorf("could not convert user to Kubernetes user") + } + } + if ok { + newCtx = request.WithUser(newCtx, usr) + } + + // App SDK logger + appLogger := logging.FromContext(refCtx) + newCtx = logging.Context(newCtx, appLogger) + // Klog logger + klogger := klog.FromContext(refCtx) + if klogger.Enabled() { + newCtx = klog.NewContext(newCtx, klogger) + } + + // The tracing package deals with both k8s trace and otel. + if span := tracing.SpanFromContext(refCtx); span != nil && *span != (tracing.Span{}) { + newCtx = tracing.ContextWithSpan(newCtx, span) + } + + deadlineCancel := context.CancelFunc(func() {}) + if deadline, ok := refCtx.Deadline(); ok { + newCtx, deadlineCancel = context.WithDeadline(newCtx, deadline) + } + + newCtx, cancel := context.WithCancelCause(newCtx) + // We intentionally do not defer a cancel(nil) here. It wouldn't make sense to cancel until (*ResponseAdapter).Close() is called. + go func() { // Even context's own impls do goroutines for this type of pattern. + select { + case <-newCtx.Done(): + // We don't have to do anything! + case <-refCtx.Done(): + cancel(context.Cause(refCtx)) + } + deadlineCancel() + }() + + return newCtx, context.CancelFunc(func() { + cancel(nil) + deadlineCancel() + }), nil +} + // ResponseAdapter is an implementation of [http.ResponseWriter] that allows conversion to a [http.Response]. type ResponseAdapter struct { req *http.Request diff --git a/pkg/apiserver/endpoints/responsewriter/responsewriter_test.go b/pkg/apiserver/endpoints/responsewriter/responsewriter_test.go index 7cfbd015fcf..02ef220e8b2 100644 --- a/pkg/apiserver/endpoints/responsewriter/responsewriter_test.go +++ b/pkg/apiserver/endpoints/responsewriter/responsewriter_test.go @@ -10,6 +10,8 @@ import ( "time" "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" grafanaresponsewriter "github.com/grafana/grafana/pkg/apiserver/endpoints/responsewriter" ) @@ -158,6 +160,44 @@ func TestResponseAdapter(t *testing.T) { } wg.Wait() }) + + t.Run("should fork the context", func(t *testing.T) { + t.Parallel() + + type K int + var key K + baseCtx := context.Background() + baseCtx = context.WithValue(baseCtx, key, "hello, world!") // we expect this one not to be sent to the inner handler. + + expectedUsr := &user.DefaultInfo{Name: "hello, world!"} + baseCtx = request.WithUser(baseCtx, expectedUsr) + // There are more keys to consider, but this should be sufficient to decide that we do actually propagate select data across. + + client := &http.Client{ + Transport: &roundTripperFunc{ + ready: make(chan struct{}), + // ignore the lint error because the response is passed directly to the client, + // so the client will be responsible for closing the response body. + //nolint:bodyclose + fn: grafanaresponsewriter.WrapHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Nil(t, r.Context().Value(key), "inner handler should not have a value for key of type K") + usr, ok := request.UserFrom(r.Context()) + require.True(t, ok, "no user found in request context") + require.Equal(t, expectedUsr.Name, usr.GetName(), "user data was not propagated through request context") + + _, err := w.Write([]byte("OK")) + require.NoError(t, err) + })), + }, + } + + req, err := http.NewRequestWithContext(baseCtx, http.MethodGet, "/", nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err, "request should not fail") + require.NoError(t, resp.Body.Close()) + }) } func syncHandler(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index dfa02fe3c7c..a4d60df1efe 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -1,10 +1,13 @@ module github.com/grafana/grafana/pkg/apiserver -go 1.23.1 +go 1.23.4 + +toolchain go1.23.6 require ( github.com/google/go-cmp v0.6.0 github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c + 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 github.com/stretchr/testify v1.10.0 diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 599ae15a527..8204c0082b3 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -81,6 +81,8 @@ 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/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/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= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1/go.mod h1:DkxMin+qOh1Fgkxfbt+CUfBqqsCQJMG9op8Os/irBPA= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= From eb4c428d4e02bea12158813f0b97c97732c45ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edvard=20Falksk=C3=A4r?= <457523+edvard-falkskar@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:28:44 +0100 Subject: [PATCH 514/894] NodeGraph: Improve view traces for uninstrumented services (#98442) * NodeGraph: Improve view traces for uninstrumented services * Switch to onBuildUrl and more peer attributes * Update unit tests * Added test for new logic * Open traces in same tab * Update the tests * bring back internal link * Update public/app/plugins/datasource/tempo/datasource.ts Co-authored-by: Joey <90795735+joey-grafana@users.noreply.github.com> * Revert export of generateInternalHref * Update tests after change from onBuildUrl to query function --------- Co-authored-by: Domas Lapinskas Co-authored-by: Joey <90795735+joey-grafana@users.noreply.github.com> --- packages/grafana-data/src/types/dataLink.ts | 3 +- packages/grafana-data/src/utils/dataLinks.ts | 6 +- packages/grafana-data/src/utils/nodeGraph.ts | 3 + .../datasource/tempo/datasource.test.ts | 181 ++++++++++++------ .../plugins/datasource/tempo/datasource.ts | 116 ++++++++--- .../datasource/tempo/graphTransform.test.ts | 60 ++++++ .../datasource/tempo/graphTransform.ts | 26 +++ public/app/plugins/panel/nodeGraph/types.ts | 1 + .../app/plugins/panel/nodeGraph/utils.test.ts | 5 +- public/app/plugins/panel/nodeGraph/utils.ts | 18 +- 10 files changed, 333 insertions(+), 86 deletions(-) diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 14ee3ad4301..16d4edbff43 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -1,3 +1,4 @@ +import { ScopedVars } from './ScopedVars'; import { ExploreCorrelationHelperData, ExplorePanelsState } from './explore'; import { InterpolateFunction } from './panel'; import { DataQuery } from './query'; @@ -80,7 +81,7 @@ export interface DataLinkTransformationConfig { /** @internal */ export interface InternalDataLink { - query: T; + query: T | ((options: { replaceVariables: InterpolateFunction; scopedVars: ScopedVars }) => T); datasourceUid: string; datasourceName: string; // used as a title if `DataLink.title` is empty panelsState?: ExplorePanelsState; diff --git a/packages/grafana-data/src/utils/dataLinks.ts b/packages/grafana-data/src/utils/dataLinks.ts index 0ca5e4e64f1..51dac431148 100644 --- a/packages/grafana-data/src/utils/dataLinks.ts +++ b/packages/grafana-data/src/utils/dataLinks.ts @@ -38,7 +38,11 @@ export type LinkToExploreOptions = { export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkModel { const { onClickFn, replaceVariables, link, scopedVars, range, field, internalLink } = options; - const interpolatedQuery = interpolateObject(link.internal?.query, scopedVars, replaceVariables); + const query = + typeof link.internal?.query === 'function' + ? link.internal.query({ replaceVariables, scopedVars }) + : internalLink.query; + const interpolatedQuery = interpolateObject(query, scopedVars, replaceVariables); const interpolatedPanelsState = interpolateObject(link.internal?.panelsState, scopedVars, replaceVariables); const interpolatedCorrelationData = interpolateObject(link.meta?.correlationData, scopedVars, replaceVariables); const title = link.title ? link.title : internalLink.datasourceName; diff --git a/packages/grafana-data/src/utils/nodeGraph.ts b/packages/grafana-data/src/utils/nodeGraph.ts index e986804e89a..b3241370e5e 100644 --- a/packages/grafana-data/src/utils/nodeGraph.ts +++ b/packages/grafana-data/src/utils/nodeGraph.ts @@ -44,4 +44,7 @@ export enum NodeGraphDataFrameFieldNames { // Supplies a fixed Y position for the node to have in the finished graph. fixedY = 'fixedy', + + // Whether the node is instrumented or not + isInstrumented = 'isinstrumented', } diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index 4289a20e1cc..c51e27fdec8 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -15,6 +15,8 @@ import { DataQueryRequest, getTimeZone, PluginMetaInfo, + DataLink, + NodeGraphDataFrameFieldNames, } from '@grafana/data'; import { BackendDataSourceResponse, @@ -544,6 +546,32 @@ describe('Tempo service graph view', () => { expect(response.data[1].fields[0]?.config?.links?.length).toBeGreaterThan(0); expect(response.data[1].fields[0]?.config?.links).toEqual(serviceGraphLinks); + const viewServicesLink = response.data[1].fields[0]?.config?.links.find( + (link: DataLink) => link.title === 'View traces' + ); + expect(viewServicesLink).toBeDefined(); + expect(viewServicesLink.internal.query({ replaceVariables: replaceVariablesInstrumented })).toEqual({ + refId: 'A', + queryType: 'traceqlSearch', + filters: [ + { + id: 'service-name', + operator: '=', + scope: 'resource', + tag: 'service.name', + value: 'my-service', + valueType: 'string', + }, + ], + }); + expect(viewServicesLink.internal.query({ replaceVariables: replaceVariablesUninstrumented })).toEqual({ + refId: 'A', + queryType: 'traceql', + filters: [], + query: + '{span.db.name="my-service" || span.db.system="my-service" || span.peer.service="my-service" || span.messaging.system="my-service" || span.net.peer.name="my-service"}', + }); + expect(response.data[2].name).toBe('Edges'); expect(response.data[2].fields[0].values.length).toBe(2); }); @@ -584,14 +612,26 @@ describe('Tempo service graph view', () => { 'sum by (client, server) (rate(traces_service_graph_request_server_seconds_sum{ foo="bar" }[$__range]))' ); expect(nthQuery(0).targets[1].expr).toBe( - 'sum by (client, server) (rate(traces_service_graph_request_total{ foo="bar" }[$__range]))' + 'group by (client, connection_type, server) (traces_service_graph_request_server_seconds_sum{ foo="bar" })' ); expect(nthQuery(0).targets[2].expr).toBe( - 'sum by (client, server) (rate(traces_service_graph_request_failed_total{ foo="bar" }[$__range]))' + 'sum by (client, server) (rate(traces_service_graph_request_total{ foo="bar" }[$__range]))' ); expect(nthQuery(0).targets[3].expr).toBe( + 'group by (client, connection_type, server) (traces_service_graph_request_total{ foo="bar" })' + ); + expect(nthQuery(0).targets[4].expr).toBe( + 'sum by (client, server) (rate(traces_service_graph_request_failed_total{ foo="bar" }[$__range]))' + ); + expect(nthQuery(0).targets[5].expr).toBe( + 'group by (client, connection_type, server) (traces_service_graph_request_failed_total{ foo="bar" })' + ); + expect(nthQuery(0).targets[6].expr).toBe( 'sum by (client, server) (rate(traces_service_graph_request_server_seconds_bucket{ foo="bar" }[$__range]))' ); + expect(nthQuery(0).targets[7].expr).toBe( + 'group by (client, connection_type, server) (traces_service_graph_request_server_seconds_bucket{ foo="bar" })' + ); }); it('runs correct queries with multiple serviceMapQuery defined', async () => { @@ -632,14 +672,26 @@ describe('Tempo service graph view', () => { 'sum by (client, server) (rate(traces_service_graph_request_server_seconds_sum{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_server_seconds_sum{baz="bad"}[$__range]))' ); expect(nthQuery(0).targets[1].expr).toBe( - 'sum by (client, server) (rate(traces_service_graph_request_total{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_total{baz="bad"}[$__range]))' + 'group by (client, connection_type, server) (traces_service_graph_request_server_seconds_sum{ foo="bar" }) OR group by (client, connection_type, server) (traces_service_graph_request_server_seconds_sum{baz="bad"})' ); expect(nthQuery(0).targets[2].expr).toBe( - 'sum by (client, server) (rate(traces_service_graph_request_failed_total{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_failed_total{baz="bad"}[$__range]))' + 'sum by (client, server) (rate(traces_service_graph_request_total{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_total{baz="bad"}[$__range]))' ); expect(nthQuery(0).targets[3].expr).toBe( + 'group by (client, connection_type, server) (traces_service_graph_request_total{ foo="bar" }) OR group by (client, connection_type, server) (traces_service_graph_request_total{baz="bad"})' + ); + expect(nthQuery(0).targets[4].expr).toBe( + 'sum by (client, server) (rate(traces_service_graph_request_failed_total{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_failed_total{baz="bad"}[$__range]))' + ); + expect(nthQuery(0).targets[5].expr).toBe( + 'group by (client, connection_type, server) (traces_service_graph_request_failed_total{ foo="bar" }) OR group by (client, connection_type, server) (traces_service_graph_request_failed_total{baz="bad"})' + ); + expect(nthQuery(0).targets[6].expr).toBe( 'sum by (client, server) (rate(traces_service_graph_request_server_seconds_bucket{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_server_seconds_bucket{baz="bad"}[$__range]))' ); + expect(nthQuery(0).targets[7].expr).toBe( + 'group by (client, connection_type, server) (traces_service_graph_request_server_seconds_bucket{ foo="bar" }) OR group by (client, connection_type, server) (traces_service_graph_request_server_seconds_bucket{baz="bad"})' + ); }); it('should build expr correctly', () => { @@ -803,27 +855,33 @@ describe('Tempo service graph view', () => { url: '', title: 'View traces', internal: { - query: { - refId: 'A', - queryType: 'traceqlSearch', - filters: [ - { - id: 'service-name', - operator: '=', - scope: 'resource', - tag: 'service.name', - value: '${__data.fields.target}', - valueType: 'string', - }, - ], - }, - datasourceUid: 'EbPO1fYnz', datasourceName: '', + datasourceUid: 'EbPO1fYnz', + query: expect.any(Function), }, }, ], }; expect(fieldConfig).toStrictEqual(resultObj); + + const viewServicesLink: DataLink | undefined = fieldConfig.links.find( + (link: DataLink) => link.title === 'View traces' + ); + expect(viewServicesLink).toBeDefined(); + expect(viewServicesLink!.internal!.query({ replaceVariables: replaceVariablesInstrumented })).toEqual({ + refId: 'A', + queryType: 'traceqlSearch', + filters: [ + { + id: 'service-name', + operator: '=', + scope: 'resource', + tag: 'service.name', + value: 'my-service', + valueType: 'string', + }, + ], + }); }); it('should get field config correctly when namespaces are present', () => { @@ -894,35 +952,41 @@ describe('Tempo service graph view', () => { url: '', title: 'View traces', internal: { - query: { - queryType: 'traceqlSearch', - refId: 'A', - filters: [ - { - id: 'service-namespace', - operator: '=', - scope: 'resource', - tag: 'service.namespace', - value: '${__data.fields.targetNamespace}', - valueType: 'string', - }, - { - id: 'service-name', - operator: '=', - scope: 'resource', - tag: 'service.name', - value: '${__data.fields.targetName}', - valueType: 'string', - }, - ], - }, - datasourceUid: 'EbPO1fYnz', datasourceName: '', + datasourceUid: 'EbPO1fYnz', + query: expect.any(Function), }, }, ], }; expect(fieldConfig).toStrictEqual(resultObj); + + const viewServicesLink: DataLink | undefined = fieldConfig.links.find( + (link: DataLink) => link.title === 'View traces' + ); + expect(viewServicesLink).toBeDefined(); + expect(viewServicesLink!.internal!.query({ replaceVariables: replaceVariablesInstrumented })).toEqual({ + refId: 'A', + queryType: 'traceqlSearch', + filters: [ + { + id: 'service-namespace', + operator: '=', + scope: 'resource', + tag: 'service.namespace', + value: 'my-namespace', + valueType: 'string', + }, + { + id: 'service-name', + operator: '=', + scope: 'resource', + tag: 'service.name', + value: 'my-service', + valueType: 'string', + }, + ], + }); }); it('should get rate aligned values correctly', () => { @@ -1435,26 +1499,31 @@ const serviceGraphLinks = [ url: '', title: 'View traces', internal: { - query: { - refId: 'A', - queryType: 'traceqlSearch', - filters: [ - { - id: 'service-name', - operator: '=', - scope: 'resource', - tag: 'service.name', - value: '${__data.fields.id}', - valueType: 'string', - }, - ], - } as TempoQuery, + query: expect.any(Function), datasourceUid: 'gdev-tempo', datasourceName: 'Tempo', }, }, ]; +const replaceVariablesInstrumented = (variable: string): string => { + const variables: Record = { + [`\${__data.fields.${NodeGraphDataFrameFieldNames.title}}`]: 'my-service', + [`\${__data.fields.${NodeGraphDataFrameFieldNames.subTitle}}`]: 'my-namespace', + [`\${__data.fields.${NodeGraphDataFrameFieldNames.isInstrumented}}`]: 'true', + }; + return variables[variable]; +}; + +const replaceVariablesUninstrumented = (variable: string): string => { + const variables: Record = { + [`\${__data.fields.${NodeGraphDataFrameFieldNames.title}}`]: 'my-service', + [`\${__data.fields.${NodeGraphDataFrameFieldNames.subTitle}}`]: 'my-namespace', + [`\${__data.fields.${NodeGraphDataFrameFieldNames.isInstrumented}}`]: 'false', + }; + return variables[variable]; +}; + interface PromQuery extends DataQuery { expr: string; } diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index a9a4cae07e3..b06a9d7bcc3 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -7,6 +7,7 @@ import { CoreApp, DataFrame, DataFrameDTO, + DataLink, DataQueryRequest, DataQueryResponse, DataQueryResponseData, @@ -15,6 +16,7 @@ import { dateTime, FieldType, LoadingState, + NodeGraphDataFrameFieldNames, rangeUtil, ScopedVars, SelectableValue, @@ -1122,13 +1124,7 @@ export function getFieldConfig( datasourceUid, false ), - makeTempoLink( - 'View traces', - namespaceFields !== undefined ? `\${${namespaceFields.targetNamespace}}` : '', - `\${${targetField}}`, - '', - tempoDatasourceUid - ), + makeTempoLinkServiceMap('View traces', tempoDatasourceUid, !!namespaceFields?.targetNamespace), ], }; } @@ -1183,25 +1179,99 @@ export function makeTempoLink( }; } +function makeTempoLinkServiceMap( + title: string, + datasourceUid: string, + includeNamespace: boolean +): DataLink { + return { + url: '', + title, + internal: { + datasourceUid, + datasourceName: getDataSourceSrv().getInstanceSettings(datasourceUid)?.name ?? '', + query: ({ replaceVariables, scopedVars }) => { + const serviceName = replaceVariables?.(`\${__data.fields.${NodeGraphDataFrameFieldNames.title}}`, scopedVars); + const serviceNamespace = replaceVariables?.( + `\${__data.fields.${NodeGraphDataFrameFieldNames.subTitle}}`, + scopedVars + ); + const isInstrumented = + replaceVariables?.(`\${__data.fields.${NodeGraphDataFrameFieldNames.isInstrumented}}`, scopedVars) !== + 'false'; + const query: TempoQuery = { refId: 'A', queryType: 'traceqlSearch', filters: [] }; + + // Only do the peer query if service is actively set as not instrumented + if (isInstrumented === false) { + const filters = ['db.name', 'db.system', 'peer.service', 'messaging.system', 'net.peer.name'] + .map((peerAttribute) => `span.${peerAttribute}="${serviceName}"`) + .join(' || '); + query.queryType = 'traceql'; + query.query = `{${filters}}`; + } else { + if (includeNamespace && serviceNamespace) { + query.filters.push({ + id: 'service-namespace', + scope: TraceqlSearchScope.Resource, + tag: 'service.namespace', + value: serviceNamespace, + operator: '=', + valueType: 'string', + }); + } + if (serviceName) { + query.filters.push({ + id: 'service-name', + scope: TraceqlSearchScope.Resource, + tag: 'service.name', + value: serviceName, + operator: '=', + valueType: 'string', + }); + } + } + + return query; + }, + }, + }; +} + function makePromServiceMapRequest(options: DataQueryRequest): DataQueryRequest { return { ...options, - targets: serviceMapMetrics.map((metric) => { - const { serviceMapQuery, serviceMapIncludeNamespace: serviceMapIncludeNamespace } = options.targets[0]; - const extraSumByFields = serviceMapIncludeNamespace ? ', client_service_namespace, server_service_namespace' : ''; - const queries = Array.isArray(serviceMapQuery) ? serviceMapQuery : [serviceMapQuery]; - const subExprs = queries.map( - (query) => `sum by (client, server${extraSumByFields}) (rate(${metric}${query || ''}[$__range]))` - ); - return { - format: 'table', - refId: metric, - // options.targets[0] is not correct here, but not sure what should happen if you have multiple queries for - // service map at the same time anyway - expr: subExprs.join(' OR '), - instant: true, - }; - }), + targets: serviceMapMetrics + .map((metric) => { + const { serviceMapQuery, serviceMapIncludeNamespace: serviceMapIncludeNamespace } = options.targets[0]; + const extraSumByFields = serviceMapIncludeNamespace + ? ', client_service_namespace, server_service_namespace' + : ''; + const queries = Array.isArray(serviceMapQuery) ? serviceMapQuery : [serviceMapQuery]; + const sumSubExprs = queries.map( + (query) => `sum by (client, server${extraSumByFields}) (rate(${metric}${query || ''}[$__range]))` + ); + const groupSubExprs = queries.map( + (query) => `group by (client, connection_type, server${extraSumByFields}) (${metric}${query || ''})` + ); + + return [ + { + format: 'table', + refId: metric, + // options.targets[0] is not correct here, but not sure what should happen if you have multiple queries for + // service map at the same time anyway + expr: sumSubExprs.join(' OR '), + instant: true, + }, + { + format: 'table', + refId: `${metric}_labels`, + expr: groupSubExprs.join(' OR '), + instant: true, + }, + ]; + }) + .flat(), }; } diff --git a/public/app/plugins/datasource/tempo/graphTransform.test.ts b/public/app/plugins/datasource/tempo/graphTransform.test.ts index 2726a1b4141..bcd723c1310 100644 --- a/public/app/plugins/datasource/tempo/graphTransform.test.ts +++ b/public/app/plugins/datasource/tempo/graphTransform.test.ts @@ -20,6 +20,7 @@ it('assigns correct field type even if values are numbers', async () => { { name: 'secondarystat', values: [10, 20], type: FieldType.number }, { name: 'arc__success', values: [1, 1], type: FieldType.number }, { name: 'arc__failed', values: [0, 0], type: FieldType.number }, + { name: 'isinstrumented', values: [true, true], type: FieldType.boolean }, ]); }); @@ -41,6 +42,7 @@ it('do not fail on response with empty list', async () => { { name: 'secondarystat', values: [], type: FieldType.number }, { name: 'arc__success', values: [], type: FieldType.number }, { name: 'arc__failed', values: [], type: FieldType.number }, + { name: 'isinstrumented', values: [], type: FieldType.boolean }, ]); }); @@ -66,6 +68,7 @@ describe('mapPromMetricsToServiceMap', () => { { name: 'secondarystat', values: [10, 20, NaN] }, { name: 'arc__success', values: [0.8, 0.25, 1] }, { name: 'arc__failed', values: [0.2, 0.75, 0] }, + { name: 'isinstrumented', values: [true, true, true] }, ]); expect(edges.fields).toMatchObject([ { name: 'id', values: ['app_db', 'lb_app'] }, @@ -101,6 +104,7 @@ describe('mapPromMetricsToServiceMap', () => { { name: 'secondarystat', values: [10, 20, NaN] }, { name: 'arc__success', values: [0.8, 0.25, 1] }, { name: 'arc__failed', values: [0.2, 0.75, 0] }, + { name: 'isinstrumented', values: [true, true, true] }, ]); expect(edges.fields).toMatchObject([ { name: 'id', values: ['ns1/app_ns3/db', 'ns2/lb_ns1/app'] }, @@ -138,6 +142,41 @@ describe('mapPromMetricsToServiceMap', () => { { name: 'secondarystat', values: [10, 20, NaN] }, { name: 'arc__success', values: [0, 0, 1] }, { name: 'arc__failed', values: [1, 1, 0] }, + { name: 'isinstrumented', values: [true, true, true] }, + ]); + }); + + it('handles setting isInstrumented based on the connection_type', () => { + const range = { + from: dateTime('2000-01-01T00:00:00'), + to: dateTime('2000-01-01T00:01:00'), + }; + const { nodes } = mapPromMetricsToServiceMap( + [ + { + data: [ + totalsPromMetric(true), + secondsPromMetric(true), + secondsLabelsPromMetric(true), + failedPromMetric(true), + ], + }, + ], + { + ...range, + raw: range, + } + ); + + expect(nodes.fields).toMatchObject([ + { name: 'id', values: ['ns3/db', 'ns1/app', 'ns2/lb'] }, + { name: 'title', values: ['db', 'app', 'lb'] }, + { name: 'subtitle', values: ['ns3', 'ns1', 'ns2'] }, + { name: 'mainstat', values: [1000, 2000, NaN] }, + { name: 'secondarystat', values: [10, 20, NaN] }, + { name: 'arc__success', values: [0.8, 0.25, 1] }, + { name: 'arc__failed', values: [0.2, 0.75, 0] }, + { name: 'isinstrumented', values: [true, false, true] }, ]); }); }); @@ -182,6 +221,27 @@ const secondsPromMetric = (namespace?: boolean) => ], }); +const secondsLabelsPromMetric = (namespace?: boolean) => + createDataFrame({ + refId: 'traces_service_graph_request_server_seconds_sum_labels', + fields: [ + { name: 'Time', values: [1628169788000, 1628169788000] }, + { name: 'client', values: ['app', 'lb'] }, + { name: 'instance', values: ['127.0.0.1:12345', '127.0.0.1:12345'] }, + { name: 'job', values: ['local_scrape', 'local_scrape'] }, + { name: 'server', values: ['db', 'app'] }, + { name: 'tempo_config', values: ['default', 'default'] }, + { name: 'Value #traces_service_graph_request_server_seconds_sum_label', values: [1, 1] }, + { name: 'connection_type', values: ['messaging_system', 'virtual_node'] }, + ...(namespace + ? [ + { name: 'client_service_namespace', values: ['ns1', 'ns2'] }, + { name: 'server_service_namespace', values: ['ns3', 'ns1'] }, + ] + : []), + ], + }); + const failedPromMetric = (namespace?: boolean) => createDataFrame({ refId: 'traces_service_graph_request_failed_total', diff --git a/public/app/plugins/datasource/tempo/graphTransform.ts b/public/app/plugins/datasource/tempo/graphTransform.ts index 9efc7eea29c..caf5f49ead7 100644 --- a/public/app/plugins/datasource/tempo/graphTransform.ts +++ b/public/app/plugins/datasource/tempo/graphTransform.ts @@ -61,6 +61,10 @@ export function mapPromMetricsToServiceMap( collectMetricData(frames[secondsMetric], 'seconds', secondsMetric, nodesMap, edgesMap); collectMetricData(frames[failedMetric], 'failed', failedMetric, nodesMap, edgesMap); + collectIsInstrumented(frames[`${totalsMetric}_labels`], nodesMap); + collectIsInstrumented(frames[`${secondsMetric}_labels`], nodesMap); + collectIsInstrumented(frames[`${failedMetric}_labels`], nodesMap); + return convertToDataFrames(nodesMap, edgesMap, range); } @@ -97,6 +101,11 @@ function createServiceMapDataFrames() { config: { displayName: 'Failed', color: { fixedColor: 'red', mode: FieldColorModeId.Fixed } }, values: [], }, + { + name: Fields.isInstrumented, + type: FieldType.boolean, + values: [], + }, ]); const edges = createDF('Edges', [ { name: Fields.id, type: FieldType.string, values: [] }, @@ -145,6 +154,7 @@ type ServiceMapStatistics = { type NodeObject = ServiceMapStatistics & { name: string; namespace?: string; + isInstrumented?: boolean; }; type EdgeObject = ServiceMapStatistics & { @@ -240,6 +250,21 @@ function collectMetricData( } } +function collectIsInstrumented(frame: DataFrameView | undefined, nodesMap: Record) { + if (!frame) { + return; + } + + for (let i = 0; i < frame.length; i++) { + const row = frame.get(i); + const serverId = row.server_service_namespace ? `${row.server_service_namespace}/${row.server}` : row.server; + + if (nodesMap[serverId] && nodesMap[serverId].isInstrumented !== true) { + nodesMap[serverId].isInstrumented = row.connection_type === '' || row.connection_type === 'messaging_system'; + } + } +} + function convertToDataFrames( nodesMap: Record, edgesMap: Record, @@ -258,6 +283,7 @@ function convertToDataFrames( [Fields.secondaryStat]: node.total ? Math.round(node.total * 100) / 100 : Number.NaN, // Request per second (to 2 decimals) [Fields.arc + 'success']: node.total ? (node.total - Math.min(node.failed || 0, node.total)) / node.total : 1, [Fields.arc + 'failed']: node.total ? Math.min(node.failed || 0, node.total) / node.total : 0, + [Fields.isInstrumented]: node.isInstrumented ?? true, }); } for (const edgeId of Object.keys(edgesMap)) { diff --git a/public/app/plugins/panel/nodeGraph/types.ts b/public/app/plugins/panel/nodeGraph/types.ts index 377c5df5526..55081318b65 100644 --- a/public/app/plugins/panel/nodeGraph/types.ts +++ b/public/app/plugins/panel/nodeGraph/types.ts @@ -17,6 +17,7 @@ export type NodeDatum = SimulationNodeDatum & { icon?: IconName; nodeRadius?: Field; highlighted: boolean; + isInstrumented?: boolean; }; export type NodeDatumFromEdge = NodeDatum & { mainStatNumeric?: number; secondaryStatNumeric?: number }; diff --git a/public/app/plugins/panel/nodeGraph/utils.test.ts b/public/app/plugins/panel/nodeGraph/utils.test.ts index 3c563cbddd6..5ad0ce82d4e 100644 --- a/public/app/plugins/panel/nodeGraph/utils.test.ts +++ b/public/app/plugins/panel/nodeGraph/utils.test.ts @@ -19,7 +19,7 @@ describe('processNodes', () => { it('returns proper nodes and edges', async () => { const { nodes, edges, legend } = processNodes( - makeNodesDataFrame(3), + makeNodesDataFrame(3, [{ isinstrumented: false }]), makeEdgesDataFrame([ { source: '0', target: '1' }, { source: '0', target: '2' }, @@ -28,7 +28,7 @@ describe('processNodes', () => { ); expect(nodes).toEqual([ - makeNodeDatum(), + makeNodeDatum({ isInstrumented: false }), makeNodeDatum({ dataFrameRowIndex: 1, id: '1', incoming: 1, title: 'service:1' }), makeNodeDatum({ dataFrameRowIndex: 2, id: '2', incoming: 2, title: 'service:2' }), ]); @@ -366,6 +366,7 @@ function makeNodeDatum(options: Partial = {}) { type: 'number', values: [40, 40, 40], }, + isInstrumented: true, ...options, }; } diff --git a/public/app/plugins/panel/nodeGraph/utils.ts b/public/app/plugins/panel/nodeGraph/utils.ts index bfeb54bd7cc..4a26ae38685 100644 --- a/public/app/plugins/panel/nodeGraph/utils.ts +++ b/public/app/plugins/panel/nodeGraph/utils.ts @@ -58,6 +58,7 @@ export type NodeFields = { icon?: Field; nodeRadius?: Field; highlighted?: Field; + isInstrumented?: Field; }; export function getNodeFields(nodes: DataFrame): NodeFields { @@ -80,6 +81,7 @@ export function getNodeFields(nodes: DataFrame): NodeFields { highlighted: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.highlighted.toLowerCase()), fixedX: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.fixedX.toLowerCase()), fixedY: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.fixedY.toLowerCase()), + isInstrumented: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.isInstrumented.toLowerCase()), }; } @@ -364,6 +366,7 @@ function makeNodeDatum(id: string, nodeFields: NodeFields, index: number): NodeD highlighted: nodeFields.highlighted?.values[index] || false, x: nodeFields.fixedX?.values[index] ?? undefined, y: nodeFields.fixedY?.values[index] ?? undefined, + isInstrumented: nodeFields.isInstrumented?.values[index] ?? true, }; } @@ -384,16 +387,19 @@ export function statToString(config: FieldConfig, value: number | string): strin * Utilities mainly for testing */ -export function makeNodesDataFrame(count: number) { +export function makeNodesDataFrame( + count: number, + partialNodes: Array>> = [] +) { const frame = nodesFrame(); for (let i = 0; i < count; i++) { - frame.add(makeNode(i)); + frame.add(makeNode(i, partialNodes[i])); } return frame; } -function makeNode(index: number) { +function makeNode(index: number, partialNode: Partial> = {}) { return { id: index.toString(), title: `service:${index}`, @@ -405,6 +411,8 @@ function makeNode(index: number) { color: 0.5, icon: 'database', noderadius: 40, + isinstrumented: true, + ...partialNode, }; } @@ -453,6 +461,10 @@ function nodesFrame() { values: [], type: FieldType.number, }, + [NodeGraphDataFrameFieldNames.isInstrumented]: { + values: [], + type: FieldType.boolean, + }, }; return new MutableDataFrame({ From 99c8d4b0c6c60b37e4d0139240e5fa56e17f50e4 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:42:53 +0100 Subject: [PATCH 515/894] Update `Intro > Queries and Conditions` (#95109) * Update `Intro > Queries and Conditions` * Small tweaks (advanced options) and screenshots * Change `Expressions` heading * Set links from Alert rules introduction * Minor intro changes * small change due to recent updates * fix vale errors * fix vale error * Remove unnecessary mention to `alertingQueryAndExpressionsStepMode` feature flag --- .../create-grafana-managed-rule.md | 2 - .../fundamentals/alert-rules/_index.md | 25 ++- .../alert-rules/queries-conditions.md | 191 +++++++++--------- 3 files changed, 114 insertions(+), 104 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 2ad18a60ab6..60697fd1707 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -148,8 +148,6 @@ You can toggle between the two options. Once you have created an alert rule, the Switching from advanced to default may result in queries and expressions that cannot be converted. In this case, a warning message asks if you want to continue to reset to default settings. -Default and advanced options are enabled by default for Grafana Cloud users and this feature is being rolled out progressively. OSS users can enable them via the [`alertingQueryAndExpressionsStepMode` feature toggle](/setup-grafana/configure-grafana/feature-toggles/). - {{< docs/shared lookup="alerts/configure-alert-rule-name.md" source="grafana" version="" >}} ## Define query and condition diff --git a/docs/sources/alerting/fundamentals/alert-rules/_index.md b/docs/sources/alerting/fundamentals/alert-rules/_index.md index 47e410b8564..439c56b130c 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/_index.md +++ b/docs/sources/alerting/fundamentals/alert-rules/_index.md @@ -25,9 +25,17 @@ refs: destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/prometheus/configure-prometheus-data-source/#alerting queries-and-conditions: - pattern: /docs/grafana/ - destination: /docs/grafana//alerting/fundamentals/alert-rules/queries-conditions/ + destination: /docs/grafana//alerting/fundamentals/alert-rules/queries-conditions/#data-source-queries - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/queries-conditions/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/queries-conditions/#data-source-queries + alert-condition: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/fundamentals/alert-rules/queries-conditions/#alert-condition + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/queries-conditions/#alert-condition + recorded-queries: + - pattern: /docs/ + destination: /docs/grafana//administration/recorded-queries/ notification-images: - pattern: /docs/grafana/ destination: /docs/grafana//alerting/configure-notifications/template-notifications/images-in-notifications/ @@ -45,14 +53,9 @@ refs: destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-recording-rules/ expression-queries: - pattern: /docs/grafana/ - destination: /docs/grafana//alerting/fundamentals/alert-rules/queries-conditions/#expression-queries + destination: /docs/grafana//alerting/fundamentals/alert-rules/queries-conditions/#advanced-options-expressions - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/queries-conditions/#expression-queries - alert-condition: - - pattern: /docs/grafana/ - destination: /docs/grafana//alerting/fundamentals/alert-rules/queries-conditions/#alert-condition - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/queries-conditions/#alert-condition + destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/queries-conditions/#advanced-options-expressions alert-rule-evaluation: - pattern: /docs/grafana/ destination: /docs/grafana//alerting/fundamentals/alert-rules/rule-evaluation/ @@ -64,8 +67,8 @@ refs: An alert rule is a set of evaluation criteria for when an alert rule should fire. An alert rule consists of: -- Queries and expressions that select the data set to evaluate. -- A condition (the threshold) that the query must meet or exceed to trigger the alert instance. +- [Queries](ref:queries-and-conditions) that select the dataset to evaluate. +- An [alert condition](ref:alert-condition) (the threshold) that the query must meet or exceed to trigger the alert instance. - An interval that specifies the frequency of [alert rule evaluation](ref:alert-rule-evaluation) and a duration indicating how long the condition must be met to trigger the alert instance. - Other customizable options, for example, setting what should happen in the absence of data, notification messages, and more. diff --git a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md index 877d1b5e76a..582d93d958e 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md +++ b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md @@ -17,21 +17,16 @@ labels: title: Queries and conditions weight: 104 refs: - data-sources: - - pattern: /docs/grafana/ - destination: /docs/grafana//datasources/ - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/ data-source-alerting: - pattern: /docs/grafana/ destination: /docs/grafana//alerting/fundamentals/alert-rules/#supported-data-sources - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/#supported-data-sources - alert-rule-evaluation: + state-and-health: - pattern: /docs/grafana/ - destination: /docs/grafana//alerting/fundamentals/alert-rule-evaluation/ + destination: /docs/grafana//alerting/fundamentals/state-and-health/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rule-evaluation/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/state-and-health/ query-transform-data: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/query-transform-data/ @@ -41,71 +36,116 @@ refs: # Queries and conditions -In Grafana, queries fetch and transform data from [data sources,](ref:data-sources) which include databases like MySQL or PostgreSQL, time series databases like Prometheus or InfluxDB, and services like Amazon CloudWatch or Azure Monitor. +In Grafana, queries fetch and transform data from data sources, which include databases like MySQL or PostgreSQL, time series databases like Prometheus or InfluxDB, and services like Amazon CloudWatch or Azure Monitor. -A query specifies the data to extract from a data source, with the syntax varying based on the type of data source used. +An alert rule defines the following components: -In Alerting, an alert rule defines of one or more queries and expressions that select the data you want to measure and a [condition](#alert-condition) that needs to be met before an alert rule fires. +- A [query](#data-source-queries) that specifies the data to retrieve from a data source, with the syntax depending on the type of data source used. +- A [condition](#alert-condition) that must be met before the alert rule fires. +- Optional [expressions](#advanced-options-expressions) to perform transformations on the retrieved data. + +Alerting periodically runs the queries and expressions, evaluating the condition. If the condition is breached, an alert instance is triggered for each time series. ## Data source queries -Alerting queries are the same type of queries available in Grafana panels. Queries in Grafana can be applied in various ways, depending on the data source and query language being used. However, not all [data sources support Alerting](ref:data-source-alerting). +Alerting queries are the same as the queries used in Grafana panels, but Grafana-managed alerts are limited to querying [data sources that have Alerting enabled](ref:data-source-alerting). -Each data source’s query editor provides a customized user interface to help you write queries that take advantage of its unique capabilities. For additional information about queries in Grafana, refer to [Query and transform data](ref:query-transform-data). +Queries in Grafana can be applied in various ways, depending on the data source and query language being used. Each data source’s query editor provides a customized user interface to help you write queries that take advantage of its unique capabilities. -Some common types of query components include: +For more details about queries in Grafana, refer to [Query and transform data](ref:query-transform-data). -**Metrics or data fields**: Specify the specific metrics or data fields you want to retrieve, such as CPU usage, network traffic, or sensor readings. +{{< figure src="/media/docs/alerting/alerting-query-conditions-default-options.png" max-width="750px" caption="Define alert query and alert condition" >}} -**Time range**: Define the time range for which you want to fetch data, such as the last hour, a specific day, or a custom time range. +## Alert condition -**Filters**: Apply filters to narrow down the data based on specific criteria, such as filtering data by a specific tag, host, or application. +The alert condition is the query or expression that determines whether the alert fires or not depending whether the value satisfies the specified comparison. There can be only one condition which determines the triggering of the alert. -**Aggregations**: Perform aggregations on the data to calculate metrics like averages, sums, or counts over a given time period. +If the queried data meets the defined condition, Grafana fires the alert. -**Grouping**: Group the data by specific dimensions or tags to create aggregated views or breakdowns. +When using **Default options**, the `When` input [reduces the query data](#reduce), and the last input defines the threshold condition. -{{% admonition type="note" %}} -Grafana doesn't support alert queries with template variables. More details [here](https://community.grafana.com/t/template-variables-are-not-supported-in-alert-queries-while-setting-up-alert/2514). -{{% /admonition %}} +When using **Advanced options**, you have to choose one of your queries or expressions as the alert condition. -## Expression queries +## Advanced options: Expressions -In Grafana, an expression is used to perform calculations, transformations, or aggregations on the data source queried data. It allows you to create custom metrics or modify existing metrics based on mathematical operations, functions, or logical expressions. +Expressions are only available for Grafana-managed alerts and when the **Advanced options** are enabled. -By leveraging expression queries, users can perform tasks such as calculating the percentage change between two values, applying functions like logarithmic or trigonometric functions, aggregating data over specific time ranges or dimensions, and implementing conditional logic to handle different scenarios. +In Grafana, expressions allow you to perform calculations, transformations, or aggregations on queried data. They modify existing metrics through mathematical operations, functions, or logical expressions. -In Alerting, you can only use expressions for Grafana-managed alert rules. For each expression, you can choose from the math, reduce, and resample expressions. These are called multi-dimensional rules, because they generate an alert instance for each series. +With expression queries, you can perform tasks such as calculating the percentage change between two values, applying functions like logarithmic or trigonometric functions, aggregating data over specific time ranges or dimensions, and implementing conditional logic to handle different scenarios. -**Reduce** +{{< figure src="/media/docs/alerting/alert-rule-expressions.png" max-width="750px" caption="Alert rule expressions" >}} -Aggregates time series values in the selected time range into a single value. It's not necessary for [rules using numeric data](#alert-on-numeric-data). +The following expressions are available: -**Math** +### Reduce -Performs free-form math functions/operations on time series and number data. Can be used to preprocess time series data or to define an alert condition for number data. For example: +Aggregates time series values within the selected time range into a single number. + +Reduce takes one or more time series and transform each series into a single number, which can then be compared in the alert condition. + +The following aggregations functions are included: `Min`, `Max`, `Mean`, `Mediam`, `Sum`, `Count`, and `Last`. + +### Math + +Performs free-form math functions/operations on time series data and numbers. For instance, `$A + 1` or `$A * 100`. + +You can also use a Math expression to define the alert condition for numbers. For example: - `$B > 70` should fire if the value of B (query or expression) is more than 70. - `$B < $C * 100` should fire if the value of B is less than the value of C multiplied by 100. If queries being compared have multiple series in their results, series from different queries are matched if they have the same labels or one is a subset of the other. -**Resample** +### Resample Realigns a time range to a new set of timestamps, this is useful when comparing time series data from different data sources where the timestamps would otherwise not align. -**Threshold** +### Threshold -Checks if any time series data matches the threshold condition. +Compares single numbers from previous queries or expressions (e.g., `$A`, `$B`) to a specified condition. It's often used to define the alert condition. -The threshold expression allows you to compare two single values. It returns `0` when the condition is false and `1` if the condition is true. The following threshold functions are available: +The threshold expression allows the comparison between two single values. Available threshold functions are: -- Is above (x > y) -- Is below (x < y) -- Is within range (x > y1 AND x < y2) -- Is outside range (x < y1 OR x > y2) +- **Is above**: `$A > 5` +- **Is below**: `$B < 3` +- **Is within range**: `$A > 0 AND $A < 10` +- **Is outside range**: `$B < 0 OR $B > 100` -**Classic condition (legacy)** +A threshold returns `0` when the condition is false and `1` when true. + +If the threshold is set as the alert condition, the alert fires when the threshold returns `1`. + +#### Recovery threshold + +To reduce the noise from flapping alerts, you can set a recovery threshold different to the alert threshold. + +Flapping alerts occur when a metric hovers around the alert threshold condition and may lead to frequent state changes, resulting in too many notifications. + +The value of a flapping metric can continually go above and below a threshold, resulting in a series of firing-resolved-firing notifications and a noisy alert state history. + +For example, if you have an alert for latency with a threshold of 1000ms and the number fluctuates around 1000 (say 980 -> 1010 -> 990 -> 1020, and so on), then each of those might trigger a notification: + +- 980 -> 1010 triggers a firing alert. +- 1010 -> 990 triggers a resolving alert. +- 990 -> 1020 triggers a firing alert again. + +To prevent this, you can set a recovery threshold to define two thresholds instead of one: + +1. An alert is triggered when the first threshold is crossed. +1. An alert is resolved only when the second (recovery) threshold is crossed. + +In the previous example, setting the recovery threshold to 900ms means the alert only resolves when the latency falls below 900ms: + +- 980 -> 1010 triggers a firing alert. +- 1010 -> 990 does not resolve the alert, keeping it in the firing state. +- 990 -> 1020 keeps the alert in the firing state. + +The recovery threshold mitigates unnecessary alert state changes and reduces alert noise. + +{{< collapse title="Classic condition (legacy)" >}} + +#### Classic condition (legacy) Classic conditions exist mainly for compatibility reasons and should be avoided if possible. @@ -113,66 +153,35 @@ Classic condition checks if any time series data matches the alert condition. It | Condition operators | How it works | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| and | Two conditions before and after must be true for the overall condition to be true. | -| or | If one of conditions before and after are true, the overall condition is true. | -| logic-or | If the condition before `logic-or` is true, the overall condition is immediately true, without evaluating subsequent conditions. For instance, `TRUE and TRUE logic-or FALSE and FALSE` evaluate to `TRUE`, because the preceding condition returns `TRUE`. | +| `and` | Two conditions before and after must be true for the overall condition to be true. | +| `or` | If one of conditions before and after are true, the overall condition is true. | +| `logic-or` | If the condition before `logic-or` is true, the overall condition is immediately true, without evaluating subsequent conditions. For instance, `TRUE and TRUE logic-or FALSE and FALSE` evaluate to `TRUE`, because the preceding condition returns `TRUE`. | -## Aggregations +The following aggregation functions are also available to further refine your query. -Grafana Alerting provides the following aggregation functions to enable you to further refine your query. +| Function | What it does | +| ------------------ | ------------------------------------------------------------------------------- | +| `avg` | Displays the average of the values | +| `min` | Displays the lowest value | +| `max` | Displays the highest value | +| `sum` | Displays the sum of all values | +| `count` | Counts the number of values in the result | +| `last` | Displays the last value | +| `median` | Displays the median value | +| `diff` | Displays the difference between the newest and oldest value | +| `diff_abs` | Displays the absolute value of diff | +| `percent_diff` | Displays the percentage value of the difference between newest and oldest value | +| `percent_diff_abs` | Displays the absolute value of `percent_diff` | +| `count_non_null` | Displays a count of values in the result set that aren't `null` | -These functions are available for **Reduce** and **Classic condition** expressions only. - -| Function | Expression | What it does | -| ---------------- | ---------------- | ------------------------------------------------------------------------------- | -| avg | Reduce / Classic | Displays the average of the values | -| min | Reduce / Classic | Displays the lowest value | -| max | Reduce / Classic | Displays the highest value | -| sum | Reduce / Classic | Displays the sum of all values | -| count | Reduce / Classic | Counts the number of values in the result | -| last | Reduce / Classic | Displays the last value | -| median | Reduce / Classic | Displays the median value | -| diff | Classic | Displays the difference between the newest and oldest value | -| diff_abs | Classic | Displays the absolute value of diff | -| percent_diff | Classic | Displays the percentage value of the difference between newest and oldest value | -| percent_diff_abs | Classic | Displays the absolute value of percent_diff | -| count_non_null | Classic | Displays a count of values in the result set that aren't `null` | - -## Alert condition - -An alert condition is the query or expression that determines whether the alert fires or not depending on the value it yields. There can be only one condition which determines the triggering of the alert. - -After you have defined your queries and expressions, choose one of them as the alert rule condition. By default, the last expression added is used as the alert condition. - -When the queried data satisfies the defined condition, Grafana triggers the associated alert, which can be configured to send notifications through various channels like email, Slack, or PagerDuty. - -For details about how the alert evaluation triggers notifications, refer to [Alert rule evaluation](ref:alert-rule-evaluation). - -## Recovery threshold - -To reduce the noise of flapping alerts, you can set a recovery threshold different to the alert threshold. - -Flapping alerts occur when a metric hovers around the alert threshold condition and may lead to frequent state changes, resulting in too many notifications being generated. - -It can be tricky to create an alert rule for a noisy metric. That is, when the value of a metric continually goes above and below a threshold. This is called flapping and results in a series of firing - resolved - firing notifications and a noisy alert state history. - -For example, if you have an alert for latency with a threshold of 1000ms and the number fluctuates around 1000 (say 980 ->1010 -> 990 -> 1020, and so on) then each of those triggers a notification. - -To solve this problem, you can set a (custom) recovery threshold, which basically means having two thresholds instead of one: - -1. An alert is triggered when the first threshold is crossed. -2. An alert is resolved only when the second threshold is crossed. - -For example, you could set a threshold of 1000ms and a recovery threshold of 900ms. This way, an alert rule only stops firing when it goes under 900ms and flapping is reduced. - -For details about how the alert evaluation triggers notifications, refer to [Alert rule evaluation](ref:alert-rule-evaluation). +{{< /collapse >}} ## Alert on numeric data Among certain data sources numeric data that is not time series can be directly alerted on, or passed into Server Side Expressions (SSE). This allows for more processing and resulting efficiency within the data source, and it can also simplify alert rules. -When alerting on numeric data instead of time series data, there is no need to reduce each labeled time series into a single number. Instead labeled numbers are returned to Grafana instead. +When alerting on numeric data instead of time series data, there is no need to [reduce](#reduce) each labeled time series into a single number. Instead labeled numbers are returned to Grafana instead. -### Tabular Data +#### Tabular Data This feature is supported with backend data sources that query tabular data: From 2b4e1f3c51faf833198be5a8ff3341dca7c51a8b Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:12:40 +0100 Subject: [PATCH 516/894] Alerting docs: clarify that `silences` and `mute timings` do not interrupt alert evaluation (#100414) * Alerting docs: clarify that `silences` and `mute timings` do not interrupt alert evaluation * replace `ad-hoc` word --- .../alerting/configure-notifications/create-silence.md | 2 +- docs/sources/alerting/configure-notifications/mute-timings.md | 4 +++- docs/sources/alerting/fundamentals/_index.md | 2 +- docs/sources/alerting/fundamentals/notifications/_index.md | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/create-silence.md b/docs/sources/alerting/configure-notifications/create-silence.md index 6f781a60ce3..5cf6f4a9c04 100644 --- a/docs/sources/alerting/configure-notifications/create-silence.md +++ b/docs/sources/alerting/configure-notifications/create-silence.md @@ -61,7 +61,7 @@ refs: # Configure silences -Silences stop notifications from getting created and last for only a specified window of time. Use them to temporarily prevent alert notifications, such as during incident response or a maintenance window. +Silences stop notifications from being created for a specified time window but do not interrupt alert evaluation. Use them to temporarily prevent alert notifications, such as during incident response or a maintenance window. {{< admonition type="note" >}} Silences are assigned to a [specific Alertmanager](ref:alertmanager-architecture) and only suppress notifications for alerts managed by that Alertmanager. diff --git a/docs/sources/alerting/configure-notifications/mute-timings.md b/docs/sources/alerting/configure-notifications/mute-timings.md index 1ef726267e3..79be30654e0 100644 --- a/docs/sources/alerting/configure-notifications/mute-timings.md +++ b/docs/sources/alerting/configure-notifications/mute-timings.md @@ -39,7 +39,9 @@ refs: # Configure mute timings -A mute timing is a recurring interval of time when no new notifications for a policy are generated or sent. Use them to prevent alerts from firing a specific and reoccurring period, for example, a regular maintenance period or weekends. +A mute timing is a recurring interval that stops notifications for one or multiple notification policies during a specified period. It suppresses notifications but does not interrupt alert evaluation. + +Use mute timings to temporarily pause notifications for a specific recurring period, such as a regular maintenance window or weekends. {{< admonition type="note" >}} Mute timings are assigned to a [specific Alertmanager](ref:alertmanager-architecture) and only suppress notifications for alerts managed by that Alertmanager. diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index 934f019cfd3..147a5e36d2d 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -132,7 +132,7 @@ Each notification policy decides where to send the alert (contact point) and whe ### Silences and mute timings -[Silences](ref:silences) and [mute timings](ref:mute-timings) allow you to pause notifications for specific alerts or even entire notification policies. Use a silence to pause notifications on an ad-hoc basis, such as during a maintenance window; and use mute timings to pause notifications at regular intervals, such as evenings and weekends. +[Silences](ref:silences) and [mute timings](ref:mute-timings) allow you to pause notifications without interrupting alert rule evaluation. Use a silence to pause notifications on a one-time basis, such as during a maintenance window; and use mute timings to pause notifications at regular intervals, such as evenings and weekends. ### Architecture diff --git a/docs/sources/alerting/fundamentals/notifications/_index.md b/docs/sources/alerting/fundamentals/notifications/_index.md index 538eaa0d00a..4c85da09ef6 100644 --- a/docs/sources/alerting/fundamentals/notifications/_index.md +++ b/docs/sources/alerting/fundamentals/notifications/_index.md @@ -126,7 +126,7 @@ Grafana Alerting provides advanced notification capabilities that you’ll find For instance, you can customize notifications with shared [templates](ref:templates) that provide actionable alert information and can be reused for multiple notifications. -Additionally, you can use [silences](ref:silences) and [mute timings](ref:mute-timings) to pause notifications for a given time window or at regular intervals, respectively. +Additionally, you can use [silences](ref:silences) and [mute timings](ref:mute-timings) to pause or suppress notifications without interrupting alert evaluation. ## Architecture From 8e436fc473ce5196fd112e27a786f10a5c0aefce Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:18:31 +0100 Subject: [PATCH 517/894] Alerting docs: Add `Enable notifications` section to `Configure contact points` docs (#100446) Alerting docs: Add `Enable notifications` section to `Configure contact points` page --- .../manage-contact-points/_index.md | 110 ++++++++++-------- .../alerting-get-started-pt2/index.md | 4 +- 2 files changed, 63 insertions(+), 51 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md index ce35c4c75e8..160d76360dc 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md @@ -86,12 +86,7 @@ refs: destination: /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt/ - alertmanager-architecture: - - pattern: /docs/grafana/ - destination: /docs/grafana//alerting/configure-notifications/#alertmanager-architecture - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/#alertmanager-architecture - external-alertmanager: + configure-alertmanager: - pattern: /docs/grafana/ destination: /docs/grafana//alerting/set-up/configure-alertmanager/ - pattern: /docs/grafana-cloud/ @@ -101,13 +96,23 @@ refs: destination: /docs/grafana//alerting/configure-notifications/template-notifications/manage-notification-templates/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/manage-notification-templates/ + configure-grafana-alerts: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule/ + configure-contact-points: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/manage-contact-points/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/ --- # Configure contact points Use contact points to specify where to receive alert notifications. Contact points contain the configuration for sending alert notifications, including destinations like email, Slack, OnCall, webhooks, and their notification messages. -A contact point can have one or multiple destinations, known as [contact point integrations](#list-of-supported-integrations). Alert notifications are sent to each integration within the chosen contact point. +A contact point can have one or multiple destinations, known as [contact point integrations](#supported-contact-point-integrations). Alert notifications are sent to each integration within the chosen contact point. On the **Contact Points** tab, you can: @@ -119,9 +124,42 @@ On the **Contact Points** tab, you can: - Delete contact points. Note that you cannot delete contact points that are in use by a notification policy. To proceed, either delete the notification policy or update it to use another contact point. {{% admonition type="note" %}} -Contact points are assigned to a [specific Alertmanager](ref:alertmanager-architecture) and cannot be used by notification policies in other Alertmanagers. +Contact points are assigned to a [specific Alertmanager](ref:configure-alertmanager) and cannot be used by notification policies in other Alertmanagers. {{% /admonition %}} +## Supported contact point integrations + +Each contact point integration has its own configuration options and setup process. The following list shows the contact point integrations supported by Grafana. + +{{< column-list >}} + +- Alertmanager +- [AWS SNS](ref:sns) +- Cisco Webex Teams +- DingDing +- [Discord](ref:discord) +- [Email](ref:email) +- [Google Chat](ref:gchat) +- [Grafana Oncall](ref:oncall) +- Kafka REST Proxy +- Line +- [Microsoft Teams](ref:teams) +- [MQTT](ref:mqtt) +- [Opsgenie](ref:opsgenie) +- [Pagerduty](ref:pagerduty) +- Pushover +- Sensu Go +- [Slack](ref:slack) +- [Telegram](ref:telegram) +- Threema Gateway +- VictorOps +- [Webhook](ref:webhook) +- WeCom + +{{< /column-list >}} + +Some of the integrations above are not supported by Prometheus Alertmanager. For the list of supported integrations, refer to the [Prometheus Alertmanager receiver settings](https://prometheus.io/docs/alerting/latest/configuration/#receiver-integration-settings). + ## Add a contact point Complete the following steps to add a contact point. @@ -132,7 +170,7 @@ Complete the following steps to add a contact point. 1. On the **Contact Points** tab, click **+ Add contact point**. 1. Enter a descriptive name for the contact point. 1. From **Integration**, select a type and fill out mandatory fields. For example, if you choose email, enter the email addresses. Or if you choose Slack, enter the Slack channel and users who should be contacted. -1. Some contact point integrations, like email or Webhook, have optional settings. In **Optional settings**, specify additional settings for the selected contact point integration. +1. Some [contact point integrations](#supported-contact-point-integrations), like email or Webhook, have optional settings. In **Optional settings**, specify additional settings for the selected contact point integration. 1. In Notification settings, optionally select **Disable resolved message** if you do not want to be notified when an alert resolves. 1. Save your changes. @@ -148,6 +186,16 @@ To add another integration to a contact point, complete the following steps. - In **Optional settings**, specify additional settings for the selected contact point integration. 1. Save your changes. +## Customize notification messages + +In contact points, you can also customize notification messages. For example, when setting up an email contact point integration, click **Message** or **Subject** to modify it. + +By default, notification messages include common alert details, which are usually sufficient for most cases. + +If necessary, you can customize the content and format of notification messages. You can create a custom notification template, which can then be applied to one or more contact points. + +On the **Notification templates** tab, you can view, edit, copy or delete notification templates. Refer to [manage notification templates](ref:manage-notification-templates) for instructions on selecting or creating a template for a contact point. + ## Test a contact point Testing a contact point is only available for Grafana Alertmanager. Complete the following steps to test a contact point. @@ -159,45 +207,9 @@ Testing a contact point is only available for Grafana Alertmanager. Complete the 1. Choose whether to send a predefined test notification or choose custom to add your own custom annotations and labels to include in the notification. 1. Click **Send test notification** to fire the alert. -## Customize notification messages +## Enable notifications for a contact point -In contact points, you can also customize notification messages. For example, when setting up an email contact point integration, click **Message** or **Subject** to modify it. +After creating a contact point, you can enable it to receive alert notifications using one of the following methods: -By default, notification messages include common alert details, which are usually sufficient for most cases. - -If necessary, you can customize the content and format of notification messages. You can create a custom notification template, which can then be applied to one or more contact points. - -On the **Notification templates** tab, you can view, edit, copy or delete notification templates. Refer to [manage notification templates](ref:manage-notification-templates) for instructions on selecting or creating a template for a contact point. - -## List of supported integrations - -Each contact point integration has its own configuration options and setup process. In most cases, this involves providing an API key or a Webhook URL. - -The following table lists the contact point integrations supported by Grafana. - -| Name | Type | -| ---------------------------- | ------------------------- | -| Alertmanager | `prometheus-alertmanager` | -| [Amazon SNS](ref:sns) | `sns` | -| Cisco Webex Teams | `webex` | -| DingDing | `dingding` | -| [Discord](ref:discord) | `discord` | -| [Email](ref:email) | `email` | -| [Google Chat](ref:gchat) | `googlechat` | -| [Grafana Oncall](ref:oncall) | `oncall` | -| Kafka REST Proxy | `kafka` | -| Line | `line` | -| [Microsoft Teams](ref:teams) | `teams` | -| [MQTT](ref:mqtt) | `mqtt` | -| [Opsgenie](ref:opsgenie) | `opsgenie` | -| [Pagerduty](ref:pagerduty) | `pagerduty` | -| Pushover | `pushover` | -| Sensu Go | `sensugo` | -| [Slack](ref:slack) | `slack` | -| [Telegram](ref:telegram) | `telegram` | -| Threema Gateway | `threema` | -| VictorOps | `victorops` | -| [Webhook](ref:webhook) | `webhook` | -| WeCom | `wecom` | - -Some of these integrations are not compatible with [external Alertmanagers](ref:external-alertmanager). For the list of Prometheus Alertmanager integrations, refer to the [Prometheus Alertmanager receiver settings](https://prometheus.io/docs/alerting/latest/configuration/#receiver-integration-settings). +- **Assign it to alert rules** – Select the contact point in the [notifications options for Grafana-managed alert rules](ref:configure-grafana-alerts) to directly associate it with specific alerts. +- **Assign it to notification policies** – Add the contact point to one or more [notification policies](ref:configure-contact-points), which manage the alert notifications you want the contact point to receive. diff --git a/docs/sources/tutorials/alerting-get-started-pt2/index.md b/docs/sources/tutorials/alerting-get-started-pt2/index.md index e913016064e..3beabdf0369 100644 --- a/docs/sources/tutorials/alerting-get-started-pt2/index.md +++ b/docs/sources/tutorials/alerting-get-started-pt2/index.md @@ -188,7 +188,7 @@ Create a notification policy if you want to handle metrics returned by alert rul This new child policy routes alerts that match the label `device=desktop` to the Webhook contact point. -1. **Repeat the steps above to create a second child policy** to match another alert instance. For labels use: `device=mobile`. Use the Webhook integration for the contact point. Alternatively, experiment by using a different Webhook endpoint or a [different integration](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/#list-of-supported-integrations). +1. **Repeat the steps above to create a second child policy** to match another alert instance. For labels use: `device=mobile`. Use the Webhook integration for the contact point. Alternatively, experiment by using a different Webhook endpoint or a [different integration](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/#supported-contact-point-integrations). @@ -206,7 +206,7 @@ Create a notification policy if you want to handle metrics returned by alert rul This new child policy routes alerts that match the label `device=desktop` to the Webhook contact point. -1. **Repeat the steps above to create a second child policy** to match another alert instance. For labels use: `device=mobile`. Use the Webhook integration for the contact point. Alternatively, experiment by using a different Webhook endpoint or a [different integration](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/#list-of-supported-integrations). +1. **Repeat the steps above to create a second child policy** to match another alert instance. For labels use: `device=mobile`. Use the Webhook integration for the contact point. Alternatively, experiment by using a different Webhook endpoint or a [different integration](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/#supported-contact-point-integrations). {{< /docs/ignore >}} From b44b82606a7367b6655684aa42895bd75dcf0659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Wed, 12 Feb 2025 11:19:28 +0100 Subject: [PATCH 518/894] MultiCombobox: Add grouping (#100297) --- .../Combobox/MultiCombobox.internal.story.tsx | 36 +++++++- .../src/components/Combobox/MultiCombobox.tsx | 88 ++++++++++++++----- .../components/Combobox/OptionListItem.tsx | 9 +- .../components/Combobox/getComboboxStyles.ts | 18 +++- .../src/components/Combobox/storyUtils.ts | 8 ++ .../src/components/Combobox/types.ts | 1 + .../src/components/Combobox/useOptions.ts | 39 ++++++-- public/locales/en-US/grafana.json | 3 + public/locales/pseudo-LOCALE/grafana.json | 3 + 9 files changed, 168 insertions(+), 37 deletions(-) diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx index 7358119d986..2022354a901 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx @@ -6,7 +6,7 @@ import { ComponentProps } from 'react'; import { Field } from '../Forms/Field'; import { MultiCombobox } from './MultiCombobox'; -import { generateOptions, fakeSearchAPI } from './storyUtils'; +import { generateOptions, fakeSearchAPI, generateGroupingOptions } from './storyUtils'; import { ComboboxOption } from './types'; const meta: Meta = { @@ -107,6 +107,40 @@ export const ManyOptions: StoryObj = { render: ManyOptionsStory, }; +const ManyOptionsGroupedStory: StoryFn = ({ numberOfOptions = 1e5, ...args }) => { + const [dynamicArgs, setArgs] = useArgs(); + + const [options, setOptions] = useState([]); + + useEffect(() => { + setTimeout(async () => { + const options = await generateGroupingOptions(numberOfOptions); + setOptions(options); + }, 1000); + }, [numberOfOptions]); + const { onChange, ...rest } = args; + return ( + { + setArgs({ value: opts }); + onChangeAction(opts); + }} + /> + ); +}; + +export const ManyOptionsGrouped: StoryObj = { + args: { + numberOfOptions: 1e4, + options: undefined, + value: undefined, + }, + render: ManyOptionsGroupedStory, +}; + function loadOptionsWithLabels(inputValue: string) { loadOptionsAction(inputValue); return fakeSearchAPI(`http://example.com/search?errorOnQuery=break&query=${inputValue}`); diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 0fbfe84d36a..7b41ca2ee53 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -246,8 +246,18 @@ export const MultiCombobox = (props: MultiComboboxPro const virtualizerOptions = { count: options.length, getScrollElement: () => scrollRef.current, - estimateSize: (index: number) => - 'description' in options[index] ? MENU_OPTION_HEIGHT_DESCRIPTION : MENU_OPTION_HEIGHT, + estimateSize: (index: number) => { + const firstGroupItem = isNewGroup(options[index], index > 0 ? options[index - 1] : undefined); + const hasDescription = 'description' in options[index]; + let itemHeight = MENU_OPTION_HEIGHT; + if (hasDescription) { + itemHeight = MENU_OPTION_HEIGHT_DESCRIPTION; + } + if (firstGroupItem) { + itemHeight += MENU_OPTION_HEIGHT; + } + return itemHeight; + }, overscan: VIRTUAL_OVERSCAN_ITEMS, }; @@ -337,6 +347,7 @@ export const MultiCombobox = (props: MultiComboboxPro
      {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const startingNewGroup = isNewGroup(options[virtualRow.index], options[virtualRow.index - 1]); const index = virtualRow.index; const item = options[index]; const itemProps = getItemProps({ item, index }); @@ -354,29 +365,46 @@ export const MultiCombobox = (props: MultiComboboxPro key={`${item.value}-${index}`} data-index={index} {...itemProps} - className={cx(styles.option, { [styles.optionFocused]: highlightedIndex === index })} + className={styles.optionBasic} style={{ height: virtualRow.size, transform: `translateY(${virtualRow.start}px)` }} > - - 0 && !allItemsSelected} - aria-labelledby={id} - onClick={(e) => { - e.stopPropagation(); - }} - /> - + + {startingNewGroup && ( +
      + +
      + )} +
      + + 0 && !allItemsSelected} + aria-labelledby={id} + onClick={(e) => { + e.stopPropagation(); + }} + /> + + +
      ); @@ -425,3 +453,17 @@ function isComboboxOptions( ): value is Array> { return typeof value[0] === 'object'; } + +const isNewGroup = (option: ComboboxOption, prevOption?: ComboboxOption) => { + const currentGroup = option.group; + + if (!currentGroup) { + return prevOption?.group ? true : false; + } + + if (!prevOption) { + return true; + } + + return prevOption.group !== currentGroup; +}; diff --git a/packages/grafana-ui/src/components/Combobox/OptionListItem.tsx b/packages/grafana-ui/src/components/Combobox/OptionListItem.tsx index 436a662e204..3a929be51a9 100644 --- a/packages/grafana-ui/src/components/Combobox/OptionListItem.tsx +++ b/packages/grafana-ui/src/components/Combobox/OptionListItem.tsx @@ -1,3 +1,5 @@ +import { cx } from '@emotion/css'; + import { useStyles2 } from '../../themes'; import { getComboboxStyles } from './getComboboxStyles'; @@ -6,13 +8,14 @@ interface Props { label: string; description?: string; id: string; + isGroup?: boolean; } -export const OptionListItem = ({ label, description, id }: Props) => { +export const OptionListItem = ({ label, description, id, isGroup = false }: Props) => { const styles = useStyles2(getComboboxStyles); return ( -
      - +
      + {label} {description && {description}} diff --git a/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts index 5da03591348..f0ea95cacb2 100644 --- a/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts +++ b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts @@ -32,9 +32,8 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => { label: 'combobox-menu-ul-container', listStyle: 'none', }), - option: css({ + optionBasic: css({ label: 'combobox-option', - padding: MENU_ITEM_PADDING, position: 'absolute', display: 'flex', alignItems: 'center', @@ -43,7 +42,11 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => { whiteSpace: 'nowrap', width: '100%', overflow: 'hidden', + }), + option: css({ + padding: MENU_ITEM_PADDING, cursor: 'pointer', + width: '100%', '&:hover': { background: theme.colors.action.hover, '@media (forced-colors: active), (prefers-contrast: more)': { @@ -51,6 +54,11 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => { }, }, }), + optionGroup: css({ + cursor: 'default', + padding: MENU_ITEM_PADDING, + borderTop: `1px solid ${theme.colors.border.weak}`, + }), optionBody: css({ label: 'combobox-option-body', display: 'flex', @@ -67,6 +75,12 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => { fontWeight: MENU_ITEM_FONT_WEIGHT, letterSpacing: 0, // pr todo: text in grafana has a slightly different letter spacing, which causes measureText() to be ~5% off }), + optionLabelGroup: css({ + label: 'combobox-option-label-group', + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + fontWeight: theme.typography.fontWeightLight, + }), optionDescription: css({ label: 'combobox-option-description', fontWeight: theme.typography.fontWeightRegular, diff --git a/packages/grafana-ui/src/components/Combobox/storyUtils.ts b/packages/grafana-ui/src/components/Combobox/storyUtils.ts index 48563324f54..d39040a6121 100644 --- a/packages/grafana-ui/src/components/Combobox/storyUtils.ts +++ b/packages/grafana-ui/src/components/Combobox/storyUtils.ts @@ -37,3 +37,11 @@ export async function generateOptions(amount: number): Promise value: index.toString(), })); } + +export async function generateGroupingOptions(amount: number): Promise { + return Array.from({ length: amount }, (_, index) => ({ + label: 'Option ' + index, + value: index.toString(), + group: index % 9 !== 0 ? 'Group ' + Math.floor(index / 10) : undefined, + })); +} diff --git a/packages/grafana-ui/src/components/Combobox/types.ts b/packages/grafana-ui/src/components/Combobox/types.ts index c942d7a6357..9bc5cde0204 100644 --- a/packages/grafana-ui/src/components/Combobox/types.ts +++ b/packages/grafana-ui/src/components/Combobox/types.ts @@ -4,4 +4,5 @@ export type ComboboxOption = { label?: string; value: T; description?: string; + group?: string; }; diff --git a/packages/grafana-ui/src/components/Combobox/useOptions.ts b/packages/grafana-ui/src/components/Combobox/useOptions.ts index b76339cf54e..fc2747cee1e 100644 --- a/packages/grafana-ui/src/components/Combobox/useOptions.ts +++ b/packages/grafana-ui/src/components/Combobox/useOptions.ts @@ -95,16 +95,39 @@ export function useOptions(rawOptions: AsyncOptions { - let currentOptions = []; - if (isAsync) { - currentOptions = addCustomValue(asyncOptions); - } else { - currentOptions = addCustomValue(rawOptions.filter(itemFilter(userTypedSearch))); + const organizeOptionsByGroup = useCallback((options: Array>) => { + const groupedOptions = new Map>>(); + for (const option of options) { + const groupExists = groupedOptions.has(option.group); + if (groupExists) { + groupedOptions.get(option.group)?.push(option); + } else { + groupedOptions.set(option.group, [option]); + } } - return currentOptions; - }, [isAsync, addCustomValue, asyncOptions, rawOptions, userTypedSearch]); + // Reorganize options to have groups first, then undefined group + const reorganizeOptions = []; + for (const [group, groupOptions] of groupedOptions) { + if (!group) { + continue; + } + reorganizeOptions.push(...groupOptions); + } + + const undefinedGroupOptions = groupedOptions.get(undefined); + if (undefinedGroupOptions) { + reorganizeOptions.push(...undefinedGroupOptions); + } + return reorganizeOptions; + }, []); + + const finalOptions = useMemo(() => { + const currentOptions = isAsync ? asyncOptions : rawOptions.filter(itemFilter(userTypedSearch)); + const currentOptionsOrganised = organizeOptionsByGroup(currentOptions); + + return addCustomValue(currentOptionsOrganised); + }, [isAsync, organizeOptionsByGroup, addCustomValue, asyncOptions, rawOptions, userTypedSearch]); return { options: finalOptions, updateOptions, asyncLoading, asyncError }; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c739515378a..2c90bf29511 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -724,6 +724,9 @@ "custom-value": { "description": "Use custom value" }, + "group": { + "undefined": "No group" + }, "options": { "no-found": "No options found." } diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 4c73778c894..61b47f9f9a5 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -724,6 +724,9 @@ "custom-value": { "description": "Ůşę čūşŧőm väľūę" }, + "group": { + "undefined": "Ńő ģřőūp" + }, "options": { "no-found": "Ńő őpŧįőʼnş ƒőūʼnđ." } From 33b48f7c6effc738efd98e28da0a7e3a4d69c835 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 12 Feb 2025 04:46:00 -0600 Subject: [PATCH 519/894] PanelPlugin: Allow inverting `hideFromDefaults` (#100473) --- .../src/panel/registryFactories.ts | 2 +- .../PanelEditor/OptionsPaneOptions.test.tsx | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/grafana-data/src/panel/registryFactories.ts b/packages/grafana-data/src/panel/registryFactories.ts index a312d0c128e..98d8acb660d 100644 --- a/packages/grafana-data/src/panel/registryFactories.ts +++ b/packages/grafana-data/src/panel/registryFactories.ts @@ -55,7 +55,7 @@ export function createFieldConfigRegistry( const customDefault = config.standardOptions[id]?.defaultValue; const customSettings = config.standardOptions[id]?.settings; - if (customHideFromDefaults) { + if (customHideFromDefaults !== undefined) { fieldConfigProp = { ...fieldConfigProp, hideFromDefaults: customHideFromDefaults, diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx index ad8c19ee30b..6387aac97a8 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx @@ -143,6 +143,29 @@ describe('OptionsPaneOptions', () => { expect(screen.queryByLabelText(OptionsPaneSelector.fieldLabel('TestPanel HiddenFromDef'))).not.toBeInTheDocument(); }); + it('should render options that are specifically not marked as hidden from defaults', () => { + const scenario = new OptionsPaneOptionsTestScenario(); + + scenario.plugin = getPanelPlugin({ + id: 'TestPanel', + }).useFieldConfig({ + standardOptions: {}, + useCustomConfig: (b) => { + b.addBooleanSwitch({ + name: 'CustomBool', + path: 'CustomBool', + }).addBooleanSwitch({ + name: 'HiddenFromDef', + path: 'HiddenFromDef', + hideFromDefaults: false, + }); + }, + }); + + scenario.render(); + expect(screen.queryByLabelText(OptionsPaneSelector.fieldLabel('TestPanel HiddenFromDef'))).toBeInTheDocument(); + }); + it('should create categories for field options with category', () => { const scenario = new OptionsPaneOptionsTestScenario(); scenario.render(); From 2f329c211d56c919ae671841ac6dfd8c4245cf94 Mon Sep 17 00:00:00 2001 From: antonio <45235678+tonypowa@users.noreply.github.com> Date: Wed, 12 Feb 2025 12:01:15 +0100 Subject: [PATCH 520/894] docs>tutorials>minor-update (#100488) * docs>tutorials>minor-update * link to part 5 from part 4 * all pretty, no pity --- .../alerting-get-started-pt4/index.md | 18 ++++++++++++++++++ .../alerting-get-started-pt5/index.md | 18 +++++++++--------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/sources/tutorials/alerting-get-started-pt4/index.md b/docs/sources/tutorials/alerting-get-started-pt4/index.md index 4ea4477e655..1b480d375b8 100644 --- a/docs/sources/tutorials/alerting-get-started-pt4/index.md +++ b/docs/sources/tutorials/alerting-get-started-pt4/index.md @@ -420,4 +420,22 @@ To deepen your understanding of Grafana’s templating, explore the following re - [**Notification template reference**](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/template-notifications/reference/): Lists the data available for use in notification templates and explores specific functions. - [**Alert rule template reference**](https://grafana.com/docs/grafana/latest/alerting/alerting-rules/templates/reference/): Covers the specifics of creating dynamic labels and annotations for alert rules using elements such as variables and functions. +## Learn more in [Grafana Alerting Part 5](http://www.grafana.com/tutorials/alerting-get-started-pt5/) + + + +{{< admonition type="tip" >}} + +In [Get started with Grafana Alerting - Part 5](http://www.grafana.com/tutorials/alerting-get-started-pt5/) you learn how to dynamically route alerts and link them to dashboards. + +{{< /admonition >}} + + + +{{< docs/ignore >}} + +In [Get started with Grafana Alerting - Part 5](http://www.grafana.com/tutorials/alerting-get-started-pt5/) you learn how to dynamically route alerts and link them to dashboards. + +{{< /docs/ignore >}} + diff --git a/docs/sources/tutorials/alerting-get-started-pt5/index.md b/docs/sources/tutorials/alerting-get-started-pt5/index.md index e4e08b82d1a..e2049b494ca 100644 --- a/docs/sources/tutorials/alerting-get-started-pt5/index.md +++ b/docs/sources/tutorials/alerting-get-started-pt5/index.md @@ -2,7 +2,7 @@ Feedback Link: https://github.com/grafana/tutorials/issues/new categories: - alerting -description: Learn how to dinamically route alerts and link them to dashboards — Part 5. +description: Learn how to dynamically route alerts and link them to dashboards — Part 5. labels: products: - enterprise @@ -14,7 +14,7 @@ title: Get started with Grafana Alerting - Part 5 weight: 60 killercoda: title: Get started with Grafana Alerting - Part 5 - description: Learn how to dinamically route alerts and link them to dashboards — Part 5. + description: Learn how to dynamically route alerts and link them to dashboards — Part 5. backend: imageid: ubuntu --- @@ -30,7 +30,7 @@ In this tutorial, we focus on optimizing your alerting strategy using Grafana fo In this tutorial you will learn how to: - Leverage notification policies for **dynamic routing based on query values**: Use notification policies to route alerts based on dynamically generated labels, in a way that critical alerts reach the on-call team and less urgent ones go to a general monitoring channel. -- Set **mute timings** to suppress certain alerts during maintenance, or weekends. +- Set **mute timings** to suppress certain alerts during maintenance or weekends. - **Link alerts to dashboards** to provide more context to resolve issues. @@ -53,7 +53,7 @@ In this tutorial you will learn how to: ### Set up the Grafana stack -To demonstrate the observation of data using the Grafana stack, download and run the following files. +To observe data using the Grafana stack, download and run the following files. 1. Clone the [tutorial environment repository](https://github.com/tonypowa/grafana-prometheus-alerting-demo.git). @@ -199,9 +199,9 @@ The time-series visualization supports alert rules to provide more context in th flask_app_memory_usage{environment="prod"} ``` - {{< figure src="/media/docs/alerting/time-series_cpu_mem_usage_metrics.png" max-width="1200px" caption="Time-series panel displaying CPU and memory usage metrics in production." >}} + {{< figure src="/media/docs/alerting/time-series_cpu_mem_usage_metrics.png" max-width="1200px" caption="Time-series panel displaying CPU and memory usage metrics in production." >}} - Both metrics return labels that we’ll use later to link alert instances with the appropriate routing. These labels help define how alerts are routed based on their environment or other criteria. + Both metrics return labels that we’ll use later to link alert instances with the appropriate routing. These labels help define how alerts are routed based on their environment or other criteria. 1. Click **Save dashboard**. @@ -252,11 +252,11 @@ Follow these steps to manually create alert rules and link them to a visualizati ## Create an alert rule for CPU usage 1. Navigate to **Alerts & IRM > Alerting > Alert rules** from the Grafana sidebar. -1. Click **+ New alert** rule to create a new alert. +1. Click **+ New alert rule** rule to create a new alert. ### Enter alert rule name -Make it short and descriptive as this will appear in your alert notification. For instance, `CPU usage` . +Make it short and descriptive, as this will appear in your alert notification. For instance, `CPU usage` . ### Define query and alert condition @@ -354,7 +354,7 @@ Now that we've set up notification policies, we can demonstrate how to mute aler Mute timings are useful for suppressing alerts with certain labels during maintenance windows or weekends. 1. Navigate to **Alerts & IRM > Alerting > Notification Policies**. - - Enter a name. E.g., `Planned downtime` , or `Non-business hours`. + - Enter a name, e.g. `Planned downtime` or `Non-business hours`. - Select **Sat** and **Sun**, to apply the mute timing to all Saturdays and Sundays. - Click **Save mute timing**. 1. Add mute timing to the desired policy: From 91242340c18b71696ef598d9c56f71fb6db63c6c Mon Sep 17 00:00:00 2001 From: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> Date: Wed, 12 Feb 2025 07:09:22 -0500 Subject: [PATCH 521/894] Prometheus: Fix operator handling when making label expressions utf-8 friendly (#100475) * fix: operator handling * refactor: stay dry --- .../src/utf8_support.test.ts | 27 +++++++++++++++++++ .../grafana-prometheus/src/utf8_support.ts | 16 +++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/grafana-prometheus/src/utf8_support.test.ts b/packages/grafana-prometheus/src/utf8_support.test.ts index 5b02d3920e3..4ee3af057ef 100644 --- a/packages/grafana-prometheus/src/utf8_support.test.ts +++ b/packages/grafana-prometheus/src/utf8_support.test.ts @@ -124,4 +124,31 @@ describe('wrapUtf8Filters', () => { const expected = 'key1="nested \\"escaped\\" quotes",key2="value with \\"escaped\\" quotes"'; expect(result).toEqual(expected); }); + + it('should handle different Prometheus operators correctly', () => { + const inputs = [ + 'label="value"', // equals + 'label!="different value"', // not equals + 'label=~"regex.*value"', // regex match + 'label!~"not.regex.*value"', // regex not match + 'utf8.label.spaß!="no match"', // utf8 with not equals + 'utf8.label.spaß=~"match.*"', // utf8 with regex match + 'complex case=~".*",simple="value"', // multiple operators + ]; + + const expected = [ + 'label="value"', + 'label!="different value"', + 'label=~"regex.*value"', + 'label!~"not.regex.*value"', + '"utf8.label.spaß"!="no match"', + '"utf8.label.spaß"=~"match.*"', + '"complex case"=~".*",simple="value"', + ]; + + inputs.forEach((input, index) => { + const result = wrapUtf8Filters(input); + expect(result).toEqual(expected[index]); + }); + }); }); diff --git a/packages/grafana-prometheus/src/utf8_support.ts b/packages/grafana-prometheus/src/utf8_support.ts index 9f757c0bc73..a9ce776364b 100644 --- a/packages/grafana-prometheus/src/utf8_support.ts +++ b/packages/grafana-prometheus/src/utf8_support.ts @@ -82,10 +82,19 @@ const isValidCodePoint = (codePoint: number): boolean => { export const wrapUtf8Filters = (filterStr: string): string => { const resultArray: string[] = []; + const operatorRegex = /(=~|!=|!~|=)/; // NOTE: the order of the operators is important here let currentKey = ''; let currentValue = ''; let inQuotes = false; let temp = ''; + const addResult = () => { + const operatorMatch = temp.match(operatorRegex); + if (operatorMatch) { + const operator = operatorMatch[0]; + [currentKey, currentValue] = temp.split(operator); + resultArray.push(`${utf8Support(currentKey.trim())}${operator}"${currentValue.slice(1, -1)}"`); + } + }; for (const char of filterStr) { if (char === '"' && temp[temp.length - 1] !== '\\') { @@ -94,8 +103,7 @@ export const wrapUtf8Filters = (filterStr: string): string => { temp += char; } else if (char === ',' && !inQuotes) { // When outside quotes and encountering ',', finalize the current pair - [currentKey, currentValue] = temp.split('='); - resultArray.push(`${utf8Support(currentKey.trim())}="${currentValue.slice(1, -1)}"`); + addResult(); temp = ''; // Reset for the next pair } else { // Collect characters @@ -105,9 +113,7 @@ export const wrapUtf8Filters = (filterStr: string): string => { // Handle the last key-value pair if (temp) { - [currentKey, currentValue] = temp.split('='); - resultArray.push(`${utf8Support(currentKey.trim())}="${currentValue.slice(1, -1)}"`); + addResult(); } - return resultArray.join(','); }; From a5c8b5ed8371d151304d2decac4d2ef3258f79bd Mon Sep 17 00:00:00 2001 From: Solomon Himelbloom <7608183+TechSolomon@users.noreply.github.com> Date: Wed, 12 Feb 2025 04:42:12 -0900 Subject: [PATCH 522/894] docs: Copy code to clipboard (`set-up-https.md`) (#100509) --- docs/sources/setup-grafana/set-up-https.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/setup-grafana/set-up-https.md b/docs/sources/setup-grafana/set-up-https.md index 7cf65699203..cb508a87898 100644 --- a/docs/sources/setup-grafana/set-up-https.md +++ b/docs/sources/setup-grafana/set-up-https.md @@ -48,13 +48,13 @@ This section shows you how to use `openssl` tooling to generate all necessary fi 1. Run the following command to generate a 2048-bit RSA private key, which is used to decrypt traffic: ```bash - $ sudo openssl genrsa -out /etc/grafana/grafana.key 2048 + sudo openssl genrsa -out /etc/grafana/grafana.key 2048 ``` 1. Run the following command to generate a certificate, using the private key from the previous step. ```bash - $ sudo openssl req -new -key /etc/grafana/grafana.key -out /etc/grafana/grafana.csr + sudo openssl req -new -key /etc/grafana/grafana.key -out /etc/grafana/grafana.csr ``` When prompted, answer the questions, which might include your fully-qualified domain name, email address, country code, and others. The following example is similar to the prompts you will see. From ee0a1391dfe851596c31b0e7aae1c9f8ec7359b9 Mon Sep 17 00:00:00 2001 From: Misi Date: Wed, 12 Feb 2025 14:51:29 +0100 Subject: [PATCH 523/894] Auth: Add OrgRole to ID token (#100383) * Changes for Users and ServiceAccounts * Align tests --- go.mod | 2 +- go.sum | 4 +- pkg/api/http_server.go | 4 +- pkg/api/org_users.go | 6 ++ pkg/api/org_users_test.go | 74 ++++++++++----- pkg/api/user_test.go | 4 +- pkg/apimachinery/go.mod | 2 +- pkg/apimachinery/go.sum | 4 +- pkg/services/auth/id.go | 1 + pkg/services/auth/idimpl/service.go | 4 + pkg/services/auth/idimpl/service_test.go | 2 +- pkg/services/auth/idtest/fake.go | 42 +++++++++ pkg/services/auth/idtest/mock.go | 93 ++++++++++++++----- .../serviceaccounts/manager/service.go | 10 ++ pkg/services/user/userimpl/verifier_test.go | 4 +- pkg/storage/unified/apistore/go.mod | 2 +- pkg/storage/unified/apistore/go.sum | 4 +- pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/go.sum | 4 +- 19 files changed, 202 insertions(+), 66 deletions(-) create mode 100644 pkg/services/auth/idtest/fake.go diff --git a/go.mod b/go.mod index 59cc35cf90e..3ded590b054 100644 --- a/go.mod +++ b/go.mod @@ -72,7 +72,7 @@ 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-20250207161551-04c87cf39038 // @grafana/alerting-backend - github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @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 diff --git a/go.sum b/go.sum index 7ce560e11a0..f73dcbe3a18 100644 --- a/go.sum +++ b/go.sum @@ -1511,8 +1511,8 @@ 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-20250207161551-04c87cf39038 h1:dG/UKAjY/KlKp9fY8aEm+gSQHHRmPm5q+9cea3hRSu8= github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 h1:0C0a1FEkecxDk60su4uuOozqBzqz/4nmfNFSxXnCViY= -github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569 h1:bQw6fdcxVdZ6xmZVhMtRgGEKIT1Zc6y+i1PWF8tMX4Q= +github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 21f6f2dcd56..850a4ea5fd5 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -207,6 +207,7 @@ type HTTPServer struct { tempUserService tempUser.Service loginAttemptService loginAttempt.Service orgService org.Service + idService auth.IDService orgDeletionService org.DeletionService TeamService team.Service accesscontrolService accesscontrol.Service @@ -272,7 +273,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService, statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, promGatherer prometheus.Gatherer, starApi *starApi.API, promRegister prometheus.Registerer, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, anonService anonymous.Service, - userVerifier user.Verifier, pluginPreinstall plugininstaller.Preinstall, + userVerifier user.Verifier, pluginPreinstall plugininstaller.Preinstall, idService auth.IDService, ) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -360,6 +361,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi tempUserService: tempUserService, loginAttemptService: loginAttemptService, orgService: orgService, + idService: idService, orgDeletionService: orgDeletionService, TeamService: teamService, navTreeService: navTreeService, diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 1b83b4e3b77..f19d4138bb8 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -7,9 +7,11 @@ import ( "net/http" "strconv" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/authn" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -432,6 +434,10 @@ func (hs *HTTPServer) updateOrgUserHelper(c *contextmodel.ReqContext, cmd org.Up } } + if err := hs.idService.RemoveIDToken(c.Req.Context(), &authn.Identity{ID: strconv.FormatInt(cmd.UserID, 10), Type: claims.TypeUser, OrgID: cmd.OrgID}); err != nil { + return response.Error(http.StatusInternalServerError, "Failed to invalidate the ID token cache", err) + } + if err := hs.orgService.UpdateOrgUser(c.Req.Context(), &cmd); err != nil { if errors.Is(err, org.ErrLastOrgAdmin) { return response.Error(http.StatusBadRequest, "Cannot change role so that there is no organization admin left", nil) diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index 59bb47330d6..324e9da2b39 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -9,9 +9,12 @@ import ( "strings" "testing" + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/services/auth/idtest" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/dtos" @@ -202,11 +205,12 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) { func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) { type testCase struct { - desc string - SkipOrgRoleSync bool - AuthEnabled bool - AuthModule string - expectedCode int + desc string + SkipOrgRoleSync bool + AuthEnabled bool + AuthModule string + shouldInvalidateIDToken bool + expectedCode int } permissions := []accesscontrol.Permission{ {Action: accesscontrol.ActionOrgUsersRead, Scope: "users:*"}, @@ -216,11 +220,12 @@ func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) { } tests := []testCase{ { - desc: "should be able to change basicRole when skip_org_role_sync true", - SkipOrgRoleSync: true, - AuthEnabled: true, - AuthModule: login.LDAPAuthModule, - expectedCode: http.StatusOK, + desc: "should be able to change basicRole when skip_org_role_sync true", + SkipOrgRoleSync: true, + AuthEnabled: true, + AuthModule: login.LDAPAuthModule, + shouldInvalidateIDToken: true, + expectedCode: http.StatusOK, }, { desc: "should not be able to change basicRole when skip_org_role_sync false", @@ -237,18 +242,20 @@ func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) { expectedCode: http.StatusForbidden, }, { - desc: "should be able to change basicRole with a basic Auth", - SkipOrgRoleSync: false, - AuthEnabled: false, - AuthModule: "", - expectedCode: http.StatusOK, + desc: "should be able to change basicRole with a basic Auth", + SkipOrgRoleSync: false, + AuthEnabled: false, + AuthModule: "", + shouldInvalidateIDToken: true, + expectedCode: http.StatusOK, }, { - desc: "should be able to change basicRole with a basic Auth", - SkipOrgRoleSync: true, - AuthEnabled: true, - AuthModule: "", - expectedCode: http.StatusOK, + desc: "should be able to change basicRole with a basic Auth", + SkipOrgRoleSync: true, + AuthEnabled: true, + AuthModule: "", + shouldInvalidateIDToken: true, + expectedCode: http.StatusOK, }, } @@ -279,6 +286,11 @@ func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) { } hs.userService = &usertest.FakeUserService{ExpectedSignedInUser: userWithPermissions} hs.orgService = &orgtest.FakeOrgService{} + idService := &idtest.MockService{} + if tt.shouldInvalidateIDToken { + idService.On("RemoveIDToken", mock.Anything, mock.Anything).Return(nil) + } + hs.idService = idService hs.SocialService = &socialtest.FakeSocialService{ ExpectedAuthInfoProvider: &social.OAuthInfo{Enabled: tt.AuthEnabled, SkipOrgRoleSync: tt.SkipOrgRoleSync}, } @@ -615,6 +627,7 @@ func TestOrgUsersAPIEndpointWithSetPerms_AccessControl(t *testing.T) { ExpectedUser: &user.User{}, ExpectedSignedInUser: userWithPermissions(1, tt.permissions), } + hs.idService = &idtest.FakeService{} hs.accesscontrolService = &actest.FakeService{} }) @@ -637,16 +650,24 @@ func TestPatchOrgUsersAPIEndpoint_AccessControl(t *testing.T) { name string role org.RoleType permissions []accesscontrol.Permission + setup func(*testing.T, *idtest.MockService) input string expectedCode int } tests := []testCase{ { - name: "user with permissions can update org role", - permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: "users:*"}}, - role: org.RoleAdmin, - input: `{"role": "Viewer"}`, + name: "user with permissions can update org role", + permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: "users:*"}}, + role: org.RoleAdmin, + input: `{"role": "Viewer"}`, + setup: func(t *testing.T, idService *idtest.MockService) { + idService.On("RemoveIDToken", mock.Anything, mock.MatchedBy(func(id *authn.Identity) bool { + return id.GetIdentityType() == types.TypeUser && + id.GetID() == "user:1" && + id.GetOrgID() == int64(1) + })).Return(nil) + }, expectedCode: http.StatusOK, }, { @@ -673,6 +694,11 @@ func TestPatchOrgUsersAPIEndpoint_AccessControl(t *testing.T) { AuthModule: "", }, } + idService := &idtest.MockService{} + if tt.setup != nil { + tt.setup(t, idService) + } + hs.idService = idService hs.accesscontrolService = &actest.FakeService{} hs.userService = &usertest.FakeUserService{ ExpectedUser: &user.User{}, diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index 557af60259a..c452802bba4 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -463,7 +463,7 @@ func setupUpdateEmailTests(t *testing.T, cfg *setting.Cfg) (*user.User, *HTTPSer require.NoError(t, err) nsMock := notifications.MockNotificationService() - verifier := userimpl.ProvideVerifier(cfg, userSvc, tempUserService, nsMock, &idtest.MockService{}) + verifier := userimpl.ProvideVerifier(cfg, userSvc, tempUserService, nsMock, &idtest.FakeService{}) hs := &HTTPServer{ Cfg: cfg, @@ -688,7 +688,7 @@ func TestUser_UpdateEmail(t *testing.T) { hs.tempUserService = tempUserSvc hs.NotificationService = nsMock hs.SecretsService = fakes.NewFakeSecretsService() - hs.userVerifier = userimpl.ProvideVerifier(settings, userSvc, tempUserSvc, nsMock, &idtest.MockService{}) + hs.userVerifier = userimpl.ProvideVerifier(settings, userSvc, tempUserSvc, nsMock, &idtest.FakeService{}) // User is internal hs.authInfoService = &authinfotest.FakeService{ExpectedError: user.ErrUserNotFound} }) diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index f74e3ba70a4..a9074f89305 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery go 1.23.1 require ( - github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team github.com/stretchr/testify v1.10.0 k8s.io/apimachinery v0.32.1 diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 20fc926c23f..852b52d54c3 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -32,8 +32,8 @@ 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-20250204100101-00a7f40e4029 h1:0C0a1FEkecxDk60su4uuOozqBzqz/4nmfNFSxXnCViY= -github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569 h1:bQw6fdcxVdZ6xmZVhMtRgGEKIT1Zc6y+i1PWF8tMX4Q= +github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/pkg/services/auth/id.go b/pkg/services/auth/id.go index 18096eee393..b6797004bc6 100644 --- a/pkg/services/auth/id.go +++ b/pkg/services/auth/id.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" ) +//go:generate mockery --name IDService --structname MockService --outpkg idtest --filename mock.go --output ./idtest/ type IDService interface { // SignIdentity signs a id token for provided identity that can be forwarded to plugins and external services SignIdentity(ctx context.Context, id identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) diff --git a/pkg/services/auth/idimpl/service.go b/pkg/services/auth/idimpl/service.go index 1ade33e37f8..a4b89204d7b 100644 --- a/pkg/services/auth/idimpl/service.go +++ b/pkg/services/auth/idimpl/service.go @@ -109,6 +109,10 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri idClaims.Rest.DisplayName = id.GetName() } + if id.GetOrgRole().IsValid() { + idClaims.Rest.Role = string(id.GetOrgRole()) + } + token, err := s.signer.SignIDToken(ctx, idClaims) if err != nil { s.metrics.failedTokenSigningCounter.Inc() diff --git a/pkg/services/auth/idimpl/service_test.go b/pkg/services/auth/idimpl/service_test.go index f0ab27db562..0f3814968e3 100644 --- a/pkg/services/auth/idimpl/service_test.go +++ b/pkg/services/auth/idimpl/service_test.go @@ -34,7 +34,7 @@ func Test_ProvideService(t *testing.T) { } func TestService_SignIdentity(t *testing.T) { - signer := &idtest.MockSigner{ + signer := &idtest.FakeSigner{ SignIDTokenFn: func(_ context.Context, claims *auth.IDClaims) (string, error) { key := []byte("key") s, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, nil) diff --git a/pkg/services/auth/idtest/fake.go b/pkg/services/auth/idtest/fake.go new file mode 100644 index 00000000000..510d5b88687 --- /dev/null +++ b/pkg/services/auth/idtest/fake.go @@ -0,0 +1,42 @@ +package idtest + +import ( + "context" + + authnlib "github.com/grafana/authlib/authn" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/auth" +) + +var _ auth.IDService = (*FakeService)(nil) + +type FakeService struct { + SignIdentityFn func(ctx context.Context, identity identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) + RemoveIDTokenFn func(ctx context.Context, identity identity.Requester) error +} + +func (m *FakeService) SignIdentity(ctx context.Context, identity identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) { + if m.SignIdentityFn != nil { + return m.SignIdentityFn(ctx, identity) + } + return "", nil, nil +} + +func (m *FakeService) RemoveIDToken(ctx context.Context, identity identity.Requester) error { + if m.RemoveIDTokenFn != nil { + return m.RemoveIDTokenFn(ctx, identity) + } + return nil +} + +type FakeSigner struct { + SignIDTokenFn func(ctx context.Context, claims *auth.IDClaims) (string, error) +} + +func (s *FakeSigner) SignIDToken(ctx context.Context, claims *auth.IDClaims) (string, error) { + if s.SignIDTokenFn != nil { + return s.SignIDTokenFn(ctx, claims) + } + return "", nil +} diff --git a/pkg/services/auth/idtest/mock.go b/pkg/services/auth/idtest/mock.go index cd506836d13..cb9aeadf316 100644 --- a/pkg/services/auth/idtest/mock.go +++ b/pkg/services/auth/idtest/mock.go @@ -1,42 +1,87 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + package idtest import ( - "context" + context "context" - authnlib "github.com/grafana/authlib/authn" + authn "github.com/grafana/authlib/authn" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/services/auth" + identity "github.com/grafana/grafana/pkg/apimachinery/identity" + + mock "github.com/stretchr/testify/mock" ) -var _ auth.IDService = (*MockService)(nil) - +// MockService is an autogenerated mock type for the IDService type type MockService struct { - SignIdentityFn func(ctx context.Context, identity identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) - RemoveIDTokenFn func(ctx context.Context, identity identity.Requester) error + mock.Mock } -func (m *MockService) SignIdentity(ctx context.Context, identity identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) { - if m.SignIdentityFn != nil { - return m.SignIdentityFn(ctx, identity) +// RemoveIDToken provides a mock function with given fields: ctx, _a1 +func (_m *MockService) RemoveIDToken(ctx context.Context, _a1 identity.Requester) error { + ret := _m.Called(ctx, _a1) + + if len(ret) == 0 { + panic("no return value specified for RemoveIDToken") } - return "", nil, nil -} -func (m *MockService) RemoveIDToken(ctx context.Context, identity identity.Requester) error { - if m.RemoveIDTokenFn != nil { - return m.RemoveIDTokenFn(ctx, identity) + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) error); ok { + r0 = rf(ctx, _a1) + } else { + r0 = ret.Error(0) } - return nil + + return r0 } -type MockSigner struct { - SignIDTokenFn func(ctx context.Context, claims *auth.IDClaims) (string, error) -} +// SignIdentity provides a mock function with given fields: ctx, id +func (_m *MockService) SignIdentity(ctx context.Context, id identity.Requester) (string, *authn.Claims[authn.IDTokenClaims], error) { + ret := _m.Called(ctx, id) -func (s *MockSigner) SignIDToken(ctx context.Context, claims *auth.IDClaims) (string, error) { - if s.SignIDTokenFn != nil { - return s.SignIDTokenFn(ctx, claims) + if len(ret) == 0 { + panic("no return value specified for SignIdentity") } - return "", nil + + var r0 string + var r1 *authn.Claims[authn.IDTokenClaims] + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) (string, *authn.Claims[authn.IDTokenClaims], error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) string); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, identity.Requester) *authn.Claims[authn.IDTokenClaims]); ok { + r1 = rf(ctx, id) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*authn.Claims[authn.IDTokenClaims]) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, identity.Requester) error); ok { + r2 = rf(ctx, id) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockService { + mock := &MockService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock } diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 2bb64c3646a..f53e7b72d9b 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -17,6 +17,8 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" @@ -42,6 +44,7 @@ type ServiceAccountsService struct { secretScanService secretscan.Checker orgService org.Service serverLock *serverlock.ServerLockService + idService auth.IDService secretScanEnabled bool secretScanInterval time.Duration @@ -58,6 +61,7 @@ func ProvideServiceAccountsService( acService accesscontrol.Service, permissions accesscontrol.ServiceAccountPermissionsService, serverLockService *serverlock.ServerLockService, + idService auth.IDService, ) (*ServiceAccountsService, error) { serviceAccountsStore := database.ProvideServiceAccountsStore( cfg, @@ -77,6 +81,7 @@ func ProvideServiceAccountsService( backgroundLog: log.New("serviceaccounts.background"), orgService: orgService, serverLock: serverLockService, + idService: idService, } if err := RegisterRoles(acService); err != nil { @@ -265,6 +270,11 @@ func (sa *ServiceAccountsService) UpdateServiceAccount(ctx context.Context, orgI if err := validServiceAccountID(serviceAccountID); err != nil { return nil, err } + + if err := sa.idService.RemoveIDToken(ctx, &authn.Identity{ID: strconv.FormatInt(serviceAccountID, 10), Type: claims.TypeServiceAccount, OrgID: orgID}); err != nil { + return nil, err + } + return sa.store.UpdateServiceAccount(ctx, orgID, serviceAccountID, saForm) } diff --git a/pkg/services/user/userimpl/verifier_test.go b/pkg/services/user/userimpl/verifier_test.go index 8f7f17218d6..0bb9ecb2437 100644 --- a/pkg/services/user/userimpl/verifier_test.go +++ b/pkg/services/user/userimpl/verifier_test.go @@ -21,7 +21,7 @@ func TestVerifier_Start(t *testing.T) { ts := &tempusertest.FakeTempUserService{} us := &usertest.FakeUserService{} ns := notifications.MockNotificationService() - is := &idtest.MockService{} + is := &idtest.FakeService{} type calls struct { expireCalled bool @@ -116,7 +116,7 @@ func TestVerifier_Complete(t *testing.T) { ts := &tempusertest.FakeTempUserService{} us := &usertest.FakeUserService{} ns := notifications.MockNotificationService() - is := &idtest.MockService{} + is := &idtest.FakeService{} type calls struct { updateCalled bool diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 61ab8c11119..a409151a0d4 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -179,7 +179,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038 // indirect - github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 // indirect + github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569 // 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 9c7ca47a33a..73e0a03e693 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -561,8 +561,8 @@ 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-20250207161551-04c87cf39038 h1:dG/UKAjY/KlKp9fY8aEm+gSQHHRmPm5q+9cea3hRSu8= github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 h1:0C0a1FEkecxDk60su4uuOozqBzqz/4nmfNFSxXnCViY= -github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569 h1:bQw6fdcxVdZ6xmZVhMtRgGEKIT1Zc6y+i1PWF8tMX4Q= +github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index b5616a63c8c..3e2c7757443 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -11,7 +11,7 @@ replace ( require ( github.com/fullstorydev/grpchan v1.1.1 github.com/google/uuid v1.6.0 - github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 + github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569 github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 3bda999b30b..4569d6883a7 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -399,8 +399,8 @@ 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-20250207161551-04c87cf39038 h1:dG/UKAjY/KlKp9fY8aEm+gSQHHRmPm5q+9cea3hRSu8= github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 h1:0C0a1FEkecxDk60su4uuOozqBzqz/4nmfNFSxXnCViY= -github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569 h1:bQw6fdcxVdZ6xmZVhMtRgGEKIT1Zc6y+i1PWF8tMX4Q= +github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= From 983829e47bd87a1d72a3b14f6ec821667c25f636 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:13:05 +0100 Subject: [PATCH 524/894] Alerting docs: remove admonition about auto-generated policies (#100501) --- .../alerting-rules/create-grafana-managed-rule.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 60697fd1707..41c9150b52c 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -257,16 +257,11 @@ Complete the following steps to set up notifications. 1. You can also optionally select a mute timing as well as groupings and timings to define when not to send notifications. - {{< admonition type="note" >}} - An auto-generated notification policy is generated. Only admins can view these auto-generated policies from the **Notification policies** list view. Any changes have to be made in the alert rules form. {{< /admonition >}} - **Use notification policy** - 1. Choose this option to use the [notification policy tree](ref:notification-policies) to direct your notifications. + 1. Choose this option to use the [notification policy tree](ref:notification-policies) to handle alert notifications. - {{< admonition type="note" >}} - All alert rules and instances, irrespective of their labels, match the default notification policy. If there are no nested policies, or no nested policies match the labels in the alert rule or alert instance, then the default notification policy is the matching policy. - {{< /admonition >}} + All notifications for this alert rule are managed by the notification policy tree, which routes alerts based on their labels. If an alert does not match a specific policy, the default notification policy applies, ensuring all alerts are handled. 1. Preview your alert instance routing set up. From a9b4b1e5be23e4bd30dce11fc738f4ff4a6111b5 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 12 Feb 2025 14:37:04 +0000 Subject: [PATCH 525/894] Chore: pin tonistiigi/binfmt version (#100510) * Chore: pin tonistiigi/binfmt version * change version to qemu-v7.0.0-28 * uninstall first, log version * uninstall first, log version * uninstall first, log version --- .drone.yml | 10 +++++++--- scripts/drone/steps/rgm.star | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.drone.yml b/.drone.yml index 12e432f896a..c17e6d24a3f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -706,7 +706,9 @@ steps: token: from_secret: drone_token - commands: - - docker run --privileged --rm tonistiigi/binfmt --install all + - docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --version + - docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*' + - docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 -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 @@ -2124,7 +2126,9 @@ steps: image: node:22.11.0-alpine name: build-frontend-packages - commands: - - docker run --privileged --rm tonistiigi/binfmt --install all + - docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --version + - docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*' + - docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 -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 @@ -5601,6 +5605,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 39572225832c2de6e648b6a4d66a36d212777390bf6fd4643f9cfaad21182df3 +hmac: ed11784cafc81bbb617e931f1aa392a9de8bde04dd5d14e51baf856a3942dd91 ... diff --git a/scripts/drone/steps/rgm.star b/scripts/drone/steps/rgm.star index fd96a60d156..68dd2a082e5 100644 --- a/scripts/drone/steps/rgm.star +++ b/scripts/drone/steps/rgm.star @@ -47,7 +47,9 @@ def rgm_artifacts_step( "_EXPERIMENTAL_DAGGER_CLOUD_TOKEN": from_secret(rgm_dagger_token), }, "commands": [ - "docker run --privileged --rm tonistiigi/binfmt --install all", + "docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --version", + "docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*'", + "docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all", cmd + "--go-version={} ".format(golang_version) + "--yarn-cache=$$YARN_CACHE_FOLDER " + @@ -78,7 +80,9 @@ def rgm_build_docker_step(ubuntu, alpine, depends_on = ["yarn-install"], file = "_EXPERIMENTAL_DAGGER_CLOUD_TOKEN": from_secret(rgm_dagger_token), }, "commands": [ - "docker run --privileged --rm tonistiigi/binfmt --install all", + "docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --version", + "docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*'", + "docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all", "/src/grafana-build artifacts " + "-a docker:grafana:linux/amd64 " + "-a docker:grafana:linux/amd64:ubuntu " + From a1e59a92b02168442481bc1efbea48f96c8986d1 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Wed, 12 Feb 2025 15:12:11 +0000 Subject: [PATCH 526/894] Alerting: Allow collapsing of rule sections and fix Grafana configure link (#100290) --- .../components/DataSourceSection.tsx | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx b/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx index 9df0aa641b3..11f4f5cbf01 100644 --- a/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx +++ b/public/app/features/alerting/unified/rule-list/components/DataSourceSection.tsx @@ -1,14 +1,16 @@ import { css } from '@emotion/css'; import { PropsWithChildren, ReactNode } from 'react'; +import { useToggle } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; -import { LinkButton, Stack, Text, useStyles2 } from '@grafana/ui'; +import { IconButton, LinkButton, Stack, Text, useStyles2 } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; -import { RulesSourceIdentifier } from 'app/types/unified-alerting'; +import { GrafanaRulesSourceSymbol, RulesSourceIdentifier } from 'app/types/unified-alerting'; import { RulesSourceApplication } from 'app/types/unified-alerting-dto'; import { Spacer } from '../../components/Spacer'; import { WithReturnButton } from '../../components/WithReturnButton'; +import { isAdmin } from '../../utils/misc'; import { DataSourceIcon } from './Namespace'; import { LoadingIndicator } from './RuleGroup'; @@ -32,7 +34,17 @@ export const DataSourceSection = ({ description = null, }: DataSourceSectionProps) => { const styles = useStyles2(getStyles); - + const [isCollapsed, toggleCollapsed] = useToggle(false); + const configureLink = (() => { + if (uid === GrafanaRulesSourceSymbol) { + const userIsAdmin = isAdmin(); + if (!userIsAdmin) { + return; + } + return '/alerting/admin'; + } + return `/connections/datasources/edit/${String(uid)}`; + })(); return (
      @@ -41,7 +53,13 @@ export const DataSourceSection = ({
      {loader ?? ( + {application && } + {name} @@ -52,19 +70,21 @@ export const DataSourceSection = ({ )} - - Configure - - } - /> + {configureLink && ( + + Configure + + } + /> + )} )}
      -
      {children}
      + {!isCollapsed &&
      {children}
      }
      ); From 1f6142dd8fa0d733c3f8da389b4e7810ce869747 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Wed, 12 Feb 2025 15:14:43 +0000 Subject: [PATCH 527/894] Chore: Remove betterer:merge and revert previous changes to betterer:stats (#98609) --- package.json | 3 +-- ...reportBettererStats.mjs => reportBettererStats.ts} | 11 +++++------ scripts/cli/tsconfig.json | 5 ++++- 3 files changed, 10 insertions(+), 9 deletions(-) rename scripts/cli/{reportBettererStats.mjs => reportBettererStats.ts} (74%) diff --git a/package.json b/package.json index 189fc00835e..45e0a3fc92d 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,7 @@ "ci:test-frontend": "yarn run test:ci", "i18n:stats": "node ./scripts/cli/reportI18nStats.mjs", "betterer": "betterer --tsconfig ./scripts/cli/tsconfig.json", - "betterer:merge": "betterer merge --tsconfig ./scripts/cli/tsconfig.json", - "betterer:stats": "node ./scripts/cli/reportBettererStats.mjs", + "betterer:stats": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/reportBettererStats.ts", "betterer:issues": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/generateBettererIssues.ts", "plugin:build": "nx run-many -t build --projects='tag:scope:plugin'", "plugin:build:commit": "nx run-many -t build:commit --projects='tag:scope:plugin'", diff --git a/scripts/cli/reportBettererStats.mjs b/scripts/cli/reportBettererStats.ts similarity index 74% rename from scripts/cli/reportBettererStats.mjs rename to scripts/cli/reportBettererStats.ts index 0f1eda1d646..135f98c7752 100644 --- a/scripts/cli/reportBettererStats.mjs +++ b/scripts/cli/reportBettererStats.ts @@ -1,8 +1,7 @@ -// @ts-check import { betterer } from '@betterer/betterer'; -import _ from 'lodash'; +import { camelCase } from 'lodash'; -function logStat(name, value) { +function logStat(name: string, value: number) { // Note that this output format must match the parsing in ci-frontend-metrics.sh // which expects the two values to be separated by a space console.log(`${name} ${value}`); @@ -13,11 +12,11 @@ async function main() { for (const testResults of results.resultSummaries) { const countByMessage = {}; - const name = _.camelCase(testResults.name); + const name = camelCase(testResults.name); Object.values(testResults.details) .flatMap((v) => v) .forEach((detail) => { - const message = _.camelCase(detail.message); + const message = camelCase(detail.message); const metricName = `${name}_${message}`; if (metricName in countByMessage) { countByMessage[metricName]++; @@ -26,7 +25,7 @@ async function main() { } }); - for (const [metricName, count] of Object.entries(countByMessage)) { + for (const [metricName, count] of Object.entries(countByMessage)) { logStat(metricName, count); } } diff --git a/scripts/cli/tsconfig.json b/scripts/cli/tsconfig.json index 9ad59a649d1..0d18e875c5e 100644 --- a/scripts/cli/tsconfig.json +++ b/scripts/cli/tsconfig.json @@ -6,6 +6,9 @@ "extends": "../../tsconfig.json", "ts-node": { "transpileOnly": true, - "swc": true + "swc": true, + "compilerOptions": { + "module": "commonjs" + } } } From 6e00954bb170b3900f6c2053326c7a3b25dc6a10 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 12 Feb 2025 16:44:49 +0100 Subject: [PATCH 528/894] Devenv: Use newer label syntax (#100507) --- devenv/docker/blocks/prometheus_random_data/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devenv/docker/blocks/prometheus_random_data/Dockerfile b/devenv/docker/blocks/prometheus_random_data/Dockerfile index c4ebc8cb0ec..41f90677409 100644 --- a/devenv/docker/blocks/prometheus_random_data/Dockerfile +++ b/devenv/docker/blocks/prometheus_random_data/Dockerfile @@ -7,7 +7,7 @@ RUN CGO_ENABLED=0 GOOS=linux go install -tags netgo -ldflags '-w' github.com/pro # Final image. FROM scratch -LABEL maintainer "The Prometheus Authors " +LABEL maintainer="The Prometheus Authors " COPY --from=builder /go/bin/random . EXPOSE 8080 ENTRYPOINT ["/random"] From f6f50f7693628878c2a1ca76114bdc6ca9f54ffb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:07:38 +0100 Subject: [PATCH 529/894] Update scenes to v6 (#100445) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +-- yarn.lock | 69 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 45e0a3fc92d..4d638dc7b9b 100644 --- a/package.json +++ b/package.json @@ -275,8 +275,8 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "5.42.0", - "@grafana/scenes-react": "5.42.0", + "@grafana/scenes": "6.0.1", + "@grafana/scenes-react": "6.0.1", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/yarn.lock b/yarn.lock index d5061c52875..f818433fbda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3814,11 +3814,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:5.42.0": - version: 5.42.0 - resolution: "@grafana/scenes-react@npm:5.42.0" +"@grafana/scenes-react@npm:6.0.1": + version: 6.0.1 + resolution: "@grafana/scenes-react@npm:6.0.1" dependencies: - "@grafana/scenes": "npm:5.42.0" + "@grafana/scenes": "npm:6.0.1" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3829,20 +3829,21 @@ __metadata: "@grafana/ui": ^11.0.0 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/c94db6d57b02be5f960e44dc0c4be46e5e4708ede34f2f4e30934134543db88c8b553aceb89d9b3ae4dbf5d1eab1f48e534eb9a31d5c99b8908f715e45cb6dcf + react-router-dom: ^6.28.0 + checksum: 10/e4ad83cc628f17232fe9c8d74f641c65e2e289c177ce88a6990d00f6bea4e1a091115e7b98200de7bcff14ace0fe20eb816141fe533fee7d2ad5f7f665404d2c languageName: node linkType: hard -"@grafana/scenes@npm:5.42.0": - version: 5.42.0 - resolution: "@grafana/scenes@npm:5.42.0" +"@grafana/scenes@npm:6.0.1": + version: 6.0.1 + resolution: "@grafana/scenes@npm:6.0.1" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" "@tanstack/react-virtual": "npm:^3.9.0" - react-grid-layout: "npm:^1.3.4" - react-use: "npm:^17.5.0" - react-virtualized-auto-sizer: "npm:^1.0.24" + react-grid-layout: "npm:1.3.4" + react-use: "npm:17.5.0" + react-virtualized-auto-sizer: "npm:1.0.24" uuid: "npm:^9.0.0" peerDependencies: "@grafana/data": ">=10.4" @@ -3852,7 +3853,8 @@ __metadata: "@grafana/ui": ">=10.4" react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/3232a499b839a45c8eed924441e4423d1b1d9f6b4ea6725437d357c1afea93d9ab300c46319b1ddab76b0da1197905b22c3abe8095d78c3320b1befb4731bd24 + react-router-dom: ^6.28.0 + checksum: 10/6862e57358ba2e63f139e7f3bb977b19945f67eb070aa2c85c073a55dc460d3ccfeecfee22aea92c660a7632ac997e6cd945f9466b64103436a221979e6e8fcb languageName: node linkType: hard @@ -18149,8 +18151,8 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:5.42.0" - "@grafana/scenes-react": "npm:5.42.0" + "@grafana/scenes": "npm:6.0.1" + "@grafana/scenes-react": "npm:6.0.1" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0" @@ -23004,7 +23006,7 @@ __metadata: languageName: node linkType: hard -"nano-css@npm:^5.3.1, nano-css@npm:^5.6.2": +"nano-css@npm:^5.3.1, nano-css@npm:^5.6.1, nano-css@npm:^5.6.2": version: 5.6.2 resolution: "nano-css@npm:5.6.2" dependencies: @@ -27008,6 +27010,31 @@ __metadata: languageName: node linkType: hard +"react-use@npm:17.5.0": + version: 17.5.0 + resolution: "react-use@npm:17.5.0" + dependencies: + "@types/js-cookie": "npm:^2.2.6" + "@xobotyi/scrollbar-width": "npm:^1.9.5" + copy-to-clipboard: "npm:^3.3.1" + fast-deep-equal: "npm:^3.1.3" + fast-shallow-equal: "npm:^1.0.0" + js-cookie: "npm:^2.2.1" + nano-css: "npm:^5.6.1" + react-universal-interface: "npm:^0.6.2" + resize-observer-polyfill: "npm:^1.5.1" + screenfull: "npm:^5.1.0" + set-harmonic-interval: "npm:^1.0.1" + throttle-debounce: "npm:^3.0.1" + ts-easing: "npm:^0.2.0" + tslib: "npm:^2.1.0" + peerDependencies: + react: "*" + react-dom: "*" + checksum: 10/5d81fe0902303d3ed7810cdd56c6cae12b08124a3d4fcbfa3924327105b81447b039ea9d6aff20aac3c13999f949000870a7a2fa29fe20ed844ac26606462fa0 + languageName: node + linkType: hard + "react-use@npm:17.5.1": version: 17.5.1 resolution: "react-use@npm:17.5.1" @@ -27069,7 +27096,17 @@ __metadata: languageName: node linkType: hard -"react-virtualized-auto-sizer@npm:1.0.25, react-virtualized-auto-sizer@npm:^1.0.24, react-virtualized-auto-sizer@npm:^1.0.6": +"react-virtualized-auto-sizer@npm:1.0.24": + version: 1.0.24 + resolution: "react-virtualized-auto-sizer@npm:1.0.24" + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + checksum: 10/02101a340bdbe3e40c49dbc52e524eb7ca18832690e91f045a25675600d7adc0a63e800a4ace6a014132adcdcce0e12a8137971de408427a5a3112d7c87c9f3e + languageName: node + linkType: hard + +"react-virtualized-auto-sizer@npm:1.0.25, react-virtualized-auto-sizer@npm:^1.0.6": version: 1.0.25 resolution: "react-virtualized-auto-sizer@npm:1.0.25" peerDependencies: From 2b054d4154ad174d630dadec49fa26a97d151232 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Wed, 12 Feb 2025 16:11:12 +0000 Subject: [PATCH 530/894] Correct release branch trigger glob (#100496) --- .github/workflows/publish-technical-documentation-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-technical-documentation-release.yml b/.github/workflows/publish-technical-documentation-release.yml index bd77c286018..57d779660c5 100644 --- a/.github/workflows/publish-technical-documentation-release.yml +++ b/.github/workflows/publish-technical-documentation-release.yml @@ -3,7 +3,7 @@ name: publish-technical-documentation-release on: push: branches: - - release-v[0-9]+.[0-9]+.[0-9]+ + - release-[0-9]+.[0-9]+.[0-9]+ tags: - v[0-9]+.[0-9]+.[0-9]+ paths: From cfc529cb035d66f89a0482ae847dce6aff29aa50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Feb 2025 17:22:56 +0100 Subject: [PATCH 531/894] Design/Theme: Change dropdown background in dark themes (#100415) * Dropdowns: Change background for Selects/Comboboxs * Update * Update * Review fixes * Update --- .../src/themes/createComponents.ts | 2 +- .../shared/OperationInfoButton.tsx | 4 +-- .../src/components/Cascader/styles.ts | 7 +++--- .../src/components/Combobox/Combobox.tsx | 2 +- .../src/components/Combobox/MultiCombobox.tsx | 2 +- .../components/Combobox/getComboboxStyles.ts | 1 + .../src/components/Select/SelectMenu.tsx | 2 +- .../src/components/Select/getSelectStyles.ts | 1 + .../core/components/TagFilter/TagOption.tsx | 7 +++--- .../components/picker/DataSourceCard.tsx | 25 +++++++++++++------ .../components/picker/DataSourceList.tsx | 1 + .../components/picker/DataSourcePicker.tsx | 4 ++- 12 files changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/grafana-data/src/themes/createComponents.ts b/packages/grafana-data/src/themes/createComponents.ts index 70dbcd67cf5..e810a94b86e 100644 --- a/packages/grafana-data/src/themes/createComponents.ts +++ b/packages/grafana-data/src/themes/createComponents.ts @@ -80,7 +80,7 @@ export function createComponents(colors: ThemeColors, shadows: ThemeShadows): Th input, panel, dropdown: { - background: input.background, + background: colors.background.elevated, }, tooltip: { background: colors.background.elevated, diff --git a/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx b/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx index 69f26d84865..b576188a581 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx @@ -94,8 +94,8 @@ const getStyles = (theme: GrafanaTheme2) => { return { docBox: css({ overflow: 'hidden', - background: theme.colors.background.primary, - border: `1px solid ${theme.colors.border.strong}`, + background: theme.colors.background.elevated, + border: `1px solid ${theme.colors.border.weak}`, boxShadow: theme.shadows.z3, maxWidth: '600px', padding: theme.spacing(1), diff --git a/packages/grafana-ui/src/components/Cascader/styles.ts b/packages/grafana-ui/src/components/Cascader/styles.ts index 537bb8c4a7e..cef38b62dcc 100644 --- a/packages/grafana-ui/src/components/Cascader/styles.ts +++ b/packages/grafana-ui/src/components/Cascader/styles.ts @@ -72,8 +72,8 @@ export const getCascaderStyles = (theme: GrafanaTheme2) => ({ '.rc-cascader': { '&-menus': { overflow: 'hidden', - background: theme.colors.background.canvas, - border: `1px solid ${theme.colors.border.weak}`, + background: theme.colors.background.elevated, + border: `none`, borderRadius: theme.shape.radius.default, boxShadow: theme.shadows.z3, whiteSpace: 'nowrap', @@ -128,7 +128,7 @@ export const getCascaderStyles = (theme: GrafanaTheme2) => ({ height: '192px', listStyle: 'none', margin: 0, - padding: 0, + padding: theme.spacing(0.5), borderRight: `1px solid ${theme.colors.border.weak}`, overflow: 'auto', @@ -140,6 +140,7 @@ export const getCascaderStyles = (theme: GrafanaTheme2) => ({ height: theme.spacing(4), lineHeight: theme.spacing(4), padding: theme.spacing(0, 4, 0, 2), + borderRadius: theme.shape.radius.default, cursor: 'pointer', whiteSpace: 'nowrap', overflow: 'hidden', diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index 471bb22fe5d..5d6c5320402 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -407,7 +407,7 @@ export const Combobox = (props: ComboboxProps) => })} > {isOpen && ( - + {!asyncError && (
        {rowVirtualizer.getVirtualItems().map((virtualRow) => { diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 7b41ca2ee53..9a90dfd4588 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -344,7 +344,7 @@ export const MultiCombobox = (props: MultiComboboxPro {...getMenuProps({ ref: floatingRef })} > {isOpen && ( - +
          {rowVirtualizer.getVirtualItems().map((virtualRow) => { const startingNewGroup = isNewGroup(options[virtualRow.index], options[virtualRow.index - 1]); diff --git a/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts index f0ea95cacb2..6c788b15cbd 100644 --- a/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts +++ b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts @@ -46,6 +46,7 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => { option: css({ padding: MENU_ITEM_PADDING, cursor: 'pointer', + borderRadius: theme.shape.radius.default, width: '100%', '&:hover': { background: theme.colors.action.hover, diff --git a/packages/grafana-ui/src/components/Select/SelectMenu.tsx b/packages/grafana-ui/src/components/Select/SelectMenu.tsx index b557b2e9a65..e19c8d87546 100644 --- a/packages/grafana-ui/src/components/Select/SelectMenu.tsx +++ b/packages/grafana-ui/src/components/Select/SelectMenu.tsx @@ -54,7 +54,7 @@ export const SelectMenu = ({ style={{ maxHeight }} aria-label="Select options menu" > - + {toggleAllOptions && ( { whiteSpace: 'nowrap', cursor: 'pointer', borderLeft: '2px solid transparent', + borderRadius: theme.shape.radius.default, '&:hover': { background: theme.colors.action.hover, diff --git a/public/app/core/components/TagFilter/TagOption.tsx b/public/app/core/components/TagFilter/TagOption.tsx index 986d3f9ec7d..57abca0f942 100644 --- a/public/app/core/components/TagFilter/TagOption.tsx +++ b/public/app/core/components/TagFilter/TagOption.tsx @@ -27,16 +27,17 @@ export const TagOption = ({ data, className, label, isFocused, innerProps }: Opt const getStyles = (theme: GrafanaTheme2) => { return { option: css({ - padding: theme.spacing(1), + padding: theme.spacing(0.5), whiteSpace: 'nowrap', cursor: 'pointer', borderLeft: '2px solid transparent', + borderRadius: theme.shape.radius.default, '&:hover': { - background: theme.colors.background.secondary, + background: theme.colors.action.hover, }, }), optionFocused: css({ - background: theme.colors.background.secondary, + background: theme.colors.action.focus, borderStyle: 'solid', borderTop: 0, borderRight: 0, diff --git a/public/app/features/datasources/components/picker/DataSourceCard.tsx b/public/app/features/datasources/components/picker/DataSourceCard.tsx index 7e14837a32c..84403ae4298 100644 --- a/public/app/features/datasources/components/picker/DataSourceCard.tsx +++ b/public/app/features/datasources/components/picker/DataSourceCard.tsx @@ -41,15 +41,14 @@ function getStyles(theme: GrafanaTheme2, builtIn = false) { return { card: css({ cursor: 'pointer', - backgroundColor: theme.colors.background.primary, - borderBottom: `1px solid ${theme.colors.border.weak}`, + backgroundColor: 'transparent', // Move to list component marginBottom: 0, - // set this to 0 to override the default card radius - // also need to disable our eslint rule - // eslint-disable-next-line @grafana/no-border-radius-literal - borderRadius: 0, padding: theme.spacing(1), + + '&:hover': { + backgroundColor: theme.colors.action.hover, + }, }), heading: css({ width: '100%', @@ -98,7 +97,19 @@ function getStyles(theme: GrafanaTheme2, builtIn = false) { color: theme.colors.border.weak, }), selected: css({ - backgroundColor: theme.colors.background.secondary, + background: theme.colors.action.selected, + + '&::before': { + backgroundImage: theme.colors.gradients.brandVertical, + borderRadius: theme.shape.radius.default, + content: '" "', + display: 'block', + height: '100%', + position: 'absolute', + transform: 'translateX(-50%)', + width: theme.spacing(0.5), + left: 0, + }, }), meta: css({ display: 'block', diff --git a/public/app/features/datasources/components/picker/DataSourceList.tsx b/public/app/features/datasources/components/picker/DataSourceList.tsx index 11fc0bc90e0..93d8e15f546 100644 --- a/public/app/features/datasources/components/picker/DataSourceList.tsx +++ b/public/app/features/datasources/components/picker/DataSourceList.tsx @@ -141,6 +141,7 @@ function getStyles(theme: GrafanaTheme2, selectedItemCssSelector: string) { container: css({ display: 'flex', flexDirection: 'column', + padding: theme.spacing(0.5), [`${selectedItemCssSelector}`]: { backgroundColor: theme.colors.background.secondary, }, diff --git a/public/app/features/datasources/components/picker/DataSourcePicker.tsx b/public/app/features/datasources/components/picker/DataSourcePicker.tsx index 47a0366230e..88aebfbafe3 100644 --- a/public/app/features/datasources/components/picker/DataSourcePicker.tsx +++ b/public/app/features/datasources/components/picker/DataSourcePicker.tsx @@ -380,8 +380,10 @@ function getStylesPickerContent(theme: GrafanaTheme2) { container: css({ display: 'flex', flexDirection: 'column', - background: theme.colors.background.primary, + background: theme.colors.background.elevated, + borderRadius: theme.shape.radius.default, boxShadow: theme.shadows.z3, + overflow: 'hidden', }), picker: css({ background: theme.colors.background.secondary, From d3de9dbce659d49c358a05515294de10f8fe353d Mon Sep 17 00:00:00 2001 From: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:29:47 -0500 Subject: [PATCH 532/894] ExploreMetrics: Fix escaping of regex metacharacters in label filters (#100513) * fix: don't over-escape label values * test: handling of regex metacharacters in filters --- public/app/features/trails/DataTrail.test.tsx | 70 ++++++++++--------- public/app/features/trails/DataTrail.tsx | 5 ++ 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/public/app/features/trails/DataTrail.test.tsx b/public/app/features/trails/DataTrail.test.tsx index 4d0d0af86c4..ecb928383d2 100644 --- a/public/app/features/trails/DataTrail.test.tsx +++ b/public/app/features/trails/DataTrail.test.tsx @@ -44,14 +44,6 @@ describe('DataTrail', () => { let trail: DataTrail; const preTrailUrl = '/'; - function getFilterVar() { - const variable = sceneGraph.lookupVariable(VAR_FILTERS, trail); - if (variable instanceof AdHocFiltersVariable) { - return variable; - } - throw new Error('getFilterVar failed'); - } - function getStepFilterVar(step: number) { const variable = trail.state.history.state.steps[step].trailState.$variables?.getByName(VAR_FILTERS); if (variable instanceof AdHocFiltersVariable) { @@ -226,12 +218,12 @@ describe('DataTrail', () => { }); it('Should have default empty filter', () => { - expect(getFilterVar().state.filters.length).toBe(0); + expect(getFilterVar(trail).state.filters.length).toBe(0); }); describe('And when changing the filter to zone=a', () => { beforeEach(() => { - getFilterVar().setState({ filters: [{ key: 'zone', operator: '=', value: 'a' }] }); + getFilterVar(trail).setState({ filters: [{ key: 'zone', operator: '=', value: 'a' }] }); }); it('should add history step', () => { @@ -247,8 +239,8 @@ describe('DataTrail', () => { }); it('Should have filter be updated to "zone=a"', () => { - expect(getFilterVar().state.filters[0].key).toBe('zone'); - expect(getFilterVar().state.filters[0].value).toBe('a'); + expect(getFilterVar(trail).state.filters[0].key).toBe('zone'); + expect(getFilterVar(trail).state.filters[0].value).toBe('a'); }); it('Previous history step should have empty filter', () => { @@ -274,12 +266,12 @@ describe('DataTrail', () => { }); it('Should have filters set back to empty', () => { - expect(getFilterVar().state.filters.length).toBe(0); + expect(getFilterVar(trail).state.filters.length).toBe(0); }); describe('And when changing the filter to zone=b', () => { beforeEach(() => { - getFilterVar().setState({ filters: [{ key: 'zone', operator: '=', value: 'b' }] }); + getFilterVar(trail).setState({ filters: [{ key: 'zone', operator: '=', value: 'b' }] }); }); it('should add history step', () => { @@ -295,8 +287,8 @@ describe('DataTrail', () => { }); it('Should have filter be updated to "zone=b"', () => { - expect(getFilterVar().state.filters[0].key).toBe('zone'); - expect(getFilterVar().state.filters[0].value).toBe('b'); + expect(getFilterVar(trail).state.filters[0].key).toBe('zone'); + expect(getFilterVar(trail).state.filters[0].value).toBe('b'); }); it('Parent history step 1 should still have empty filter', () => { @@ -327,7 +319,7 @@ describe('DataTrail', () => { }); it('Should have filters set back to empty', () => { - expect(getFilterVar().state.filters.length).toBe(0); + expect(getFilterVar(trail).state.filters.length).toBe(0); }); it('History step 1 should still have empty filter', () => { @@ -417,12 +409,12 @@ describe('DataTrail', () => { describe('And filter is added zone=a', () => { beforeEach(() => { - getFilterVar().setState({ filters: [{ key: 'zone', operator: '=', value: 'a' }] }); + getFilterVar(trail).setState({ filters: [{ key: 'zone', operator: '=', value: 'a' }] }); }); it('Filter of trail should be zone=a', () => { - expect(getFilterVar().state.filters[0].key).toBe('zone'); - expect(getFilterVar().state.filters[0].value).toBe('a'); + expect(getFilterVar(trail).state.filters[0].key).toBe('zone'); + expect(getFilterVar(trail).state.filters[0].value).toBe('a'); }); it('Filter of step 2 should be zone=a', () => { @@ -440,7 +432,7 @@ describe('DataTrail', () => { }); it('Filter of trail should be empty', () => { - expect(getFilterVar().state.filters.length).toBe(0); + expect(getFilterVar(trail).state.filters.length).toBe(0); }); }); }); @@ -518,14 +510,6 @@ describe('DataTrail', () => { throw new Error('getOtelGroupLeftVar failed'); } - function getFilterVar() { - const variable = sceneGraph.lookupVariable(VAR_FILTERS, trail); - if (variable instanceof AdHocFiltersVariable) { - return variable; - } - throw new Error('getFilterVar failed'); - } - beforeEach(() => { trail = new DataTrail({ nonPromotedOtelResources, @@ -540,7 +524,7 @@ describe('DataTrail', () => { it('clicking start button should start with OTel off and showing var filters', () => { trail.setState({ startButtonClicked: true }); const otelResourcesHide = getOtelResourcesVar(trail).state.hide; - const varFiltersHide = getFilterVar().state.hide; + const varFiltersHide = getFilterVar(trail).state.hide; expect(otelResourcesHide).toBe(VariableHide.hideVariable); expect(varFiltersHide).toBe(VariableHide.hideLabel); }); @@ -557,7 +541,7 @@ describe('DataTrail', () => { describe('resetting the OTel experience', () => { it('should display with hideLabel var filters and hide VAR_OTEL_AND_METRIC_FILTERS when resetting otel experience', () => { trail.resetOtelExperience(); - expect(getFilterVar().state.hide).toBe(VariableHide.hideLabel); + expect(getFilterVar(trail).state.hide).toBe(VariableHide.hideLabel); expect(getOtelAndMetricsVar(trail).state.hide).toBe(VariableHide.hideVariable); }); @@ -589,7 +573,7 @@ describe('DataTrail', () => { it('should automatically update the var filters when a promoted resource has been selected from VAR_OTEL_AND_METRICS', () => { getOtelAndMetricsVar(trail).setState({ filters: [{ key: 'promoted', operator: '=', value: 'resource' }] }); - const varFilters = getFilterVar().state.filters[0]; + const varFilters = getFilterVar(trail).state.filters[0]; expect(varFilters.key).toBe('promoted'); expect(varFilters.value).toBe('resource'); }); @@ -600,4 +584,26 @@ describe('DataTrail', () => { }); }); }); + + describe('Label filters', () => { + let trail: DataTrail; + + beforeEach(() => { + trail = new DataTrail({}); + }); + + it('should not escape regex metacharacters in label values', () => { + const filterVar = getFilterVar(trail); + filterVar.setState({ filters: [{ key: 'app', operator: '=~', value: '.*end' }] }); // matches app=frontend, app=backend, etc. + expect(filterVar.getValue()).toBe('app=~".*end"'); + }); + }); }); + +function getFilterVar(trail: DataTrail) { + const variable = sceneGraph.lookupVariable(VAR_FILTERS, trail); + if (variable instanceof AdHocFiltersVariable) { + return variable; + } + throw new Error('getFilterVar failed'); +} diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index fa85dd32773..32d353a9e9b 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -669,6 +669,11 @@ function getVariableSet( // since we only support prometheus datasources, this is always true supportsMultiValueOperators: true, allowCustomValue: true, + expressionBuilder: (filters: AdHocVariableFilter[]) => { + return [...getBaseFiltersForMetric(metric), ...filters] + .map((filter) => `${filter.key}${filter.operator}"${filter.value}"`) + .join(','); + }, }), ...getVariablesWithOtelJoinQueryConstant(otelJoinQuery ?? ''), new ConstantVariable({ From 21861867c13fcb0bdb4a41fbf63161f1b288af09 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:51:39 +0100 Subject: [PATCH 533/894] Combobox: Fix broken styles for options (#100536) Add basicOption styles to Combobox --- packages/grafana-ui/src/components/Combobox/Combobox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index 5d6c5320402..62f231975e3 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -416,6 +416,7 @@ export const Combobox = (props: ComboboxProps) => key={`${items[virtualRow.index].value}-${virtualRow.index}`} data-index={virtualRow.index} className={cx( + styles.optionBasic, styles.option, selectedItem && items[virtualRow.index].value === selectedItem.value && styles.optionSelected, highlightedIndex === virtualRow.index && styles.optionFocused From e2a101cde3bd03c3b9ad4377a802de277770b920 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Wed, 12 Feb 2025 10:19:55 -0700 Subject: [PATCH 534/894] Dashboard history: Track version created timestamp in restore (#100451) --- .../settings/version-history/VersionHistoryTable.test.tsx | 1 + .../settings/version-history/VersionHistoryTable.tsx | 1 + public/app/features/dashboard-scene/utils/interactions.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.test.tsx b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.test.tsx index 63339065a30..0a15aff1dd5 100644 --- a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.test.tsx +++ b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.test.tsx @@ -52,6 +52,7 @@ describe('VersionHistoryTable', () => { version: mockVersions[1].version, index: 1, confirm: false, + timestamp: mockVersions[1].created, }); }); }); diff --git a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx index 3713f235106..48fc15963b8 100644 --- a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx +++ b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx @@ -70,6 +70,7 @@ export const VersionHistoryTable = ({ versions, canCompare, onCheck, onRestore } version: version.version, index: idx, confirm: false, + timestamp: version.created, }); }} > diff --git a/public/app/features/dashboard-scene/utils/interactions.ts b/public/app/features/dashboard-scene/utils/interactions.ts index ad1b3fbea0f..b52c09e190f 100644 --- a/public/app/features/dashboard-scene/utils/interactions.ts +++ b/public/app/features/dashboard-scene/utils/interactions.ts @@ -118,7 +118,7 @@ export const DashboardInteractions = { }, // Dashboards versions interactions - versionRestoreClicked: (properties: { version: number; index?: number; confirm: boolean }) => { + versionRestoreClicked: (properties: { version: number; index?: number; confirm: boolean; timestamp?: Date }) => { reportDashboardInteraction('version_restore_clicked', properties); }, showMoreVersionsClicked: () => { From 3cc4320aa9e209c191366d63199d38de5ffce451 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 12 Feb 2025 18:38:48 +0100 Subject: [PATCH 535/894] Alerting: Add rule conversion package (#100224) --- pkg/services/ngalert/models/alert_rule.go | 12 +- .../ngalert/models/alert_rule_test.go | 35 ++- pkg/services/ngalert/prom/convert.go | 220 ++++++++++++++++++ pkg/services/ngalert/prom/convert_test.go | 192 +++++++++++++++ pkg/services/ngalert/prom/models.go | 25 ++ pkg/services/ngalert/prom/models_test.go | 106 +++++++++ 6 files changed, 584 insertions(+), 6 deletions(-) create mode 100644 pkg/services/ngalert/prom/convert.go create mode 100644 pkg/services/ngalert/prom/convert_test.go create mode 100644 pkg/services/ngalert/prom/models.go create mode 100644 pkg/services/ngalert/prom/models_test.go diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index e11c5eb1add..66be69d0079 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -295,7 +295,8 @@ type AlertRule struct { } type AlertRuleMetadata struct { - EditorSettings EditorSettings `json:"editor_settings"` + EditorSettings EditorSettings `json:"editor_settings"` + PrometheusStyleRule *PrometheusStyleRule `json:"prometheus_style_rule,omitempty"` } type EditorSettings struct { @@ -303,6 +304,10 @@ type EditorSettings struct { SimplifiedNotificationsSection bool `json:"simplified_notifications_section"` } +type PrometheusStyleRule struct { + OriginalRuleDefinition string `json:"original_rule_definition,omitempty"` +} + // Namespaced describes a class of resources that are stored in a specific namespace. type Namespaced interface { GetNamespaceUID() string @@ -748,6 +753,11 @@ func (alertRule *AlertRule) Copy() *AlertRule { } } + if alertRule.Metadata.PrometheusStyleRule != nil { + prometheusStyleRule := *alertRule.Metadata.PrometheusStyleRule + result.Metadata.PrometheusStyleRule = &prometheusStyleRule + } + for _, s := range alertRule.NotificationSettings { result.NotificationSettings = append(result.NotificationSettings, CopyNotificationSettings(s)) } diff --git a/pkg/services/ngalert/models/alert_rule_test.go b/pkg/services/ngalert/models/alert_rule_test.go index 6594732c04b..9f6c8b52b96 100644 --- a/pkg/services/ngalert/models/alert_rule_test.go +++ b/pkg/services/ngalert/models/alert_rule_test.go @@ -841,7 +841,7 @@ func TestDiff(t *testing.T) { } }) - t.Run("should detect changes in Metadata", func(t *testing.T) { + t.Run("should detect changes in Metadata.EditorSettings", func(t *testing.T) { rule1 := RuleGen.With(RuleGen.WithMetadata(AlertRuleMetadata{EditorSettings: EditorSettings{ SimplifiedQueryAndExpressionsSection: false, SimplifiedNotificationsSection: false, @@ -858,6 +858,21 @@ func TestDiff(t *testing.T) { "Metadata.EditorSettings.SimplifiedNotificationsSection", }, diff.Paths()) }) + + t.Run("should detect changes in Metadata.PrometheusStyleRule", func(t *testing.T) { + rule1 := RuleGen.With(RuleGen.WithMetadata(AlertRuleMetadata{PrometheusStyleRule: &PrometheusStyleRule{ + OriginalRuleDefinition: "data", + }})).GenerateRef() + + rule2 := CopyRule(rule1, RuleGen.WithMetadata(AlertRuleMetadata{PrometheusStyleRule: &PrometheusStyleRule{ + OriginalRuleDefinition: "updated data", + }})) + + diff := rule1.Diff(rule2) + assert.ElementsMatch(t, []string{ + "Metadata.PrometheusStyleRule.OriginalRuleDefinition", + }, diff.Paths()) + }) } func TestSortByGroupIndex(t *testing.T) { @@ -940,11 +955,21 @@ func TestAlertRuleGetKeyWithGroup(t *testing.T) { } func TestAlertRuleCopy(t *testing.T) { - for i := 0; i < 100; i++ { - rule := RuleGen.GenerateRef() + t.Run("should return a copy of the rule", func(t *testing.T) { + for i := 0; i < 100; i++ { + rule := RuleGen.GenerateRef() + copied := rule.Copy() + require.Empty(t, rule.Diff(copied)) + } + }) + + t.Run("should create a copy of the prometheus rule definition from the metadata", func(t *testing.T) { + rule := RuleGen.With(RuleGen.WithMetadata(AlertRuleMetadata{PrometheusStyleRule: &PrometheusStyleRule{ + OriginalRuleDefinition: "data", + }})).GenerateRef() copied := rule.Copy() - require.Empty(t, rule.Diff(copied)) - } + require.NotSame(t, rule.Metadata.PrometheusStyleRule, copied.Metadata.PrometheusStyleRule) + }) } // This test makes sure the default generator diff --git a/pkg/services/ngalert/prom/convert.go b/pkg/services/ngalert/prom/convert.go new file mode 100644 index 00000000000..d14a37aa9f5 --- /dev/null +++ b/pkg/services/ngalert/prom/convert.go @@ -0,0 +1,220 @@ +package prom + +import ( + "encoding/json" + "fmt" + "time" + + "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +type Config struct { + DatasourceUID string + DatasourceType string + FromTimeRange *time.Duration + EvaluationOffset *time.Duration + ExecErrState models.ExecutionErrorState + NoDataState models.NoDataState + RecordingRules RulesConfig + AlertRules RulesConfig +} + +type RulesConfig struct { + IsPaused bool +} + +var ( + defaultTimeRange = 600 * time.Second + defaultEvaluationOffset = 0 * time.Minute + + defaultConfig = Config{ + FromTimeRange: &defaultTimeRange, + EvaluationOffset: &defaultEvaluationOffset, + ExecErrState: models.ErrorErrState, + NoDataState: models.NoData, + } +) + +type Converter struct { + cfg Config +} + +func NewConverter(cfg Config) (*Converter, error) { + if cfg.DatasourceUID == "" { + return nil, fmt.Errorf("datasource UID is required") + } + if cfg.DatasourceType == "" { + return nil, fmt.Errorf("datasource type is required") + } + if cfg.FromTimeRange == nil { + cfg.FromTimeRange = defaultConfig.FromTimeRange + } + if cfg.EvaluationOffset == nil { + cfg.EvaluationOffset = defaultConfig.EvaluationOffset + } + if cfg.ExecErrState == "" { + cfg.ExecErrState = defaultConfig.ExecErrState + } + if cfg.NoDataState == "" { + cfg.NoDataState = defaultConfig.NoDataState + } + + if cfg.DatasourceType != datasources.DS_PROMETHEUS && cfg.DatasourceType != datasources.DS_LOKI { + return nil, fmt.Errorf("invalid datasource type: %s", cfg.DatasourceType) + } + + return &Converter{ + cfg: cfg, + }, nil +} + +// PrometheusRulesToGrafana converts a Prometheus rule group into Grafana Alerting rule group. +func (p *Converter) PrometheusRulesToGrafana(orgID int64, namespaceUID string, group PrometheusRuleGroup) (*models.AlertRuleGroup, error) { + for _, rule := range group.Rules { + err := validatePrometheusRule(rule) + if err != nil { + return nil, fmt.Errorf("invalid Prometheus rule '%s': %w", rule.Alert, err) + } + } + + grafanaGroup, err := p.convertRuleGroup(orgID, namespaceUID, group) + if err != nil { + return nil, fmt.Errorf("failed to convert rule group '%s': %w", group.Name, err) + } + + return grafanaGroup, nil +} + +func validatePrometheusRule(rule PrometheusRule) error { + if rule.KeepFiringFor != nil { + return fmt.Errorf("keep_firing_for is not supported") + } + + return nil +} + +func (p *Converter) convertRuleGroup(orgID int64, namespaceUID string, promGroup PrometheusRuleGroup) (*models.AlertRuleGroup, error) { + uniqueNames := map[string]int{} + rules := make([]models.AlertRule, 0, len(promGroup.Rules)) + interval := time.Duration(promGroup.Interval) + for i, rule := range promGroup.Rules { + gr, err := p.convertRule(orgID, namespaceUID, promGroup.Name, rule) + if err != nil { + return nil, fmt.Errorf("failed to convert Prometheus rule '%s' to Grafana rule: %w", rule.Alert, err) + } + gr.RuleGroupIndex = i + 1 + gr.IntervalSeconds = int64(interval.Seconds()) + + // Check rule title uniqueness within the group. + uniqueNames[gr.Title]++ + if val := uniqueNames[gr.Title]; val > 1 { + gr.Title = fmt.Sprintf("%s (%d)", gr.Title, val) + } + + rules = append(rules, gr) + } + + result := &models.AlertRuleGroup{ + FolderUID: namespaceUID, + Interval: int64(interval.Seconds()), + Rules: rules, + Title: promGroup.Name, + } + + return result, nil +} + +func (p *Converter) convertRule(orgID int64, namespaceUID, group string, rule PrometheusRule) (models.AlertRule, error) { + var forInterval time.Duration + if rule.For != nil { + forInterval = time.Duration(*rule.For) + } + + queryNode, err := createAlertQueryNode(p.cfg.DatasourceUID, p.cfg.DatasourceType, rule.Expr, *p.cfg.FromTimeRange, *p.cfg.EvaluationOffset) + if err != nil { + return models.AlertRule{}, err + } + + var title string + if rule.Record != "" { + title = rule.Record + } else { + title = rule.Alert + } + + labels := make(map[string]string, len(rule.Labels)+1) + for k, v := range rule.Labels { + labels[k] = v + } + + originalRuleDefinition, err := yaml.Marshal(rule) + if err != nil { + return models.AlertRule{}, fmt.Errorf("failed to marshal original rule definition: %w", err) + } + + result := models.AlertRule{ + OrgID: orgID, + NamespaceUID: namespaceUID, + Title: title, + Data: []models.AlertQuery{queryNode}, + Condition: "A", + NoDataState: p.cfg.NoDataState, + ExecErrState: p.cfg.ExecErrState, + Annotations: rule.Annotations, + Labels: labels, + For: forInterval, + RuleGroup: group, + Metadata: models.AlertRuleMetadata{ + PrometheusStyleRule: &models.PrometheusStyleRule{ + OriginalRuleDefinition: string(originalRuleDefinition), + }, + }, + } + + if rule.Record != "" { + result.Record = &models.Record{ + From: "A", + Metric: rule.Record, + } + result.IsPaused = p.cfg.RecordingRules.IsPaused + } else { + result.IsPaused = p.cfg.AlertRules.IsPaused + } + + return result, nil +} + +func createAlertQueryNode(datasourceUID, datasourceType, expr string, fromTimeRange, evaluationOffset time.Duration) (models.AlertQuery, error) { + modelData := map[string]interface{}{ + "datasource": map[string]interface{}{ + "type": datasourceType, + "uid": datasourceUID, + }, + "expr": expr, + "instant": true, + "range": false, + "refId": "A", + } + + if datasourceType == datasources.DS_LOKI { + modelData["queryType"] = "instant" + } + + modelJSON, err := json.Marshal(modelData) + if err != nil { + return models.AlertQuery{}, err + } + + return models.AlertQuery{ + DatasourceUID: datasourceUID, + Model: modelJSON, + RefID: "A", + RelativeTimeRange: models.RelativeTimeRange{ + From: models.Duration(fromTimeRange + evaluationOffset), + To: models.Duration(0 + evaluationOffset), + }, + }, nil +} diff --git a/pkg/services/ngalert/prom/convert_test.go b/pkg/services/ngalert/prom/convert_test.go new file mode 100644 index 00000000000..f175686fd3d --- /dev/null +++ b/pkg/services/ngalert/prom/convert_test.go @@ -0,0 +1,192 @@ +package prom + +import ( + "testing" + "time" + + prommodel "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +func TestPrometheusRulesToGrafana(t *testing.T) { + fiveMin := prommodel.Duration(5 * time.Minute) + + testCases := []struct { + name string + orgID int64 + namespace string + promGroup PrometheusRuleGroup + config Config + expectError bool + }{ + { + name: "valid rule group", + orgID: 1, + namespace: "some-namespace-uid", + promGroup: PrometheusRuleGroup{ + Name: "test-group-1", + Interval: prommodel.Duration(10 * time.Second), + Rules: []PrometheusRule{ + { + Alert: "alert-1", + Expr: "cpu_usage > 80", + For: &fiveMin, + Labels: map[string]string{ + "severity": "critical", + }, + Annotations: map[string]string{ + "summary": "CPU usage is critical", + }, + }, + }, + }, + expectError: false, + }, + { + name: "rules with keep_firing_for are not supported", + orgID: 1, + namespace: "namespaceUID", + promGroup: PrometheusRuleGroup{ + Name: "test-group-1", + Interval: prommodel.Duration(1 * time.Minute), + Rules: []PrometheusRule{ + { + Alert: "alert-1", + Expr: "up == 0", + KeepFiringFor: &fiveMin, + }, + }, + }, + expectError: true, + }, + { + name: "rule with empty interval", + orgID: 1, + namespace: "namespaceUID", + promGroup: PrometheusRuleGroup{ + Name: "test-group-1", + Rules: []PrometheusRule{ + { + Alert: "alert-1", + Expr: "up == 0", + }, + }, + }, + expectError: false, + }, + { + name: "recording rule", + orgID: 1, + namespace: "namespaceUID", + promGroup: PrometheusRuleGroup{ + Name: "test-group-1", + Rules: []PrometheusRule{ + { + Record: "some_metric", + Expr: "sum(rate(http_requests_total[5m]))", + }, + }, + }, + expectError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.config.DatasourceUID = "datasource-uid" + tc.config.DatasourceType = datasources.DS_PROMETHEUS + converter, err := NewConverter(tc.config) + require.NoError(t, err) + + grafanaGroup, err := converter.PrometheusRulesToGrafana(tc.orgID, tc.namespace, tc.promGroup) + + if tc.expectError { + require.Error(t, err, tc.name) + return + } + require.NoError(t, err, tc.name) + + require.Equal(t, tc.promGroup.Name, grafanaGroup.Title, tc.name) + expectedInterval := int64(time.Duration(tc.promGroup.Interval).Seconds()) + require.Equal(t, expectedInterval, grafanaGroup.Interval, tc.name) + + require.Equal(t, len(tc.promGroup.Rules), len(grafanaGroup.Rules), tc.name) + + for j, promRule := range tc.promGroup.Rules { + grafanaRule := grafanaGroup.Rules[j] + + if promRule.Record != "" { + require.Equal(t, promRule.Record, grafanaRule.Title) + } else { + require.Equal(t, promRule.Alert, grafanaRule.Title) + } + + var expectedFor time.Duration + if promRule.For != nil { + expectedFor = time.Duration(*promRule.For) + } + require.Equal(t, expectedFor, grafanaRule.For, tc.name) + + expectedLabels := make(map[string]string, len(promRule.Labels)+1) + for k, v := range promRule.Labels { + expectedLabels[k] = v + } + + require.Equal(t, expectedLabels, grafanaRule.Labels, tc.name) + require.Equal(t, promRule.Annotations, grafanaRule.Annotations, tc.name) + require.Equal(t, models.Duration(0*time.Minute), grafanaRule.Data[0].RelativeTimeRange.To) + require.Equal(t, models.Duration(10*time.Minute), grafanaRule.Data[0].RelativeTimeRange.From) + + originalRuleDefinition, err := yaml.Marshal(promRule) + require.NoError(t, err) + require.Equal(t, string(originalRuleDefinition), grafanaRule.Metadata.PrometheusStyleRule.OriginalRuleDefinition) + } + }) + } +} + +func TestPrometheusRulesToGrafanaWithDuplicateRuleNames(t *testing.T) { + cfg := Config{ + DatasourceUID: "datasource-uid", + DatasourceType: datasources.DS_PROMETHEUS, + } + converter, err := NewConverter(cfg) + require.NoError(t, err) + + promGroup := PrometheusRuleGroup{ + Name: "test-group-1", + Interval: prommodel.Duration(10 * time.Second), + Rules: []PrometheusRule{ + { + Alert: "alert", + Expr: "up", + }, + { + Alert: "alert", + Expr: "up", + }, + { + Alert: "another alert", + Expr: "up", + }, + { + Alert: "alert", + Expr: "up", + }, + }, + } + + group, err := converter.PrometheusRulesToGrafana(1, "namespaceUID", promGroup) + require.NoError(t, err) + + 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) +} diff --git a/pkg/services/ngalert/prom/models.go b/pkg/services/ngalert/prom/models.go new file mode 100644 index 00000000000..f7e8bbfc95b --- /dev/null +++ b/pkg/services/ngalert/prom/models.go @@ -0,0 +1,25 @@ +package prom + +import ( + prommodel "github.com/prometheus/common/model" +) + +type PrometheusRulesFile struct { + Groups []PrometheusRuleGroup `yaml:"groups"` +} + +type PrometheusRuleGroup struct { + Name string `yaml:"name"` + Interval prommodel.Duration `yaml:"interval"` + Rules []PrometheusRule `yaml:"rules"` +} + +type PrometheusRule struct { + Alert string `yaml:"alert,omitempty"` + Expr string `yaml:"expr,omitempty"` + For *prommodel.Duration `yaml:"for,omitempty"` + KeepFiringFor *prommodel.Duration `yaml:"keep_firing_for,omitempty"` + Labels map[string]string `yaml:"labels,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty"` + Record string `yaml:"record,omitempty"` +} diff --git a/pkg/services/ngalert/prom/models_test.go b/pkg/services/ngalert/prom/models_test.go new file mode 100644 index 00000000000..fbf7d65a847 --- /dev/null +++ b/pkg/services/ngalert/prom/models_test.go @@ -0,0 +1,106 @@ +package prom + +import ( + "testing" + "time" + + prommodel "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestPrometheusRulesFileYAML(t *testing.T) { + interval := prommodel.Duration(5 * time.Minute) + alertFor := prommodel.Duration(10 * time.Minute) + keepFiring := prommodel.Duration(15 * time.Minute) + + tests := []struct { + name string + input PrometheusRulesFile + expectedYAML string + }{ + { + name: "simple alert rule and a recording rule", + input: PrometheusRulesFile{ + Groups: []PrometheusRuleGroup{ + { + Name: "test_group", + Interval: interval, + Rules: []PrometheusRule{ + { + Alert: "alert-1", + Expr: "vector(0) > 90", + For: &alertFor, + KeepFiringFor: &keepFiring, + Labels: map[string]string{ + "team": "alerting", + }, + Annotations: map[string]string{ + "summary": "some summary", + "description": "some description", + }, + }, + { + Record: "vector(1)", + }, + }, + }, + }, + }, + expectedYAML: ` +groups: + - name: test_group + interval: 5m + rules: + - alert: alert-1 + expr: vector(0) > 90 + for: 10m + keep_firing_for: 15m + labels: + team: alerting + annotations: + description: some description + summary: some summary + - record: vector(1) +`, + }, + { + name: "empty rules file", + input: PrometheusRulesFile{ + Groups: []PrometheusRuleGroup{}, + }, + expectedYAML: `groups: []`, + }, + { + name: "empty group", + input: PrometheusRulesFile{ + Groups: []PrometheusRuleGroup{ + { + Name: "empty_group", + Interval: interval, + Rules: []PrometheusRule{}, + }, + }, + }, + expectedYAML: ` +groups: + - name: empty_group + interval: 5m + rules: []`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + yamlData, err := yaml.Marshal(tt.input) + require.NoError(t, err, "Failed to marshal to YAML") + require.YAMLEq(t, tt.expectedYAML, string(yamlData)) + + var parsed PrometheusRulesFile + err = yaml.Unmarshal(yamlData, &parsed) + require.NoError(t, err, "Failed to unmarshal from YAML") + + require.Equal(t, tt.input, parsed) + }) + } +} From c556f2062795f1f5ac4913a4996a079e9e4bbe82 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 12 Feb 2025 19:24:22 +0100 Subject: [PATCH 536/894] Alerting: Fix default max_attempts value in the docs (#100497) --- docs/sources/setup-grafana/configure-grafana/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 4805b42a1a7..e0b265a30d1 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1821,7 +1821,7 @@ The timeout string is a possibly signed sequence of decimal numbers, followed by #### `max_attempts` -Sets a maximum number of times Grafana attempts to evaluate an alert rule before giving up on that evaluation. The default value is `1`. +Sets a maximum number of times Grafana attempts to evaluate an alert rule before giving up on that evaluation. The default value is `3`. #### `min_interval` From 950726a3c5a3b151e61a2b5ddb7eab9e6adc39d0 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Wed, 12 Feb 2025 19:52:39 +0100 Subject: [PATCH 537/894] Dashboard V0->V1 Migration: Schema migration v38 (#99778) --- .../migration/schemaversion/migrations.go | 3 +- .../dashboard/migration/schemaversion/v38.go | 122 ++++++ .../migration/schemaversion/v38_test.go | 251 ++++++++++++ .../37.timeseries_table_display_mode.json | 360 +++++++++++++++++ .../37.timeseries_table_display_mode.38.json | 375 ++++++++++++++++++ .../37.timeseries_table_display_mode.39.json | 375 ++++++++++++++++++ .../37.timeseries_table_display_mode.40.json | 375 ++++++++++++++++++ .../38.transform_timeseries_table.38.json | 153 +++++++ 8 files changed, 2013 insertions(+), 1 deletion(-) create mode 100644 pkg/apis/dashboard/migration/schemaversion/v38.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/v38_test.go create mode 100644 pkg/apis/dashboard/migration/testdata/input/37.timeseries_table_display_mode.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.38.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.39.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.40.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.38.json diff --git a/pkg/apis/dashboard/migration/schemaversion/migrations.go b/pkg/apis/dashboard/migration/schemaversion/migrations.go index f689894cf34..5875261aaeb 100644 --- a/pkg/apis/dashboard/migration/schemaversion/migrations.go +++ b/pkg/apis/dashboard/migration/schemaversion/migrations.go @@ -5,11 +5,12 @@ import "strconv" type SchemaVersionMigrationFunc func(map[string]interface{}) error const ( - MINIUM_VERSION = 38 + MINIUM_VERSION = 37 LATEST_VERSION = 40 ) var Migrations = map[int]SchemaVersionMigrationFunc{ + 38: V38, 39: V39, 40: V40, } diff --git a/pkg/apis/dashboard/migration/schemaversion/v38.go b/pkg/apis/dashboard/migration/schemaversion/v38.go new file mode 100644 index 00000000000..a750ac4223f --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/v38.go @@ -0,0 +1,122 @@ +package schemaversion + +// V38 updates the configuration of the table panel to use the new cellOptions format +// and updates the overrides to use the new cellOptions format +func V38(dashboard map[string]interface{}) error { + dashboard["schemaVersion"] = int(38) + + panels, ok := dashboard["panels"].([]interface{}) + if !ok { + return nil + } + + for _, panel := range panels { + p, ok := panel.(map[string]interface{}) + if !ok { + continue + } + + // Only process table panels + if p["type"] != "table" { + continue + } + + fieldConfig, ok := p["fieldConfig"].(map[string]interface{}) + if !ok { + continue + } + + defaults, ok := fieldConfig["defaults"].(map[string]interface{}) + if !ok { + continue + } + + custom, ok := defaults["custom"].(map[string]interface{}) + if !ok { + continue + } + + // Migrate displayMode to cellOptions + if displayMode, exists := custom["displayMode"]; exists { + if displayModeStr, ok := displayMode.(string); ok { + custom["cellOptions"] = migrateTableDisplayModeToCellOptions(displayModeStr) + } + // Delete the legacy field + delete(custom, "displayMode") + } + + // Update any overrides referencing the cell display mode + migrateOverrides(fieldConfig) + } + + return nil +} + +// migrateOverrides updates the overrides configuration to use the new cellOptions format +func migrateOverrides(fieldConfig map[string]interface{}) { + overrides, ok := fieldConfig["overrides"].([]interface{}) + if !ok { + return + } + + for _, override := range overrides { + o, ok := override.(map[string]interface{}) + if !ok { + continue + } + + properties, ok := o["properties"].([]interface{}) + if !ok { + continue + } + + for _, property := range properties { + prop, ok := property.(map[string]interface{}) + if !ok { + continue + } + + // Update the id to cellOptions + if prop["id"] == "custom.displayMode" { + prop["id"] = "custom.cellOptions" + if value, ok := prop["value"]; ok { + if valueStr, ok := value.(string); ok { + prop["value"] = migrateTableDisplayModeToCellOptions(valueStr) + } + } + } + } + } +} + +// migrateTableDisplayModeToCellOptions converts the old displayMode string to the new cellOptions format +func migrateTableDisplayModeToCellOptions(displayMode string) map[string]interface{} { + switch displayMode { + case "basic", "gradient-gauge", "lcd-gauge": + gaugeMode := "basic" + if displayMode == "gradient-gauge" { + gaugeMode = "gradient" + } else if displayMode == "lcd-gauge" { + gaugeMode = "lcd" + } + return map[string]interface{}{ + "type": "gauge", + "mode": gaugeMode, + } + + case "color-background", "color-background-solid": + mode := "basic" + if displayMode == "color-background" { + mode = "gradient" + } + return map[string]interface{}{ + "type": "color-background", + "mode": mode, + } + + default: + return map[string]interface{}{ + "type": displayMode, + } + } +} diff --git a/pkg/apis/dashboard/migration/schemaversion/v38_test.go b/pkg/apis/dashboard/migration/schemaversion/v38_test.go new file mode 100644 index 00000000000..f4bbb8c9cb6 --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/v38_test.go @@ -0,0 +1,251 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" +) + +func TestV38(t *testing.T) { + tests := []migrationTestCase{ + { + name: "no table panels", + input: map[string]interface{}{ + "schemaVersion": 37, + "title": "Test Dashboard", + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Panel 1", + }, + }, + }, + expected: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 38, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Panel 1", + }, + }, + }, + }, + { + name: "table panel with basic gauge displayMode", + input: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "displayMode": "basic", + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 38, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "cellOptions": map[string]interface{}{ + "type": "gauge", + "mode": "basic", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "table panel with gradient-gauge displayMode", + input: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "displayMode": "gradient-gauge", + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 38, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "cellOptions": map[string]interface{}{ + "type": "gauge", + "mode": "gradient", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "table panel with lcd-gauge displayMode", + input: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "displayMode": "lcd-gauge", + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 38, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "cellOptions": map[string]interface{}{ + "type": "gauge", + "mode": "lcd", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "table panel with color-background displayMode", + input: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "displayMode": "color-background", + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 38, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "cellOptions": map[string]interface{}{ + "type": "color-background", + "mode": "gradient", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "table panel with color-background-solid displayMode", + input: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "displayMode": "color-background-solid", + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 38, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "cellOptions": map[string]interface{}{ + "type": "color-background", + "mode": "basic", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "table panel with default displayMode", + input: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "displayMode": "some-other-mode", + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 38, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "cellOptions": map[string]interface{}{ + "type": "some-other-mode", + }, + }, + }, + }, + }, + }, + }, + }, + } + runMigrationTests(t, tests, schemaversion.V38) +} diff --git a/pkg/apis/dashboard/migration/testdata/input/37.timeseries_table_display_mode.json b/pkg/apis/dashboard/migration/testdata/input/37.timeseries_table_display_mode.json new file mode 100644 index 00000000000..e800450fd51 --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/input/37.timeseries_table_display_mode.json @@ -0,0 +1,360 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "basic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Basic Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "gradient-gauge" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Gradient Gauge Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "lcd-gauge" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "LCD Gauge Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "color-background" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Color Background Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "color-background-solid" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Color Background Solid Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "some-other-mode" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Other Display Mode", + "type": "table" + } + + + ], + "preload": false, + "refresh": true, + "schemaVersion": 37, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.38.json b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.38.json new file mode 100644 index 00000000000..30f64b0bc8e --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.38.json @@ -0,0 +1,375 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Basic Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Gradient Gauge Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "lcd", + "type": "gauge" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "LCD Gauge Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "color-background" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Color Background Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "basic", + "type": "color-background" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Color Background Solid Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "type": "some-other-mode" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Other Display Mode", + "type": "table" + } + ], + "preload": false, + "refresh": true, + "schemaVersion": 38, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.39.json b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.39.json new file mode 100644 index 00000000000..1173963dc58 --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.39.json @@ -0,0 +1,375 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Basic Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Gradient Gauge Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "lcd", + "type": "gauge" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "LCD Gauge Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "color-background" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Color Background Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "basic", + "type": "color-background" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Color Background Solid Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "type": "some-other-mode" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Other Display Mode", + "type": "table" + } + ], + "preload": false, + "refresh": true, + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.40.json b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.40.json new file mode 100644 index 00000000000..843cc6284f7 --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.40.json @@ -0,0 +1,375 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Basic Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Gradient Gauge Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "lcd", + "type": "gauge" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "LCD Gauge Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "color-background" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Color Background Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "mode": "basic", + "type": "color-background" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Color Background Solid Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "cellOptions": { + "type": "some-other-mode" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Other Display Mode", + "type": "table" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.38.json b/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.38.json new file mode 100644 index 00000000000..081cb14634f --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.38.json @@ -0,0 +1,153 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "B" + } + ], + "title": "Panel Title", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "refIdToStat": { + "A": "mean", + "B": "max" + } + } + } + ], + "type": "timeseries" + } + ], + "preload": false, + "refresh": true, + "schemaVersion": 38, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file From e60e217a235cddaf90d5229eea85f8bcb6c4c814 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Wed, 12 Feb 2025 20:19:57 +0100 Subject: [PATCH 538/894] Dashboard V0->V1 Migration: Schema migration v37 (#99962) --- .../migration/schemaversion/migrations.go | 3 +- .../dashboard/migration/schemaversion/v37.go | 62 +++ .../migration/schemaversion/v37_test.go | 170 +++++++++ .../input/36.legend_normalization.json | 123 ++++++ .../output/36.legend_normalization.37.json | 132 +++++++ .../output/36.legend_normalization.38.json | 132 +++++++ .../output/36.legend_normalization.39.json | 132 +++++++ .../output/36.legend_normalization.40.json | 132 +++++++ .../37.timeseries_table_display_mode.37.json | 358 ++++++++++++++++++ 9 files changed, 1243 insertions(+), 1 deletion(-) create mode 100644 pkg/apis/dashboard/migration/schemaversion/v37.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/v37_test.go create mode 100644 pkg/apis/dashboard/migration/testdata/input/36.legend_normalization.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.37.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.38.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.39.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.40.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.37.json diff --git a/pkg/apis/dashboard/migration/schemaversion/migrations.go b/pkg/apis/dashboard/migration/schemaversion/migrations.go index 5875261aaeb..6d3955c329f 100644 --- a/pkg/apis/dashboard/migration/schemaversion/migrations.go +++ b/pkg/apis/dashboard/migration/schemaversion/migrations.go @@ -5,11 +5,12 @@ import "strconv" type SchemaVersionMigrationFunc func(map[string]interface{}) error const ( - MINIUM_VERSION = 37 + MINIUM_VERSION = 36 LATEST_VERSION = 40 ) var Migrations = map[int]SchemaVersionMigrationFunc{ + 37: V37, 38: V38, 39: V39, 40: V40, diff --git a/pkg/apis/dashboard/migration/schemaversion/v37.go b/pkg/apis/dashboard/migration/schemaversion/v37.go new file mode 100644 index 00000000000..c040daf842c --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/v37.go @@ -0,0 +1,62 @@ +package schemaversion + +// V37 normalizes legend configuration in panels to use a consistent format: +// - Converts boolean legend values to object format +// - Standardizes hidden legends to use showLegend: false with displayMode: list +// - Ensures visible legends have showLegend: true +func V37(dashboard map[string]interface{}) error { + dashboard["schemaVersion"] = int(37) + + panels, ok := dashboard["panels"].([]interface{}) + if !ok { + return nil + } + + for _, panel := range panels { + p, ok := panel.(map[string]interface{}) + if !ok { + continue + } + + options, ok := p["options"].(map[string]interface{}) + if !ok { + continue + } + + // Skip if no legend config exists + legendValue := options["legend"] + if legendValue == nil { + continue + } + + // Convert boolean legend to object format + if legendBool, ok := legendValue.(bool); ok { + options["legend"] = map[string]interface{}{ + "displayMode": "list", + "showLegend": legendBool, + } + continue + } + + // Handle object format legend + legend, ok := legendValue.(map[string]interface{}) + if !ok { + continue + } + + displayMode, hasDisplayMode := legend["displayMode"].(string) + showLegend, hasShowLegend := legend["showLegend"].(bool) + + // Normalize hidden legends + if (hasDisplayMode && displayMode == "hidden") || (hasShowLegend && !showLegend) { + legend["displayMode"] = "list" + legend["showLegend"] = false + continue + } + + // Ensure visible legends have showLegend true + legend["showLegend"] = true + } + + return nil +} diff --git a/pkg/apis/dashboard/migration/schemaversion/v37_test.go b/pkg/apis/dashboard/migration/schemaversion/v37_test.go new file mode 100644 index 00000000000..e0b2f4c27cb --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/v37_test.go @@ -0,0 +1,170 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" +) + +func TestV37(t *testing.T) { + tests := []migrationTestCase{ + { + name: "no legend config", + input: map[string]interface{}{ + "schemaVersion": 36, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "options": map[string]interface{}{}, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "options": map[string]interface{}{}, + }, + }, + }, + }, + { + name: "boolean legend true", + input: map[string]interface{}{ + "schemaVersion": 36, + "panels": []interface{}{ + map[string]interface{}{ + "options": map[string]interface{}{ + "legend": true, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "displayMode": "list", + "showLegend": true, + }, + }, + }, + }, + }, + }, + { + name: "boolean legend false", + input: map[string]interface{}{ + "schemaVersion": 36, + "panels": []interface{}{ + map[string]interface{}{ + "options": map[string]interface{}{ + "legend": false, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "displayMode": "list", + "showLegend": false, + }, + }, + }, + }, + }, + }, + { + name: "hidden displayMode", + input: map[string]interface{}{ + "schemaVersion": 36, + "panels": []interface{}{ + map[string]interface{}{ + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "displayMode": "hidden", + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "displayMode": "list", + "showLegend": false, + }, + }, + }, + }, + }, + }, + { + name: "showLegend false", + input: map[string]interface{}{ + "schemaVersion": 36, + "panels": []interface{}{ + map[string]interface{}{ + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "showLegend": false, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "displayMode": "list", + "showLegend": false, + }, + }, + }, + }, + }, + }, + { + name: "visible legend", + input: map[string]interface{}{ + "schemaVersion": 36, + "panels": []interface{}{ + map[string]interface{}{ + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "displayMode": "table", + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 37, + "panels": []interface{}{ + map[string]interface{}{ + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "displayMode": "table", + "showLegend": true, + }, + }, + }, + }, + }, + }, + } + runMigrationTests(t, tests, schemaversion.V37) +} diff --git a/pkg/apis/dashboard/migration/testdata/input/36.legend_normalization.json b/pkg/apis/dashboard/migration/testdata/input/36.legend_normalization.json new file mode 100644 index 00000000000..93ddfecb078 --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/input/36.legend_normalization.json @@ -0,0 +1,123 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "type": "graph", + "options": {}, + "title": "No Legend Config", + "id": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + } + }, + { + "options": { + "legend": true + }, + "title": "Boolean Legend True", + "id": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + } + }, + { + "options": { + "legend": false + }, + "title": "Boolean Legend False", + "id": 3, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + } + }, + { + "options": { + "legend": { + "displayMode": "hidden" + } + }, + "title": "Hidden DisplayMode", + "id": 4, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + } + }, + { + "options": { + "legend": { + "showLegend": false + } + }, + "title": "ShowLegend False", + "id": 5, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + } + }, + { + "options": { + "legend": { + "displayMode": "table" + } + }, + "title": "Visible Legend", + "id": 6, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + } + } + ], + "preload": false, + "refresh": true, + "schemaVersion": 36, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.37.json b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.37.json new file mode 100644 index 00000000000..1e6e0484aad --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.37.json @@ -0,0 +1,132 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": {}, + "title": "No Legend Config", + "type": "graph" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "displayMode": "list", + "showLegend": true + } + }, + "title": "Boolean Legend True" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Boolean Legend False" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Hidden DisplayMode" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "ShowLegend False" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "legend": { + "displayMode": "table", + "showLegend": true + } + }, + "title": "Visible Legend" + } + ], + "preload": false, + "refresh": true, + "schemaVersion": 37, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.38.json b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.38.json new file mode 100644 index 00000000000..14c5c32071f --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.38.json @@ -0,0 +1,132 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": {}, + "title": "No Legend Config", + "type": "graph" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "displayMode": "list", + "showLegend": true + } + }, + "title": "Boolean Legend True" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Boolean Legend False" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Hidden DisplayMode" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "ShowLegend False" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "legend": { + "displayMode": "table", + "showLegend": true + } + }, + "title": "Visible Legend" + } + ], + "preload": false, + "refresh": true, + "schemaVersion": 38, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.39.json b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.39.json new file mode 100644 index 00000000000..5e3239e89fa --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.39.json @@ -0,0 +1,132 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": {}, + "title": "No Legend Config", + "type": "graph" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "displayMode": "list", + "showLegend": true + } + }, + "title": "Boolean Legend True" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Boolean Legend False" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Hidden DisplayMode" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "ShowLegend False" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "legend": { + "displayMode": "table", + "showLegend": true + } + }, + "title": "Visible Legend" + } + ], + "preload": false, + "refresh": true, + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.40.json b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.40.json new file mode 100644 index 00000000000..91d625264b0 --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.40.json @@ -0,0 +1,132 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": {}, + "title": "No Legend Config", + "type": "graph" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "displayMode": "list", + "showLegend": true + } + }, + "title": "Boolean Legend True" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Boolean Legend False" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Hidden DisplayMode" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "ShowLegend False" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "legend": { + "displayMode": "table", + "showLegend": true + } + }, + "title": "Visible Legend" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.37.json b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.37.json new file mode 100644 index 00000000000..4b7c2fa4572 --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.37.json @@ -0,0 +1,358 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "basic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Basic Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "gradient-gauge" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Gradient Gauge Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "lcd-gauge" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "LCD Gauge Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "color-background" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Color Background Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "color-background-solid" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Color Background Solid Display Mode", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "displayMode": "some-other-mode" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "showHeader": true + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Other Display Mode", + "type": "table" + } + ], + "preload": false, + "refresh": true, + "schemaVersion": 37, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file From d1dee968c38791ed8fc4e1ae71229064b0e5bd96 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Wed, 12 Feb 2025 19:23:09 +0000 Subject: [PATCH 539/894] Schema V2: Adjust quick_ranges in time settings and remove time_options (#100135) * adjut quickRanges type in v2 * clean up unused time_options property * remove deprecated time_options property on time picker * add schema migration for time_options * adjust test --- .betterer.results | 3 +- devenv/bulk-dashboards/bulkdash.jsonnet | 12 -------- .../panel_tests_graph.json | 3 +- .../panel_tests_graph_time_regions.json | 11 ------- .../panel_tests_polystat.json | 3 +- .../influxdb-templated.json | 1 - .../datasource-mssql/mssql_fakedata.json | 3 +- .../datasource-mssql/mssql_unittest.json | 3 +- .../datasource-mysql/mysql_fakedata.json | 3 +- .../datasource-mysql/mysql_unittest.json | 3 +- .../postgres_fakedata.json | 3 +- .../postgres_unittest.json | 3 +- .../datasource-testdata/demo1.json | 1 - .../new_features_in_v62.json | 3 +- devenv/dev-dashboards/home.json | 11 ------- .../panel-bargauge/bar_gauge_demo.json | 3 +- .../panel-bargauge/panel_tests_bar_gauge.json | 11 ------- .../panel_tests_bar_gauge2.json | 3 +- .../panel-common/lazy_loading.json | 3 +- .../panel-common/panels_without_title.json | 3 +- .../panel-gauge/gauge-multi-series.json | 3 +- .../panel-gauge/gauge_tests.json | 3 +- .../graph-gradient-area-fills.json | 3 +- .../panel-graph/graph-time-regions.json | 3 +- .../panel-graph/graph_tests.json | 3 +- .../panel-polystat/polystat_test.json | 3 +- .../panel-table/table_tests.json | 3 +- .../timeseries-gradient-area.json | 3 +- .../slow_queries_and_annotations.json | 3 +- .../scenarios/time_zone_support.json | 3 +- .../dashboards/alerts/overview.json | 11 ------- .../dashboards/mysql/overview.json | 11 ------- .../dashboards/alerts/overview.json | 11 ------- .../dashboards/mysql/overview.json | 11 ------- .../view-dashboard-json-model/index.md | 4 +-- kinds/dashboard/dashboard_kind.cue | 4 +-- .../src/dashboards/grafana_stats.json | 3 +- .../src/dashboards/prometheus_2_stats.json | 3 +- .../src/dashboards/prometheus_stats.json | 3 +- .../raw/dashboard/x/dashboard_types.gen.ts | 7 +---- .../dashboard/v2alpha0/dashboard.schema.cue | 8 ++++- .../src/schema/dashboard/v2alpha0/examples.ts | 1 - .../schema/dashboard/v2alpha0/types.gen.ts | 27 ++++++++--------- pkg/kinds/dashboard/dashboard_spec_gen.go | 5 +--- .../service/testdata/dashboard.json | 3 +- .../containing-id/dashboard1.json | 11 ------- .../dashboard-with-uid/dashboard1.json | 11 ------- .../folder-one/dashboard1.json | 11 ------- .../folder-one/dashboard2.json | 11 ------- .../folderOne/dashboard1.json | 11 ------- .../folderTwo/dashboard2.json | 11 ------- .../folders-from-files-structure/root.json | 11 ------- .../one-dashboard/dashboard1.json | 11 ------- .../two-dashboards-with-uid/dashboard1.json | 11 ------- .../two-dashboards-with-uid/dashboard2.json | 11 ------- .../unprovision/dashboard1.json | 11 ------- pkg/tests/api/dashboards/home.json | 11 ------- .../DashboardScenePageStateManager.test.ts | 1 - .../DashboardSceneSerializer.test.ts | 2 -- .../serialization/DashboardSceneSerializer.ts | 3 +- .../transformSceneToSaveModel.test.ts.snap | 6 ++-- ...sformSceneToSaveModelSchemaV2.test.ts.snap | 1 - .../transformSceneToSaveModel.test.ts | 1 - .../transformSceneToSaveModelSchemaV2.ts | 7 +++-- .../__mocks__/dashboardHistoryMocks.ts | 1 - .../api/ResponseTransformers.test.ts | 29 ++++++++++++++++--- .../dashboard/api/ResponseTransformers.ts | 4 +-- .../GeneralSettings.test.tsx | 1 - .../containers/PublicDashboardPage.test.tsx | 2 +- .../dashboard/state/DashboardMigrator.test.ts | 22 ++++++++++++++ .../dashboard/state/DashboardMigrator.ts | 12 ++++++-- .../dashboards/streaming.json | 3 +- .../graphite/dashboards/carbon_metrics.json | 1 - .../graphite/dashboards/metrictank.json | 1 - .../prometheus/dashboards/grafana_stats.json | 3 +- .../dashboards/prometheus_2_stats.json | 3 +- .../dashboards/prometheus_stats.json | 3 +- public/dashboards/default.json | 1 - public/dashboards/home.json | 1 - public/dashboards/template_vars.json | 1 - scripts/import_many_dashboards.sh | 2 +- 81 files changed, 129 insertions(+), 345 deletions(-) diff --git a/.betterer.results b/.betterer.results index 9e1de13c1be..ca4b89c9605 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2212,8 +2212,7 @@ exports[`better eslint`] = { [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. Wrap text with ", "11"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "10"] ], "public/app/features/alerting/unified/components/rule-editor/GrafanaFolderAndLabelsStep.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] diff --git a/devenv/bulk-dashboards/bulkdash.jsonnet b/devenv/bulk-dashboards/bulkdash.jsonnet index 1a77d8abd70..05e396df2d6 100644 --- a/devenv/bulk-dashboards/bulkdash.jsonnet +++ b/devenv/bulk-dashboards/bulkdash.jsonnet @@ -1118,18 +1118,6 @@ "1d" ], "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "2h", - " 6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "type": "timepicker" }, "timezone": "browser", diff --git a/devenv/dev-dashboards-without-uid/panel_tests_graph.json b/devenv/dev-dashboards-without-uid/panel_tests_graph.json index b5d50f4f7b7..6bba1d9ae02 100644 --- a/devenv/dev-dashboards-without-uid/panel_tests_graph.json +++ b/devenv/dev-dashboards-without-uid/panel_tests_graph.json @@ -1639,8 +1639,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "browser", "title": "Panel Tests - Graph", diff --git a/devenv/dev-dashboards-without-uid/panel_tests_graph_time_regions.json b/devenv/dev-dashboards-without-uid/panel_tests_graph_time_regions.json index 3ff76d12df2..98d49958aaf 100644 --- a/devenv/dev-dashboards-without-uid/panel_tests_graph_time_regions.json +++ b/devenv/dev-dashboards-without-uid/panel_tests_graph_time_regions.json @@ -490,17 +490,6 @@ "1h", "2h", "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" ] }, "timezone": "browser", diff --git a/devenv/dev-dashboards-without-uid/panel_tests_polystat.json b/devenv/dev-dashboards-without-uid/panel_tests_polystat.json index 951bb780017..25b1f7154e5 100644 --- a/devenv/dev-dashboards-without-uid/panel_tests_polystat.json +++ b/devenv/dev-dashboards-without-uid/panel_tests_polystat.json @@ -3408,8 +3408,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Panel Tests - Polystat", diff --git a/devenv/dev-dashboards/datasource-influxdb/influxdb-templated.json b/devenv/dev-dashboards/datasource-influxdb/influxdb-templated.json index 97719e82251..f46ccc2042f 100644 --- a/devenv/dev-dashboards/datasource-influxdb/influxdb-templated.json +++ b/devenv/dev-dashboards/datasource-influxdb/influxdb-templated.json @@ -312,7 +312,6 @@ "now": true, "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], "status": "Stable", - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"], "type": "timepicker" }, "timezone": "browser", diff --git a/devenv/dev-dashboards/datasource-mssql/mssql_fakedata.json b/devenv/dev-dashboards/datasource-mssql/mssql_fakedata.json index 19c5e1d0718..8fda3e8e714 100644 --- a/devenv/dev-dashboards/datasource-mssql/mssql_fakedata.json +++ b/devenv/dev-dashboards/datasource-mssql/mssql_fakedata.json @@ -537,8 +537,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Datasource tests - MSSQL", diff --git a/devenv/dev-dashboards/datasource-mssql/mssql_unittest.json b/devenv/dev-dashboards/datasource-mssql/mssql_unittest.json index 0137001067c..40f9d55e5ca 100644 --- a/devenv/dev-dashboards/datasource-mssql/mssql_unittest.json +++ b/devenv/dev-dashboards/datasource-mssql/mssql_unittest.json @@ -2831,8 +2831,7 @@ "to": "2018-03-15T13:55:01.000Z" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Datasource tests - MSSQL (unit test)", diff --git a/devenv/dev-dashboards/datasource-mysql/mysql_fakedata.json b/devenv/dev-dashboards/datasource-mysql/mysql_fakedata.json index 96e4688f7bd..cd1a24fb5bd 100644 --- a/devenv/dev-dashboards/datasource-mysql/mysql_fakedata.json +++ b/devenv/dev-dashboards/datasource-mysql/mysql_fakedata.json @@ -541,8 +541,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Datasource tests - MySQL", diff --git a/devenv/dev-dashboards/datasource-mysql/mysql_unittest.json b/devenv/dev-dashboards/datasource-mysql/mysql_unittest.json index b68c9db97ec..2f2a42c1175 100644 --- a/devenv/dev-dashboards/datasource-mysql/mysql_unittest.json +++ b/devenv/dev-dashboards/datasource-mysql/mysql_unittest.json @@ -2643,8 +2643,7 @@ "to": "2018-03-15T13:55:01.000Z" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Datasource tests - MySQL (unittest)", diff --git a/devenv/dev-dashboards/datasource-postgres/postgres_fakedata.json b/devenv/dev-dashboards/datasource-postgres/postgres_fakedata.json index d7d9514e639..750b3284517 100644 --- a/devenv/dev-dashboards/datasource-postgres/postgres_fakedata.json +++ b/devenv/dev-dashboards/datasource-postgres/postgres_fakedata.json @@ -577,8 +577,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Datasource tests - Postgres", diff --git a/devenv/dev-dashboards/datasource-postgres/postgres_unittest.json b/devenv/dev-dashboards/datasource-postgres/postgres_unittest.json index a114ed1b7ef..acec0d08a44 100644 --- a/devenv/dev-dashboards/datasource-postgres/postgres_unittest.json +++ b/devenv/dev-dashboards/datasource-postgres/postgres_unittest.json @@ -2621,8 +2621,7 @@ "to": "2018-03-15T13:55:01.000Z" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Datasource tests - Postgres (unittest)", diff --git a/devenv/dev-dashboards/datasource-testdata/demo1.json b/devenv/dev-dashboards/datasource-testdata/demo1.json index abe39ffeb55..6d8034f81e6 100644 --- a/devenv/dev-dashboards/datasource-testdata/demo1.json +++ b/devenv/dev-dashboards/datasource-testdata/demo1.json @@ -1092,7 +1092,6 @@ "now": true, "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], "status": "Stable", - "time_options": ["5m", "15m", "1h", "2h", " 6h", "12h", "24h", "2d", "7d", "30d"], "type": "timepicker" }, "timezone": "browser", diff --git a/devenv/dev-dashboards/datasource-testdata/new_features_in_v62.json b/devenv/dev-dashboards/datasource-testdata/new_features_in_v62.json index 4b9535ecbbd..a002a208f85 100644 --- a/devenv/dev-dashboards/datasource-testdata/new_features_in_v62.json +++ b/devenv/dev-dashboards/datasource-testdata/new_features_in_v62.json @@ -1326,8 +1326,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "New Features in v6.2", diff --git a/devenv/dev-dashboards/home.json b/devenv/dev-dashboards/home.json index 9c3d65d4add..840d32919ea 100644 --- a/devenv/dev-dashboards/home.json +++ b/devenv/dev-dashboards/home.json @@ -240,17 +240,6 @@ "1h", "2h", "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" ] }, "timezone": "", diff --git a/devenv/dev-dashboards/panel-bargauge/bar_gauge_demo.json b/devenv/dev-dashboards/panel-bargauge/bar_gauge_demo.json index a708467d7be..eda6ccfe996 100644 --- a/devenv/dev-dashboards/panel-bargauge/bar_gauge_demo.json +++ b/devenv/dev-dashboards/panel-bargauge/bar_gauge_demo.json @@ -654,8 +654,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["2s", "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["2s", "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Bar Gauge Demo", diff --git a/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json b/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json index 29a6929cf8f..3dfa360c740 100644 --- a/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json +++ b/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json @@ -1423,17 +1423,6 @@ "1h", "2h", "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" ] }, "timezone": "", diff --git a/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge2.json b/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge2.json index 0f36c203cd0..06fc26e382d 100644 --- a/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge2.json +++ b/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge2.json @@ -519,8 +519,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Panel Tests - Bar Gauge 2", diff --git a/devenv/dev-dashboards/panel-common/lazy_loading.json b/devenv/dev-dashboards/panel-common/lazy_loading.json index 859eede4b4f..960c466124c 100644 --- a/devenv/dev-dashboards/panel-common/lazy_loading.json +++ b/devenv/dev-dashboards/panel-common/lazy_loading.json @@ -2202,8 +2202,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Lazy Loading", diff --git a/devenv/dev-dashboards/panel-common/panels_without_title.json b/devenv/dev-dashboards/panel-common/panels_without_title.json index 44bd210e73b..8f62c9cca5e 100644 --- a/devenv/dev-dashboards/panel-common/panels_without_title.json +++ b/devenv/dev-dashboards/panel-common/panels_without_title.json @@ -893,8 +893,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Panel Tests - With & Without title", diff --git a/devenv/dev-dashboards/panel-gauge/gauge-multi-series.json b/devenv/dev-dashboards/panel-gauge/gauge-multi-series.json index 09b72e5c030..f7ff80edc12 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge-multi-series.json +++ b/devenv/dev-dashboards/panel-gauge/gauge-multi-series.json @@ -254,8 +254,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Panel Tests - Gauge Multi Series", diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests.json b/devenv/dev-dashboards/panel-gauge/gauge_tests.json index 458f53dbc08..309a255fcc7 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests.json @@ -1319,8 +1319,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Panel Tests - Gauge", diff --git a/devenv/dev-dashboards/panel-graph/graph-gradient-area-fills.json b/devenv/dev-dashboards/panel-graph/graph-gradient-area-fills.json index 01e9e8c2f43..c2d27efd469 100644 --- a/devenv/dev-dashboards/panel-graph/graph-gradient-area-fills.json +++ b/devenv/dev-dashboards/panel-graph/graph-gradient-area-fills.json @@ -372,8 +372,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Panel Tests - Graph - Gradient Area Fills", diff --git a/devenv/dev-dashboards/panel-graph/graph-time-regions.json b/devenv/dev-dashboards/panel-graph/graph-time-regions.json index 2031788ae3f..a4e03148536 100644 --- a/devenv/dev-dashboards/panel-graph/graph-time-regions.json +++ b/devenv/dev-dashboards/panel-graph/graph-time-regions.json @@ -569,8 +569,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "browser", "title": "Panel Tests - Graph Time Regions", diff --git a/devenv/dev-dashboards/panel-graph/graph_tests.json b/devenv/dev-dashboards/panel-graph/graph_tests.json index 4d01c2cb534..bd1fc95d3d6 100644 --- a/devenv/dev-dashboards/panel-graph/graph_tests.json +++ b/devenv/dev-dashboards/panel-graph/graph_tests.json @@ -1639,8 +1639,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "browser", "title": "Panel Tests - Graph", diff --git a/devenv/dev-dashboards/panel-polystat/polystat_test.json b/devenv/dev-dashboards/panel-polystat/polystat_test.json index 6be355ebd99..faa84463019 100644 --- a/devenv/dev-dashboards/panel-polystat/polystat_test.json +++ b/devenv/dev-dashboards/panel-polystat/polystat_test.json @@ -3408,8 +3408,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Panel Tests - Polystat", diff --git a/devenv/dev-dashboards/panel-table/table_tests.json b/devenv/dev-dashboards/panel-table/table_tests.json index 8582ef068d7..b8ca436e110 100644 --- a/devenv/dev-dashboards/panel-table/table_tests.json +++ b/devenv/dev-dashboards/panel-table/table_tests.json @@ -440,8 +440,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "browser", "title": "Panel Tests - Table", diff --git a/devenv/dev-dashboards/panel-timeseries/timeseries-gradient-area.json b/devenv/dev-dashboards/panel-timeseries/timeseries-gradient-area.json index a7389cc6ed6..8f3267a62d7 100644 --- a/devenv/dev-dashboards/panel-timeseries/timeseries-gradient-area.json +++ b/devenv/dev-dashboards/panel-timeseries/timeseries-gradient-area.json @@ -562,8 +562,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Panel Tests - Graph NG - Gradient Area Fills", diff --git a/devenv/dev-dashboards/scenarios/slow_queries_and_annotations.json b/devenv/dev-dashboards/scenarios/slow_queries_and_annotations.json index a7cc41acd8f..8966137b55e 100644 --- a/devenv/dev-dashboards/scenarios/slow_queries_and_annotations.json +++ b/devenv/dev-dashboards/scenarios/slow_queries_and_annotations.json @@ -1132,8 +1132,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Panel tests - Slow Queries & Annotations", diff --git a/devenv/dev-dashboards/scenarios/time_zone_support.json b/devenv/dev-dashboards/scenarios/time_zone_support.json index feb317f917e..cc1f81ee221 100644 --- a/devenv/dev-dashboards/scenarios/time_zone_support.json +++ b/devenv/dev-dashboards/scenarios/time_zone_support.json @@ -684,8 +684,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "utc", "title": "Panel Tests - Time zone support", diff --git a/devenv/docker/ha-test-unified-alerting/grafana/provisioning/dashboards/alerts/overview.json b/devenv/docker/ha-test-unified-alerting/grafana/provisioning/dashboards/alerts/overview.json index b4946cfb6b3..0357c3d10f1 100644 --- a/devenv/docker/ha-test-unified-alerting/grafana/provisioning/dashboards/alerts/overview.json +++ b/devenv/docker/ha-test-unified-alerting/grafana/provisioning/dashboards/alerts/overview.json @@ -151,17 +151,6 @@ "1h", "2h", "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" ] }, "timezone": "", diff --git a/devenv/docker/ha-test-unified-alerting/grafana/provisioning/dashboards/mysql/overview.json b/devenv/docker/ha-test-unified-alerting/grafana/provisioning/dashboards/mysql/overview.json index 7643250ec28..2bf789366cc 100644 --- a/devenv/docker/ha-test-unified-alerting/grafana/provisioning/dashboards/mysql/overview.json +++ b/devenv/docker/ha-test-unified-alerting/grafana/provisioning/dashboards/mysql/overview.json @@ -5376,17 +5376,6 @@ "1d" ], "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "type": "timepicker" }, "timezone": "browser", diff --git a/devenv/docker/ha_test/grafana/provisioning/dashboards/alerts/overview.json b/devenv/docker/ha_test/grafana/provisioning/dashboards/alerts/overview.json index b4946cfb6b3..0357c3d10f1 100644 --- a/devenv/docker/ha_test/grafana/provisioning/dashboards/alerts/overview.json +++ b/devenv/docker/ha_test/grafana/provisioning/dashboards/alerts/overview.json @@ -151,17 +151,6 @@ "1h", "2h", "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" ] }, "timezone": "", diff --git a/devenv/docker/ha_test/grafana/provisioning/dashboards/mysql/overview.json b/devenv/docker/ha_test/grafana/provisioning/dashboards/mysql/overview.json index 7643250ec28..2bf789366cc 100644 --- a/devenv/docker/ha_test/grafana/provisioning/dashboards/mysql/overview.json +++ b/devenv/docker/ha_test/grafana/provisioning/dashboards/mysql/overview.json @@ -5376,17 +5376,6 @@ "1d" ], "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "type": "timepicker" }, "timezone": "browser", diff --git a/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md b/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md index 44b20434a0d..0fff2d65d9f 100644 --- a/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md +++ b/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md @@ -138,12 +138,12 @@ The grid has a negative gravity that moves panels up if there is empty space abo "nowDelay": "", "quick_ranges": [ { - "display": "Last 6 hours" + "display": "Last 6 hours", "from": "now-6h", "to": "now" }, { - "display": "Last 7 days" + "display": "Last 7 days", "from": "now-7d", "to": "now" } diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index da8be7ef3d5..bbf3cb24321 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -74,7 +74,7 @@ lineage: schemas: [{ // Version of the JSON schema, incremented each time a Grafana update brings // changes to said schema. - schemaVersion: uint16 | *39 + schemaVersion: uint16 | *41 // Version of the dashboard, incremented each time the dashboard is updated. version?: uint32 @@ -473,8 +473,6 @@ lineage: schemas: [{ hidden?: bool | *false // Interval options available in the refresh picker dropdown. refresh_intervals?: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] - // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. - time_options?: [...string] | *["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] // Quick ranges for time picker. quick_ranges?: [...#TimeOption] // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. diff --git a/packages/grafana-prometheus/src/dashboards/grafana_stats.json b/packages/grafana-prometheus/src/dashboards/grafana_stats.json index 0131aa9bc40..292f93394f3 100644 --- a/packages/grafana-prometheus/src/dashboards/grafana_stats.json +++ b/packages/grafana-prometheus/src/dashboards/grafana_stats.json @@ -1178,8 +1178,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Grafana metrics", diff --git a/packages/grafana-prometheus/src/dashboards/prometheus_2_stats.json b/packages/grafana-prometheus/src/dashboards/prometheus_2_stats.json index 034663fe6f4..5a6fbdf8518 100644 --- a/packages/grafana-prometheus/src/dashboards/prometheus_2_stats.json +++ b/packages/grafana-prometheus/src/dashboards/prometheus_2_stats.json @@ -1394,8 +1394,7 @@ }, "timepicker": { "now": true, - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "browser", "title": "Prometheus 2.0 Stats", diff --git a/packages/grafana-prometheus/src/dashboards/prometheus_stats.json b/packages/grafana-prometheus/src/dashboards/prometheus_stats.json index 8a2764c5cb7..42ea6e7a4d5 100644 --- a/packages/grafana-prometheus/src/dashboards/prometheus_stats.json +++ b/packages/grafana-prometheus/src/dashboards/prometheus_stats.json @@ -825,8 +825,7 @@ }, "timepicker": { "now": true, - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "browser", "title": "Prometheus Stats", diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index e200106086a..fbaa9c8f09b 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -681,17 +681,12 @@ export interface TimePickerConfig { * Interval options available in the refresh picker dropdown. */ refresh_intervals?: Array; - /** - * Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. - */ - time_options?: Array; } export const defaultTimePickerConfig: Partial = { hidden: false, quick_ranges: [], refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], - time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'], }; /** @@ -1205,7 +1200,7 @@ export const defaultDashboard: Partial = { graphTooltip: DashboardCursorSync.Off, links: [], panels: [], - schemaVersion: 39, + schemaVersion: 41, tags: [], timezone: 'browser', }; diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 4b983ddf7ae..21012f3461a 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -447,6 +447,12 @@ QueryGroupKind: { spec: QueryGroupSpec } +TimeRangeOption: { + display: string | *"Last 6 hours" + from: string | *"now-6h" + to: string | *"now" +} + // Time configuration // It defines the default time config for the time picker, the refresh picker for the specific dashboard. TimeSettingsSpec: { @@ -463,7 +469,7 @@ TimeSettingsSpec: { // Interval options available in the refresh picker dropdown. autoRefreshIntervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] // v1: timepicker.refresh_intervals // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. - quickRanges: [...string] | *["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] // v1: timepicker.time_options , not exposed in the UI + quickRanges?: [...TimeRangeOption] // v1: timepicker.quick_ranges , not exposed in the UI // Whether timepicker is visible or not. hideTimepicker: bool // v1: timepicker.hidden // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts index 40b091f29dd..c12f1cc73f7 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts @@ -15,7 +15,6 @@ export const handyTestingSchema: DashboardV2Spec = { from: 'now-1h', hideTimepicker: false, nowDelay: '1m', - quickRanges: [], timezone: 'UTC', to: 'now', weekStart: 'monday', diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index 83c733c6f60..e85e71996fd 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -620,6 +620,18 @@ export const defaultQueryGroupKind = (): QueryGroupKind => ({ spec: defaultQueryGroupSpec(), }); +export interface TimeRangeOption { + display: string; + from: string; + to: string; +} + +export const defaultTimeRangeOption = (): TimeRangeOption => ({ + display: "Last 6 hours", + from: "now-6h", + to: "now", +}); + // Time configuration // It defines the default time config for the time picker, the refresh picker for the specific dashboard. export interface TimeSettingsSpec { @@ -638,8 +650,8 @@ export interface TimeSettingsSpec { // v1: timepicker.refresh_intervals autoRefreshIntervals: string[]; // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. - // v1: timepicker.time_options , not exposed in the UI - quickRanges: string[]; + // v1: timepicker.quick_ranges , not exposed in the UI + quickRanges?: TimeRangeOption[]; // Whether timepicker is visible or not. // v1: timepicker.hidden hideTimepicker: boolean; @@ -668,17 +680,6 @@ export const defaultTimeSettingsSpec = (): TimeSettingsSpec => ({ "1h", "2h", "1d", -], - quickRanges: [ -"5m", -"15m", -"1h", -"6h", -"12h", -"24h", -"2d", -"7d", -"30d", ], hideTimepicker: false, fiscalYearStartMonth: 0, diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index 38e17ae6219..526c5a66056 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -48,8 +48,6 @@ type TimePickerConfig struct { Hidden *bool `json:"hidden,omitempty"` // Interval options available in the refresh picker dropdown. RefreshIntervals []string `json:"refresh_intervals,omitempty"` - // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. - TimeOptions []string `json:"time_options,omitempty"` // Quick ranges for time picker. QuickRanges []TimeOption `json:"quick_ranges,omitempty"` // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. @@ -61,7 +59,6 @@ func NewTimePickerConfig() *TimePickerConfig { return &TimePickerConfig{ Hidden: (func(input bool) *bool { return &input })(false), RefreshIntervals: []string{"5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"}, - TimeOptions: []string{"5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"}, } } @@ -927,7 +924,7 @@ func NewSpec() *Spec { Editable: (func(input bool) *bool { return &input })(true), GraphTooltip: (func(input DashboardCursorSync) *DashboardCursorSync { return &input })(DashboardCursorSyncOff), FiscalYearStartMonth: (func(input uint8) *uint8 { return &input })(0), - SchemaVersion: 39, + SchemaVersion: 41, } } diff --git a/pkg/services/dashboardimport/service/testdata/dashboard.json b/pkg/services/dashboardimport/service/testdata/dashboard.json index 401d2e2676a..9358c6b40fb 100644 --- a/pkg/services/dashboardimport/service/testdata/dashboard.json +++ b/pkg/services/dashboardimport/service/testdata/dashboard.json @@ -209,8 +209,7 @@ }, "timepicker": { "now": true, - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "browser", "title": "Prometheus 2.0 Stats", diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/containing-id/dashboard1.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/containing-id/dashboard1.json index 94b5c9a1c02..668b7ba4f1b 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-dashboards/containing-id/dashboard1.json +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/containing-id/dashboard1.json @@ -30,17 +30,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "refresh_intervals": [ "5s", "10s", diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/dashboard-with-uid/dashboard1.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/dashboard-with-uid/dashboard1.json index c0ab4838bf3..c69015426a6 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-dashboards/dashboard-with-uid/dashboard1.json +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/dashboard-with-uid/dashboard1.json @@ -134,17 +134,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "refresh_intervals": [ "5s", "10s", diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard1.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard1.json index 8c8cf42fc78..b45c8dece35 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard1.json +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard1.json @@ -134,17 +134,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "refresh_intervals": [ "5s", "10s", diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard2.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard2.json index 94d29339a13..aa15ce8a12d 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard2.json +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard2.json @@ -134,17 +134,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "refresh_intervals": [ "5s", "10s", diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderOne/dashboard1.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderOne/dashboard1.json index 8c8cf42fc78..b45c8dece35 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderOne/dashboard1.json +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderOne/dashboard1.json @@ -134,17 +134,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "refresh_intervals": [ "5s", "10s", diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderTwo/dashboard2.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderTwo/dashboard2.json index 94d29339a13..aa15ce8a12d 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderTwo/dashboard2.json +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderTwo/dashboard2.json @@ -134,17 +134,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "refresh_intervals": [ "5s", "10s", diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/root.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/root.json index 6743fb1f6a6..4948686435d 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/root.json +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/root.json @@ -134,17 +134,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "refresh_intervals": [ "5s", "10s", diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/one-dashboard/dashboard1.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/one-dashboard/dashboard1.json index 9f786032f0e..3fd0d5aa927 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-dashboards/one-dashboard/dashboard1.json +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/one-dashboard/dashboard1.json @@ -134,17 +134,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "refresh_intervals": [ "5s", "10s", diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/two-dashboards-with-uid/dashboard1.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/two-dashboards-with-uid/dashboard1.json index c0ab4838bf3..c69015426a6 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-dashboards/two-dashboards-with-uid/dashboard1.json +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/two-dashboards-with-uid/dashboard1.json @@ -134,17 +134,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "refresh_intervals": [ "5s", "10s", diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/two-dashboards-with-uid/dashboard2.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/two-dashboards-with-uid/dashboard2.json index c0ab4838bf3..c69015426a6 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-dashboards/two-dashboards-with-uid/dashboard2.json +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/two-dashboards-with-uid/dashboard2.json @@ -134,17 +134,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "refresh_intervals": [ "5s", "10s", diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/unprovision/dashboard1.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/unprovision/dashboard1.json index 8c8cf42fc78..b45c8dece35 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-dashboards/unprovision/dashboard1.json +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/unprovision/dashboard1.json @@ -134,17 +134,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], "refresh_intervals": [ "5s", "10s", diff --git a/pkg/tests/api/dashboards/home.json b/pkg/tests/api/dashboards/home.json index ee516adac16..08e991b5925 100644 --- a/pkg/tests/api/dashboards/home.json +++ b/pkg/tests/api/dashboards/home.json @@ -210,17 +210,6 @@ "1h", "2h", "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" ] }, "timezone": "", diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 2e46767d485..a70fa61ab05 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -661,7 +661,6 @@ const customHomeDashboardV2Spec = { to: 'now', autoRefresh: '', autoRefreshIntervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], - quickRanges: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'], hideTimepicker: false, fiscalYearStartMonth: 0, }, diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index 8f09578ae54..747c69eaf07 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -584,7 +584,6 @@ describe('DashboardSceneSerializer', () => { to: '', autoRefresh: '', autoRefreshIntervals: [], - quickRanges: [], hideTimepicker: false, fiscalYearStartMonth: 0, timezone: '', @@ -646,7 +645,6 @@ describe('DashboardSceneSerializer', () => { from: 'now-1h', hideTimepicker: false, nowDelay: undefined, - quickRanges: [], timezone: 'browser', to: 'now', }); diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts index 039f366de9a..442b8356f91 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts @@ -4,6 +4,7 @@ import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alp import { AnnoKeyDashboardSnapshotOriginalUrl } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types'; +import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator'; import { getPanelPluginCounts, getV1SchemaVariables, @@ -185,7 +186,7 @@ export class V2DashboardSerializer if (this.initialSaveModel) { return { - schemaVersion: 40, + schemaVersion: DASHBOARD_SCHEMA_VERSION, uid: s.state.uid, title: this.initialSaveModel.title, panels_count: panelPluginIds.length || 0, diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index 8ab27fc7a70..8146499446c 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -280,7 +280,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back ], "preload": false, "refresh": "", - "schemaVersion": 40, + "schemaVersion": 41, "tags": [ "templating", "gdev", @@ -548,7 +548,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho ], "preload": false, "refresh": "5m", - "schemaVersion": 40, + "schemaVersion": 41, "tags": [ "tag1", "tag2", @@ -906,7 +906,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr ], "preload": false, "refresh": "", - "schemaVersion": 40, + "schemaVersion": 41, "tags": [ "gdev", "graph-ng", diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap index 7695bba0ab6..41482314925 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap @@ -195,7 +195,6 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model "from": "now-1h", "hideTimepicker": false, "nowDelay": "1m", - "quickRanges": [], "timezone": "UTC", "to": "now", "weekStart": "monday", diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index fbe3b3c4c0d..d9576c47fd0 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -185,7 +185,6 @@ describe('transformSceneToSaveModel', () => { timepicker: { ...dashboard_to_load1.timepicker, refresh_intervals: ['5m', '15m', '30m', '1h'], - time_options: ['5m', '15m', '30m'], hidden: true, }, links: [{ ...NEW_LINK, title: 'Link 1' }], diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index bd8ed9f3fac..979fba6483c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -89,7 +89,6 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps to: timeRange.to, autoRefresh: refreshPicker?.state.refresh || '', autoRefreshIntervals: refreshPicker?.state.intervals, - quickRanges: [], //FIXME is coming timepicker.time_options, hideTimepicker: controlsState?.hideTimeControls ?? false, weekStart: timeRange.weekStart, fiscalYearStartMonth: timeRange.fiscalYearStartMonth, @@ -502,7 +501,11 @@ function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec { if (!('autoRefreshIntervals' in dash.timeSettings) || !Array.isArray(dash.timeSettings.autoRefreshIntervals)) { throw new Error('AutoRefreshIntervals is not an array'); } - if (!('quickRanges' in dash.timeSettings) || !Array.isArray(dash.timeSettings.quickRanges)) { + if ( + 'quickRanges' in dash.timeSettings && + dash.timeSettings.quickRanges && + !Array.isArray(dash.timeSettings.quickRanges) + ) { throw new Error('QuickRanges is not an array'); } if (!('hideTimepicker' in dash.timeSettings) || typeof dash.timeSettings.hideTimepicker !== 'boolean') { diff --git a/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts b/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts index 3fbefb31c92..0b6de1db51a 100644 --- a/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts +++ b/public/app/features/dashboard-scene/settings/version-history/__mocks__/dashboardHistoryMocks.ts @@ -166,7 +166,6 @@ export function restore(version: number, restoredFrom?: number) { }, timepicker: { refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], - time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'], }, timezone: 'utc', title: 'History Dashboard', diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index 63cb8dc1e26..c598db36084 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -113,8 +113,19 @@ describe('ResponseTransformers', () => { timepicker: { refresh_intervals: ['5s', '10s', '30s'], hidden: false, - time_options: ['5m', '15m', '1h'], nowDelay: '1m', + quick_ranges: [ + { + display: 'Last 6 hours', + from: 'now-6h', + to: 'now', + }, + { + display: 'Last 7 days', + from: 'now-7d', + to: 'now', + }, + ], }, fiscalYearStartMonth: 1, weekStart: 'monday', @@ -462,7 +473,7 @@ describe('ResponseTransformers', () => { expect(spec.timeSettings.autoRefresh).toBe(dashboardV1.refresh); expect(spec.timeSettings.autoRefreshIntervals).toEqual(dashboardV1.timepicker?.refresh_intervals); expect(spec.timeSettings.hideTimepicker).toBe(dashboardV1.timepicker?.hidden); - expect(spec.timeSettings.quickRanges).toEqual(dashboardV1.timepicker?.time_options); + expect(spec.timeSettings.quickRanges).toEqual(dashboardV1.timepicker?.quick_ranges); expect(spec.timeSettings.nowDelay).toBe(dashboardV1.timepicker?.nowDelay); expect(spec.timeSettings.fiscalYearStartMonth).toBe(dashboardV1.fiscalYearStartMonth); expect(spec.timeSettings.weekStart).toBe(dashboardV1.weekStart); @@ -655,7 +666,18 @@ describe('ResponseTransformers', () => { autoRefresh: '5m', autoRefreshIntervals: ['5s', '10s', '30s'], hideTimepicker: false, - quickRanges: ['5m', '15m', '1h'], + quickRanges: [ + { + display: 'Last 6 hours', + from: 'now-6h', + to: 'now', + }, + { + display: 'Last 7 days', + from: 'now-7d', + to: 'now', + }, + ], nowDelay: '1m', fiscalYearStartMonth: 1, weekStart: 'monday', @@ -730,7 +752,6 @@ describe('ResponseTransformers', () => { expect(dashboard.refresh).toBe(dashboardV2.spec.timeSettings.autoRefresh); expect(dashboard.timepicker?.refresh_intervals).toEqual(dashboardV2.spec.timeSettings.autoRefreshIntervals); expect(dashboard.timepicker?.hidden).toBe(dashboardV2.spec.timeSettings.hideTimepicker); - expect(dashboard.timepicker?.time_options).toEqual(dashboardV2.spec.timeSettings.quickRanges); expect(dashboard.timepicker?.nowDelay).toBe(dashboardV2.spec.timeSettings.nowDelay); expect(dashboard.fiscalYearStartMonth).toBe(dashboardV2.spec.timeSettings.fiscalYearStartMonth); expect(dashboard.weekStart).toBe(dashboardV2.spec.timeSettings.weekStart); diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index b4841435f3a..69edee427e4 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -160,7 +160,7 @@ export function ensureV2Response( autoRefreshIntervals: dashboard.timepicker?.refresh_intervals || timeSettingsDefaults.autoRefreshIntervals, fiscalYearStartMonth: dashboard.fiscalYearStartMonth || timeSettingsDefaults.fiscalYearStartMonth, hideTimepicker: dashboard.timepicker?.hidden || timeSettingsDefaults.hideTimepicker, - quickRanges: dashboard.timepicker?.time_options || timeSettingsDefaults.quickRanges, + quickRanges: dashboard.timepicker?.quick_ranges, // casting WeekStart here to avoid editing old schema weekStart: (dashboard.weekStart as WeekStart) || timeSettingsDefaults.weekStart, nowDelay: dashboard.timepicker?.nowDelay || timeSettingsDefaults.nowDelay, @@ -252,7 +252,7 @@ export function ensureV1Response( timepicker: { refresh_intervals: spec.timeSettings.autoRefreshIntervals, hidden: spec.timeSettings.hideTimepicker, - time_options: spec.timeSettings.quickRanges, + quick_ranges: spec.timeSettings.quickRanges, nowDelay: spec.timeSettings.nowDelay, }, fiscalYearStartMonth: spec.timeSettings.fiscalYearStartMonth, diff --git a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.test.tsx index edd31d452a5..0983707cf94 100644 --- a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.test.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.test.tsx @@ -23,7 +23,6 @@ const setupTestContext = (options: Partial) => { description: 'test dashboard description', timepicker: { refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d', '2d'], - time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'], hidden: false, }, timezone: 'utc', diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx index 915dd50d69a..b7e9e3ecf2e 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx @@ -245,7 +245,7 @@ describe('PublicDashboardPage', () => { ...dashboardBase, getModel: () => getTestDashboard({ - timepicker: { hidden: false, refresh_intervals: [], time_options: [] }, + timepicker: { hidden: false, refresh_intervals: [] }, }), }, }); diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index 6ba62a860e5..c41b87c0e4b 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -2428,6 +2428,28 @@ describe('when migrating variable refresh to on dashboard load', () => { }); }); +describe('when migrating time_options in timepicker', () => { + let model: DashboardModel; + + it('should remove the property', () => { + model = new DashboardModel({ + timepicker: { + //@ts-expect-error + time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'], + }, + }); + + expect(model.timepicker).not.toHaveProperty('time_options'); + }); + + it('should not throw with empty timepicker', () => { + //@ts-expect-error + model = new DashboardModel({}); + + expect(model.timepicker).not.toHaveProperty('time_options'); + }); +}); + function createRow(options: any, panelDescriptions: any[]) { const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN; const { collapse, showTitle, title, repeat, repeatIteration } = options; diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index 02efa8b8ff0..7cfa50f0b7d 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -81,7 +81,7 @@ type PanelSchemeUpgradeHandler = (panel: PanelModel) => PanelModel; * kinds/dashboard/dashboard_kind.cue * Example PR: #87712 */ -export const DASHBOARD_SCHEMA_VERSION = 40; +export const DASHBOARD_SCHEMA_VERSION = 41; export class DashboardMigrator { dashboard: DashboardModel; @@ -905,12 +905,20 @@ export class DashboardMigrator { } if (oldVersion < 40) { - // In old ashboards refresh property can be a boolean + // In old dashboards refresh property can be a boolean if (typeof this.dashboard.refresh !== 'string') { this.dashboard.refresh = ''; } } + if (oldVersion < 41) { + // time_options is a legacy property that was not used since grafana version 5 + // therefore deprecating this property from the schema + if ('time_options' in this.dashboard.timepicker) { + delete this.dashboard.timepicker.time_options; + } + } + /** * -==- Add migration here -==- * Your migration should go below the previous diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/dashboards/streaming.json b/public/app/plugins/datasource/grafana-testdata-datasource/dashboards/streaming.json index 8498d79f2f1..b5bd877bcaa 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/dashboards/streaming.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/dashboards/streaming.json @@ -199,8 +199,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Simple Streaming Example", diff --git a/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json b/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json index 94e8e685d45..418ba46835f 100644 --- a/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json +++ b/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json @@ -154,7 +154,6 @@ "now": true, "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], "status": "Stable", - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"], "type": "timepicker" }, "templating": { diff --git a/public/app/plugins/datasource/graphite/dashboards/metrictank.json b/public/app/plugins/datasource/graphite/dashboards/metrictank.json index 18b2e3939ff..b70ad4d58ec 100644 --- a/public/app/plugins/datasource/graphite/dashboards/metrictank.json +++ b/public/app/plugins/datasource/graphite/dashboards/metrictank.json @@ -4771,7 +4771,6 @@ "now": true, "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], "status": "Stable", - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"], "type": "timepicker" }, "timezone": "utc", diff --git a/public/app/plugins/datasource/prometheus/dashboards/grafana_stats.json b/public/app/plugins/datasource/prometheus/dashboards/grafana_stats.json index a121de7fe5a..d465f31c1f5 100644 --- a/public/app/plugins/datasource/prometheus/dashboards/grafana_stats.json +++ b/public/app/plugins/datasource/prometheus/dashboards/grafana_stats.json @@ -1177,8 +1177,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Grafana metrics", diff --git a/public/app/plugins/datasource/prometheus/dashboards/prometheus_2_stats.json b/public/app/plugins/datasource/prometheus/dashboards/prometheus_2_stats.json index 3d4cea64f05..57e2bb5e47d 100644 --- a/public/app/plugins/datasource/prometheus/dashboards/prometheus_2_stats.json +++ b/public/app/plugins/datasource/prometheus/dashboards/prometheus_2_stats.json @@ -1393,8 +1393,7 @@ }, "timepicker": { "now": true, - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "browser", "title": "Prometheus 2.0 Stats", diff --git a/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json b/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json index 9169006b895..383f75c9011 100644 --- a/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json +++ b/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json @@ -824,8 +824,7 @@ }, "timepicker": { "now": true, - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "browser", "title": "Prometheus Stats", diff --git a/public/dashboards/default.json b/public/dashboards/default.json index c59f98ec1dd..2b2ef5d1c9b 100644 --- a/public/dashboards/default.json +++ b/public/dashboards/default.json @@ -131,7 +131,6 @@ "collapse": false, "enable": true, "status": "Stable", - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"], "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], "now": true } diff --git a/public/dashboards/home.json b/public/dashboards/home.json index 718b6b52079..8d5cfd00e52 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -68,7 +68,6 @@ "timepicker": { "hidden": true, "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"], "type": "timepicker" }, "timezone": "browser", diff --git a/public/dashboards/template_vars.json b/public/dashboards/template_vars.json index 33478bc8081..04cb1c95d79 100644 --- a/public/dashboards/template_vars.json +++ b/public/dashboards/template_vars.json @@ -169,7 +169,6 @@ "notice": false, "enable": true, "status": "Stable", - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"], "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], "now": true } diff --git a/scripts/import_many_dashboards.sh b/scripts/import_many_dashboards.sh index 60d4fcd55b3..47cbb2c36c2 100755 --- a/scripts/import_many_dashboards.sh +++ b/scripts/import_many_dashboards.sh @@ -3,6 +3,6 @@ for index in {0..3000} do echo -n "index $index" - curl 'http://localhost:3000/api/dashboards/import' -H 'Pragma: no-cache' -H 'Origin: http://localhost:3000' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: en-US,en;q=0.8,sv;q=0.6' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.101 Safari/537.36' -H 'Content-Type: application/json;charset=UTF-8' -H 'Accept: application/json, text/plain, */*' -H 'Cache-Control: no-cache' -H 'Referer: http://localhost:3000/dashboard/new?editview=import' -H 'Cookie: grafana_sess=662a67f11b47e657; grafana_user=admin; grafana_remember=bd839923f24f648c7cb53ede6ff9ef40826204e9a22df8f9; toggles=%7B%7D' -H 'Connection: keep-alive' --data-binary $'{"dashboard":{"__inputs":[{"name":"DS_GRAPHITE","label":"graphite","description":"","type":"datasource","pluginId":"graphite","pluginName":"Graphite"}],"__requires":[{"type":"panel","id":"singlestat","name":"Singlestat","version":""},{"type":"panel","id":"graph","name":"Graph","version":""},{"type":"grafana","id":"grafana","name":"Grafana","version":"3.1.0"},{"type":"datasource","id":"graphite","name":"Graphite","version":"1.0.0"}],"id":null,"title":"Big Dashboard dashname '"$index"$'","tags":["startpage","home","presentation"],"style":"dark","timezone":"browser","editable":true,"hideControls":false,"sharedCrosshair":true,"rows":[{"collapse":false,"editable":true,"height":"100px","panels":[{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(245, 54, 54, 0.9)","rgba(237, 129, 40, 0.89)","rgba(50, 172, 45, 0.97)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":100,"minValue":0,"show":false,"thresholdLabels":false,"thresholdMarkers":true},"id":16,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":3,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":true},"targets":[{"refId":"A","target":"apps.backend.backend_02.counters.requests.count"}],"thresholds":"100,270","title":"Sign ups","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(245, 54, 54, 0.9)","rgba(237, 129, 40, 0.89)","rgba(50, 172, 45, 0.97)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":100,"minValue":0,"show":false,"thresholdLabels":false,"thresholdMarkers":true},"id":15,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":3,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":true},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.7)"}],"thresholds":"100,270","title":"Logins","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(245, 54, 54, 0.9)","rgba(237, 129, 40, 0.89)","rgba(50, 172, 45, 0.97)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":100,"minValue":0,"show":false,"thresholdLabels":false,"thresholdMarkers":true},"id":17,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":3,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":true},"targets":[{"refId":"A","target":"apps.backend.backend_04.counters.requests.count"}],"thresholds":"100,270","title":"Sign outs","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(245, 54, 54, 0.9)","rgba(237, 129, 40, 0.89)","rgba(50, 172, 45, 0.97)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":100,"minValue":0,"show":false,"thresholdLabels":false,"thresholdMarkers":true},"id":18,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":3,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":true},"targets":[{"refId":"A","target":"scale(apps.backend.backend_03.counters.requests.count, 0.3)"}],"thresholds":"100,270","title":"Support calls","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1}],"title":"New row"},{"collapse":false,"editable":true,"height":218.4375,"panels":[{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":20,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.7)"}],"thresholds":"200,270","title":"Logins","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":24,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.2)"}],"thresholds":"200,270","title":"Google hits","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"bytes","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":22,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.4)"}],"thresholds":"200,270","title":"Memory","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":21,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.8)"}],"thresholds":"200,270","title":"Logouts","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":26,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.2)"}],"thresholds":"200,270","title":"Google hits","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":25,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.8)"}],"thresholds":"200,270","title":"Logouts","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1}],"title":"New row"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"cpu":"#E24D42","memory":"#6ED0E0","statsd.fakesite.counters.session_start.desktop.count":"#6ED0E0"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":3,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":4,"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[{"alias":"cpu","fill":0,"lines":true,"yaxis":2,"zindex":2},{"alias":"memory","pointradius":2,"points":true}],"span":4,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"hide":false,"refId":"A","target":"alias(movingAverage(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count, 10), 20), \'cpu\')"},{"refId":"B","target":"alias(statsd.fakesite.counters.session_start.desktop.count, \'memory\')"}],"timeFrom":null,"timeShift":null,"timezone":"browser","title":"Memory / CPU","tooltip":{"msResolution":false,"query_as_alias":true,"shared":false,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"bytes","logBase":1,"max":null,"min":null,"show":true},{"format":"percent","logBase":1,"max":null,"min":0,"show":true}],"zerofill":true},{"aliasColors":{"logins":"#7EB26D","logins (-1 day)":"#447EBC"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":1,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":3,"interactive":true,"legend":{"alignAsTable":false,"avg":false,"current":true,"max":true,"min":true,"rightSide":false,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":1,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[],"span":4,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"refId":"A","target":"alias(movingAverage(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count, 1), 2), \'logins\')"},{"refId":"B","target":"alias(movingAverage(timeShift(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count, 1), \'1h\'), 2), \'logins (-1 hour)\')"}],"timeFrom":null,"timeShift":"1h","timezone":"browser","title":"logins","tooltip":{"msResolution":false,"query_as_alias":true,"shared":false,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"short","logBase":1,"max":null,"min":null,"show":true},{"format":"short","logBase":1,"max":null,"min":null,"show":true}],"zerofill":true},{"aliasColors":{"cpu":"#E24D42","memory":"#6ED0E0","statsd.fakesite.counters.session_start.desktop.count":"#6ED0E0"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":3,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":19,"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[{"alias":"cpu","fill":0,"lines":true,"yaxis":2,"zindex":2},{"alias":"memory","pointradius":2,"points":true}],"span":4,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"hide":false,"refId":"A","target":"alias(movingAverage(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count, 10), 20), \'cpu\')"},{"refId":"B","target":"alias(statsd.fakesite.counters.session_start.desktop.count, \'memory\')"}],"timeFrom":null,"timeShift":"1h","timezone":"browser","title":"Memory / CPU","tooltip":{"msResolution":false,"query_as_alias":true,"shared":false,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"bytes","logBase":1,"max":null,"min":null,"show":true},{"format":"percent","logBase":1,"max":null,"min":0,"show":true}],"zerofill":true}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"300px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#B7DBAB","web_server_02":"#7EB26D","web_server_03":"#508642","web_server_04":"#3F6833"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":8,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":2,"interactive":true,"legend":{"alignAsTable":false,"avg":false,"current":false,"max":false,"min":false,"rightSide":false,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[],"span":4,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"refId":"A","target":"aliasByNode(movingAverage(scaleToSeconds(apps.fakesite.*.counters.requests.count, 1), 2), 2)"}],"timeFrom":null,"timeShift":null,"timezone":"browser","title":"server requests","tooltip":{"msResolution":false,"query_as_alias":true,"shared":true,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"short","logBase":1,"max":null,"min":null,"show":true},{"format":"short","logBase":1,"max":null,"min":null,"show":true}],"zerofill":true},{"aliasColors":{"upper_25":"#F9E2D2","upper_50":"#F2C96D","upper_75":"#EAB839"},"annotate":{"enable":false},"bars":true,"datasource":"${DS_GRAPHITE}","editable":true,"fill":1,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":5,"interactive":true,"legend":{"alignAsTable":true,"avg":true,"current":false,"max":false,"min":false,"rightSide":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[],"span":4,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"refId":"A","target":"aliasByNode(summarize(statsd.fakesite.timers.ads_timer.*, \'4min\', \'avg\'), 4)"}],"timeFrom":null,"timeShift":null,"timezone":"browser","title":"client side full page load","tooltip":{"msResolution":false,"query_as_alias":true,"shared":false,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"ms","logBase":1,"max":null,"min":null,"show":true},{"format":"short","logBase":1,"max":null,"min":null,"show":true}],"zerofill":true},{"aliasColors":{"web_server_01":"#B7DBAB","web_server_02":"#7EB26D","web_server_03":"#508642","web_server_04":"#3F6833"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":8,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":14,"interactive":true,"legend":{"alignAsTable":false,"avg":false,"current":false,"max":false,"min":false,"rightSide":false,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[],"span":4,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"refId":"A","target":"aliasByNode(movingAverage(scaleToSeconds(apps.fakesite.*.counters.requests.count, 1), 2), 2)"}],"timeFrom":null,"timeShift":null,"timezone":"browser","title":"server requests","tooltip":{"msResolution":false,"query_as_alias":true,"shared":true,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"short","logBase":1,"max":null,"min":null,"show":true},{"format":"short","logBase":1,"max":null,"min":null,"show":true}],"zerofill":true}],"title":""},{"collapsable":true,"collapse":false,"editable":true,"height":"200px","notice":false,"panels":[{"aliasColors":{"cpu1":"#EF843C","cpu2":"#EAB839","upper_25":"#B7DBAB","upper_50":"#7EB26D","upper_75":"#629E51","upper_90":"#629E51","upper_95":"#508642"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":3,"grid":{"max":null,"min":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":6,"interactive":true,"legend":{"alignAsTable":true,"avg":true,"current":true,"legendSideLastValue":true,"max":false,"min":false,"rightSide":true,"show":false,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"links":[],"nullPointMode":"connected","options":false,"percentage":false,"pointradius":1,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[{"alias":"this is test of breaking","yaxis":1}],"span":12,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"refId":"A","target":"aliasByNode(statsd.fakesite.timers.ads_timer.*,4)"},{"refId":"B","target":"alias(scale(statsd.fakesite.timers.ads_timer.upper_95,-1),\'cpu1\')"},{"refId":"C","target":"alias(scale(statsd.fakesite.timers.ads_timer.upper_75,-1),\'cpu2\')"}],"timeFrom":null,"timeShift":null,"timezone":"browser","title":"","tooltip":{"msResolution":false,"query_as_alias":true,"shared":false,"value_type":"cumulative","sort":0},"transparent":true,"type":"graph","xaxis":{"show":false},"yaxes":[{"format":"ms","logBase":1,"max":null,"min":null,"show":false},{"format":"short","logBase":1,"max":null,"min":null,"show":false}],"zerofill":true}],"title":"test"}],"time":{"from":"now-30m","to":"now"},"timepicker":{"collapse":false,"enable":true,"notice":false,"now":true,"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"status":"Stable","time_options":["5m","15m","1h","2h"," 6h","12h","24h","2d","7d","30d"],"type":"timepicker"},"templating":{"enable":false,"list":[]},"annotations":{"enable":false,"list":[]},"refresh":false,"schemaVersion":12,"version":5,"links":[],"gnetId":null},"overwrite":true,"inputs":[{"name":"DS_GRAPHITE","type":"datasource","pluginId":"graphite","value":"graphite"}]}' --compressed + curl 'http://localhost:3000/api/dashboards/import' -H 'Pragma: no-cache' -H 'Origin: http://localhost:3000' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: en-US,en;q=0.8,sv;q=0.6' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.101 Safari/537.36' -H 'Content-Type: application/json;charset=UTF-8' -H 'Accept: application/json, text/plain, */*' -H 'Cache-Control: no-cache' -H 'Referer: http://localhost:3000/dashboard/new?editview=import' -H 'Cookie: grafana_sess=662a67f11b47e657; grafana_user=admin; grafana_remember=bd839923f24f648c7cb53ede6ff9ef40826204e9a22df8f9; toggles=%7B%7D' -H 'Connection: keep-alive' --data-binary $'{"dashboard":{"__inputs":[{"name":"DS_GRAPHITE","label":"graphite","description":"","type":"datasource","pluginId":"graphite","pluginName":"Graphite"}],"__requires":[{"type":"panel","id":"singlestat","name":"Singlestat","version":""},{"type":"panel","id":"graph","name":"Graph","version":""},{"type":"grafana","id":"grafana","name":"Grafana","version":"3.1.0"},{"type":"datasource","id":"graphite","name":"Graphite","version":"1.0.0"}],"id":null,"title":"Big Dashboard dashname '"$index"$'","tags":["startpage","home","presentation"],"style":"dark","timezone":"browser","editable":true,"hideControls":false,"sharedCrosshair":true,"rows":[{"collapse":false,"editable":true,"height":"100px","panels":[{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(245, 54, 54, 0.9)","rgba(237, 129, 40, 0.89)","rgba(50, 172, 45, 0.97)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":100,"minValue":0,"show":false,"thresholdLabels":false,"thresholdMarkers":true},"id":16,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":3,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":true},"targets":[{"refId":"A","target":"apps.backend.backend_02.counters.requests.count"}],"thresholds":"100,270","title":"Sign ups","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(245, 54, 54, 0.9)","rgba(237, 129, 40, 0.89)","rgba(50, 172, 45, 0.97)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":100,"minValue":0,"show":false,"thresholdLabels":false,"thresholdMarkers":true},"id":15,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":3,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":true},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.7)"}],"thresholds":"100,270","title":"Logins","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(245, 54, 54, 0.9)","rgba(237, 129, 40, 0.89)","rgba(50, 172, 45, 0.97)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":100,"minValue":0,"show":false,"thresholdLabels":false,"thresholdMarkers":true},"id":17,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":3,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":true},"targets":[{"refId":"A","target":"apps.backend.backend_04.counters.requests.count"}],"thresholds":"100,270","title":"Sign outs","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(245, 54, 54, 0.9)","rgba(237, 129, 40, 0.89)","rgba(50, 172, 45, 0.97)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":100,"minValue":0,"show":false,"thresholdLabels":false,"thresholdMarkers":true},"id":18,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":3,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":true},"targets":[{"refId":"A","target":"scale(apps.backend.backend_03.counters.requests.count, 0.3)"}],"thresholds":"100,270","title":"Support calls","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1}],"title":"New row"},{"collapse":false,"editable":true,"height":218.4375,"panels":[{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":20,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.7)"}],"thresholds":"200,270","title":"Logins","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":24,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.2)"}],"thresholds":"200,270","title":"Google hits","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"bytes","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":22,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.4)"}],"thresholds":"200,270","title":"Memory","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":21,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.8)"}],"thresholds":"200,270","title":"Logouts","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":26,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.2)"}],"thresholds":"200,270","title":"Google hits","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1},{"cacheTimeout":null,"colorBackground":false,"colorValue":true,"colors":["rgba(50, 172, 45, 0.97)","rgba(237, 129, 40, 0.89)","rgba(245, 54, 54, 0.9)"],"datasource":"${DS_GRAPHITE}","editable":true,"error":false,"format":"none","gauge":{"maxValue":300,"minValue":0,"show":true,"thresholdLabels":false,"thresholdMarkers":true},"id":25,"interval":null,"links":[],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","span":2,"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":true,"lineColor":"rgb(31, 120, 193)","show":false},"targets":[{"refId":"A","target":"scale(apps.backend.backend_01.counters.requests.count, 0.8)"}],"thresholds":"200,270","title":"Logouts","type":"singlestat","valueFontSize":"100%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg","mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"rangeMaps":[{"from":"null","to":"null","text":"N/A"}],"mappingType":1}],"title":"New row"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"cpu":"#E24D42","memory":"#6ED0E0","statsd.fakesite.counters.session_start.desktop.count":"#6ED0E0"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":3,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":4,"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[{"alias":"cpu","fill":0,"lines":true,"yaxis":2,"zindex":2},{"alias":"memory","pointradius":2,"points":true}],"span":4,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"hide":false,"refId":"A","target":"alias(movingAverage(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count, 10), 20), \'cpu\')"},{"refId":"B","target":"alias(statsd.fakesite.counters.session_start.desktop.count, \'memory\')"}],"timeFrom":null,"timeShift":null,"timezone":"browser","title":"Memory / CPU","tooltip":{"msResolution":false,"query_as_alias":true,"shared":false,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"bytes","logBase":1,"max":null,"min":null,"show":true},{"format":"percent","logBase":1,"max":null,"min":0,"show":true}],"zerofill":true},{"aliasColors":{"logins":"#7EB26D","logins (-1 day)":"#447EBC"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":1,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":3,"interactive":true,"legend":{"alignAsTable":false,"avg":false,"current":true,"max":true,"min":true,"rightSide":false,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":1,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[],"span":4,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"refId":"A","target":"alias(movingAverage(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count, 1), 2), \'logins\')"},{"refId":"B","target":"alias(movingAverage(timeShift(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count, 1), \'1h\'), 2), \'logins (-1 hour)\')"}],"timeFrom":null,"timeShift":"1h","timezone":"browser","title":"logins","tooltip":{"msResolution":false,"query_as_alias":true,"shared":false,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"short","logBase":1,"max":null,"min":null,"show":true},{"format":"short","logBase":1,"max":null,"min":null,"show":true}],"zerofill":true},{"aliasColors":{"cpu":"#E24D42","memory":"#6ED0E0","statsd.fakesite.counters.session_start.desktop.count":"#6ED0E0"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":3,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":19,"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[{"alias":"cpu","fill":0,"lines":true,"yaxis":2,"zindex":2},{"alias":"memory","pointradius":2,"points":true}],"span":4,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"hide":false,"refId":"A","target":"alias(movingAverage(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count, 10), 20), \'cpu\')"},{"refId":"B","target":"alias(statsd.fakesite.counters.session_start.desktop.count, \'memory\')"}],"timeFrom":null,"timeShift":"1h","timezone":"browser","title":"Memory / CPU","tooltip":{"msResolution":false,"query_as_alias":true,"shared":false,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"bytes","logBase":1,"max":null,"min":null,"show":true},{"format":"percent","logBase":1,"max":null,"min":0,"show":true}],"zerofill":true}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"300px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#B7DBAB","web_server_02":"#7EB26D","web_server_03":"#508642","web_server_04":"#3F6833"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":8,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":2,"interactive":true,"legend":{"alignAsTable":false,"avg":false,"current":false,"max":false,"min":false,"rightSide":false,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[],"span":4,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"refId":"A","target":"aliasByNode(movingAverage(scaleToSeconds(apps.fakesite.*.counters.requests.count, 1), 2), 2)"}],"timeFrom":null,"timeShift":null,"timezone":"browser","title":"server requests","tooltip":{"msResolution":false,"query_as_alias":true,"shared":true,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"short","logBase":1,"max":null,"min":null,"show":true},{"format":"short","logBase":1,"max":null,"min":null,"show":true}],"zerofill":true},{"aliasColors":{"upper_25":"#F9E2D2","upper_50":"#F2C96D","upper_75":"#EAB839"},"annotate":{"enable":false},"bars":true,"datasource":"${DS_GRAPHITE}","editable":true,"fill":1,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":5,"interactive":true,"legend":{"alignAsTable":true,"avg":true,"current":false,"max":false,"min":false,"rightSide":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[],"span":4,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"refId":"A","target":"aliasByNode(summarize(statsd.fakesite.timers.ads_timer.*, \'4min\', \'avg\'), 4)"}],"timeFrom":null,"timeShift":null,"timezone":"browser","title":"client side full page load","tooltip":{"msResolution":false,"query_as_alias":true,"shared":false,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"ms","logBase":1,"max":null,"min":null,"show":true},{"format":"short","logBase":1,"max":null,"min":null,"show":true}],"zerofill":true},{"aliasColors":{"web_server_01":"#B7DBAB","web_server_02":"#7EB26D","web_server_03":"#508642","web_server_04":"#3F6833"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":8,"grid":{"max":null,"min":0,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":14,"interactive":true,"legend":{"alignAsTable":false,"avg":false,"current":false,"max":false,"min":false,"rightSide":false,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[],"span":4,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"refId":"A","target":"aliasByNode(movingAverage(scaleToSeconds(apps.fakesite.*.counters.requests.count, 1), 2), 2)"}],"timeFrom":null,"timeShift":null,"timezone":"browser","title":"server requests","tooltip":{"msResolution":false,"query_as_alias":true,"shared":true,"value_type":"cumulative","sort":0},"type":"graph","xaxis":{"show":true},"yaxes":[{"format":"short","logBase":1,"max":null,"min":null,"show":true},{"format":"short","logBase":1,"max":null,"min":null,"show":true}],"zerofill":true}],"title":""},{"collapsable":true,"collapse":false,"editable":true,"height":"200px","notice":false,"panels":[{"aliasColors":{"cpu1":"#EF843C","cpu2":"#EAB839","upper_25":"#B7DBAB","upper_50":"#7EB26D","upper_75":"#629E51","upper_90":"#629E51","upper_95":"#508642"},"annotate":{"enable":false},"bars":false,"datasource":"${DS_GRAPHITE}","editable":true,"fill":3,"grid":{"max":null,"min":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":6,"interactive":true,"legend":{"alignAsTable":true,"avg":true,"current":true,"legendSideLastValue":true,"max":false,"min":false,"rightSide":true,"show":false,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"links":[],"nullPointMode":"connected","options":false,"percentage":false,"pointradius":1,"points":false,"renderer":"flot","resolution":100,"scale":1,"seriesOverrides":[{"alias":"this is test of breaking","yaxis":1}],"span":12,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"refId":"A","target":"aliasByNode(statsd.fakesite.timers.ads_timer.*,4)"},{"refId":"B","target":"alias(scale(statsd.fakesite.timers.ads_timer.upper_95,-1),\'cpu1\')"},{"refId":"C","target":"alias(scale(statsd.fakesite.timers.ads_timer.upper_75,-1),\'cpu2\')"}],"timeFrom":null,"timeShift":null,"timezone":"browser","title":"","tooltip":{"msResolution":false,"query_as_alias":true,"shared":false,"value_type":"cumulative","sort":0},"transparent":true,"type":"graph","xaxis":{"show":false},"yaxes":[{"format":"ms","logBase":1,"max":null,"min":null,"show":false},{"format":"short","logBase":1,"max":null,"min":null,"show":false}],"zerofill":true}],"title":"test"}],"time":{"from":"now-30m","to":"now"},"timepicker":{"collapse":false,"enable":true,"notice":false,"now":true,"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"status":"Stable","type":"timepicker"},"templating":{"enable":false,"list":[]},"annotations":{"enable":false,"list":[]},"refresh":false,"schemaVersion":12,"version":5,"links":[],"gnetId":null},"overwrite":true,"inputs":[{"name":"DS_GRAPHITE","type":"datasource","pluginId":"graphite","value":"graphite"}]}' --compressed done From 62e06cfac8096d7547d6e3cc7a28c6bcfdc626c8 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 12 Feb 2025 13:53:32 -0600 Subject: [PATCH 540/894] Actions: Fix support in `StateTimeline` and `XYChart` (#100543) Co-authored-by: Leon Sorokin --- .../plugins/panel/barchart/BarChartPanel.tsx | 4 +-- .../panel/candlestick/CandlestickPanel.tsx | 4 +-- .../state-timeline/StateTimelinePanel.tsx | 4 +-- .../state-timeline/StateTimelineTooltip2.tsx | 12 +++++--- .../status-history/StatusHistoryPanel.tsx | 4 +-- .../panel/timeseries/TimeSeriesPanel.tsx | 4 +-- public/app/plugins/panel/trend/TrendPanel.tsx | 4 +-- .../plugins/panel/xychart/XYChartPanel.tsx | 9 +++++- .../plugins/panel/xychart/XYChartTooltip.tsx | 29 ++++++++++++++----- 9 files changed, 49 insertions(+), 25 deletions(-) diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index dba432c57d0..424bbd16de0 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -157,8 +157,8 @@ export const BarChartPanel = (props: PanelProps) => { hoverMode={ options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll } - getDataLinks={(seriesIdx: number, dataIdx: number) => - vizSeries[0].fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + getDataLinks={(seriesIdx, dataIdx) => + vizSeries[0].fields[seriesIdx].getLinks?.({ valueRowIndex: dataIdx }) ?? [] } render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync, dataLinks) => { return ( diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index 4ed97d59456..16eeeae3ca6 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -282,8 +282,8 @@ export const CandlestickPanel = ({ clientZoom={true} syncMode={cursorSync} syncScope={eventsScope} - getDataLinks={(seriesIdx: number, dataIdx: number) => - alignedFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + getDataLinks={(seriesIdx, dataIdx) => + alignedFrame.fields[seriesIdx].getLinks?.({ valueRowIndex: dataIdx }) ?? [] } render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2, viaSync, dataLinks) => { if (enableAnnotationCreation && timeRange2 != null) { diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 9524beba351..7a23e4a6898 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -107,8 +107,8 @@ export const StateTimelinePanel = ({ queryZoom={onChangeTimeRange} syncMode={cursorSync} syncScope={eventsScope} - getDataLinks={(seriesIdx: number, dataIdx: number) => - alignedFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + getDataLinks={(seriesIdx, dataIdx) => + alignedFrame.fields[seriesIdx].getLinks?.({ valueRowIndex: dataIdx }) ?? [] } render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync, dataLinks) => { if (enableAnnotationCreation && timeRange2 != null) { diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx index a2f2665a11d..b400b9e6e73 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx @@ -68,12 +68,16 @@ export const StateTimelineTooltip2 = ({ let footer: ReactNode; - if (isPinned && seriesIdx != null) { + if (seriesIdx != null) { const field = series.fields[seriesIdx]; - const dataIdx = dataIdxs[seriesIdx]!; - const actions = getFieldActions(series, field, replaceVariables!, dataIdx); + const hasOneClickLink = dataLinks.some((dataLink) => dataLink.oneClick === true); - footer = ; + if (isPinned || hasOneClickLink) { + const dataIdx = dataIdxs[seriesIdx]!; + const actions = getFieldActions(series, field, replaceVariables!, dataIdx); + + footer = ; + } } const headerItem: VizTooltipItem = { diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 8ed1a266a27..1f410d1578c 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -119,8 +119,8 @@ export const StatusHistoryPanel = ({ queryZoom={onChangeTimeRange} syncMode={cursorSync} syncScope={eventsScope} - getDataLinks={(seriesIdx: number, dataIdx: number) => - alignedFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + getDataLinks={(seriesIdx, dataIdx) => + alignedFrame.fields[seriesIdx].getLinks?.({ valueRowIndex: dataIdx }) ?? [] } render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync, dataLinks) => { if (enableAnnotationCreation && timeRange2 != null) { diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index c83af4b2703..f8866b705f3 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -106,8 +106,8 @@ export const TimeSeriesPanel = ({ clientZoom={true} syncMode={cursorSync} syncScope={eventsScope} - getDataLinks={(seriesIdx: number, dataIdx: number) => - alignedFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + getDataLinks={(seriesIdx, dataIdx) => + alignedFrame.fields[seriesIdx].getLinks?.({ valueRowIndex: dataIdx }) ?? [] } render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2, viaSync, dataLinks) => { if (enableAnnotationCreation && timeRange2 != null) { diff --git a/public/app/plugins/panel/trend/TrendPanel.tsx b/public/app/plugins/panel/trend/TrendPanel.tsx index 67dcf81bb4e..1fe5ced2a49 100644 --- a/public/app/plugins/panel/trend/TrendPanel.tsx +++ b/public/app/plugins/panel/trend/TrendPanel.tsx @@ -119,8 +119,8 @@ export const TrendPanel = ({ hoverMode={ options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll } - getDataLinks={(seriesIdx: number, dataIdx: number) => - alignedDataFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? [] + getDataLinks={(seriesIdx, dataIdx) => + alignedDataFrame.fields[seriesIdx].getLinks?.({ valueRowIndex: dataIdx }) ?? [] } render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange, viaSync, dataLinks) => { return ( diff --git a/public/app/plugins/panel/xychart/XYChartPanel.tsx b/public/app/plugins/panel/xychart/XYChartPanel.tsx index f22fd8248e1..25ef452dc6b 100644 --- a/public/app/plugins/panel/xychart/XYChartPanel.tsx +++ b/public/app/plugins/panel/xychart/XYChartPanel.tsx @@ -17,6 +17,8 @@ import { import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { getDisplayValuesForCalcs } from '@grafana/ui/src/components/uPlot/utils'; +import { getDataLinks } from '../status-history/utils'; + import { XYChartTooltip } from './XYChartTooltip'; import { Options } from './panelcfg.gen'; import { prepConfig } from './scatter'; @@ -113,7 +115,11 @@ export const XYChartPanel2 = (props: Props2) => { { + getDataLinks={(seriesIdx, dataIdx) => { + const xySeries = series[seriesIdx - 1]; + return getDataLinks(xySeries.y.field, dataIdx); + }} + render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync, dataLinks) => { return ( { isPinned={isPinned} seriesIdx={seriesIdx!} replaceVariables={props.replaceVariables} + dataLinks={dataLinks} /> ); }} diff --git a/public/app/plugins/panel/xychart/XYChartTooltip.tsx b/public/app/plugins/panel/xychart/XYChartTooltip.tsx index abd3cb32bc2..08bd3903bad 100644 --- a/public/app/plugins/panel/xychart/XYChartTooltip.tsx +++ b/public/app/plugins/panel/xychart/XYChartTooltip.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; -import { DataFrame, InterpolateFunction } from '@grafana/data'; +import { DataFrame, InterpolateFunction, LinkModel } from '@grafana/data'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; @@ -8,7 +8,7 @@ import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizToolt import { VizTooltipWrapper } from '@grafana/ui/src/components/VizTooltip/VizTooltipWrapper'; import { ColorIndicator, VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; -import { getDataLinks, getFieldActions } from '../status-history/utils'; +import { getFieldActions } from '../status-history/utils'; import { XYSeries } from './types2'; import { fmt } from './utils'; @@ -21,6 +21,7 @@ export interface Props { data: DataFrame[]; xySeries: XYSeries[]; replaceVariables: InterpolateFunction; + dataLinks: LinkModel[]; } function stripSeriesName(fieldName: string, seriesName: string) { @@ -31,7 +32,16 @@ function stripSeriesName(fieldName: string, seriesName: string) { return fieldName; } -export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, xySeries, dismiss, isPinned, replaceVariables }: Props) => { +export const XYChartTooltip = ({ + dataIdxs, + seriesIdx, + data, + xySeries, + dismiss, + isPinned, + replaceVariables, + dataLinks, +}: Props) => { const rowIndex = dataIdxs.find((idx) => idx !== null)!; const series = xySeries[seriesIdx! - 1]; @@ -93,12 +103,15 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, xySeries, dismiss, i let footer: ReactNode; - if (isPinned && seriesIdx != null) { - const links = getDataLinks(yField, rowIndex); - const yFieldFrame = data.find((frame) => frame.fields.includes(yField))!; - const actions = getFieldActions(yFieldFrame, yField, replaceVariables, rowIndex); + if (seriesIdx != null) { + const hasOneClickLink = dataLinks?.some((dataLink) => dataLink.oneClick === true); - footer = ; + if (isPinned || hasOneClickLink) { + const yFieldFrame = data.find((frame) => frame.fields.includes(yField))!; + const actions = getFieldActions(yFieldFrame, yField, replaceVariables, rowIndex); + + footer = ; + } } return ( From f9c329bbd1cb5829a9a755449a61b1b4ef07d9f3 Mon Sep 17 00:00:00 2001 From: margotphelps <123196595+margotphelps@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:15:28 -0500 Subject: [PATCH 541/894] Docs: updated supported versions with 11.5 (#100548) Co-authored-by: Isabel Matwawana --- docs/sources/upgrade-guide/when-to-upgrade/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/upgrade-guide/when-to-upgrade/index.md b/docs/sources/upgrade-guide/when-to-upgrade/index.md index 052107861c4..31d6385ef9f 100644 --- a/docs/sources/upgrade-guide/when-to-upgrade/index.md +++ b/docs/sources/upgrade-guide/when-to-upgrade/index.md @@ -99,6 +99,7 @@ Here is an overview of projected version support through 2024: | 11.2 | August 2024 | May 2025 | | 11.3 | October 2024 | July 2025 | | 11.4 | December 2024 | September 2025 | +| 11.5 | January 2025 | October 2025 | {{< admonition type="note" >}} Grafana 9.5.x was the last supported minor for the 9.0 major release and is no longer supported as of July 2024. From 3dcd885644ab91381e4c270d432104a134f64868 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:14:52 -0600 Subject: [PATCH 542/894] Data links: Show oneClick option just for specific panels (#100298) Co-authored-by: Leon Sorokin --- .../grafana-data/src/field/overrides/processors.ts | 4 +++- packages/grafana-data/src/types/fieldOverrides.ts | 1 + .../DataLinksInlineEditor/DataLinksInlineEditor.tsx | 9 +++++++-- public/app/core/components/OptionsUI/links.tsx | 3 ++- public/app/core/components/OptionsUI/registry.tsx | 10 ++++++---- public/app/features/actions/ActionsInlineEditor.tsx | 9 +++++++-- public/app/plugins/panel/barchart/module.tsx | 8 ++++++++ .../panel/canvas/editor/element/DataLinksEditor.tsx | 2 +- public/app/plugins/panel/canvas/module.tsx | 8 ++++++++ public/app/plugins/panel/heatmap/module.tsx | 7 +++++++ public/app/plugins/panel/histogram/module.tsx | 5 +++++ public/app/plugins/panel/state-timeline/module.tsx | 8 ++++++++ public/app/plugins/panel/status-history/module.tsx | 8 ++++++++ public/app/plugins/panel/timeseries/config.ts | 8 ++++++++ public/app/plugins/panel/xychart/config.ts | 9 ++++++++- 15 files changed, 87 insertions(+), 12 deletions(-) diff --git a/packages/grafana-data/src/field/overrides/processors.ts b/packages/grafana-data/src/field/overrides/processors.ts index 8c8542fd6ff..e80a73de866 100644 --- a/packages/grafana-data/src/field/overrides/processors.ts +++ b/packages/grafana-data/src/field/overrides/processors.ts @@ -50,7 +50,9 @@ export interface SliderFieldConfigSettings { ariaLabelForHandle?: string; } -export interface DataLinksFieldConfigSettings {} +export interface DataLinksFieldConfigSettings { + showOneClick?: boolean; +} export const dataLinksOverrideProcessor = ( value: any, diff --git a/packages/grafana-data/src/types/fieldOverrides.ts b/packages/grafana-data/src/types/fieldOverrides.ts index d96f22190bd..f2283c5956e 100644 --- a/packages/grafana-data/src/types/fieldOverrides.ts +++ b/packages/grafana-data/src/types/fieldOverrides.ts @@ -144,6 +144,7 @@ export enum FieldConfigProperty { Thresholds = 'thresholds', Mappings = 'mappings', Links = 'links', + Actions = 'actions', Color = 'color', Filterable = 'filterable', } diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx index 99b15ea1697..2b5a6ee7fe1 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx @@ -9,7 +9,12 @@ type DataLinksInlineEditorProps = Omit, getSuggestions: () => VariableSuggestion[]; }; -export const DataLinksInlineEditor = ({ links, getSuggestions, showOneClick, ...rest }: DataLinksInlineEditorProps) => ( +export const DataLinksInlineEditor = ({ + links, + getSuggestions, + showOneClick = false, + ...rest +}: DataLinksInlineEditorProps) => ( type="link" items={links} {...rest}> {(item, index, onSave, onCancel) => ( )} diff --git a/public/app/core/components/OptionsUI/links.tsx b/public/app/core/components/OptionsUI/links.tsx index 1a73b41c477..2d144b40e6c 100644 --- a/public/app/core/components/OptionsUI/links.tsx +++ b/public/app/core/components/OptionsUI/links.tsx @@ -3,13 +3,14 @@ import { DataLinksInlineEditor } from '@grafana/ui'; type Props = StandardEditorProps; -export const DataLinksValueEditor = ({ value, onChange, context }: Props) => { +export const DataLinksValueEditor = ({ value, onChange, context, item }: Props) => { return ( (context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : [])} + showOneClick={item.settings?.showOneClick} /> ); }; diff --git a/public/app/core/components/OptionsUI/registry.tsx b/public/app/core/components/OptionsUI/registry.tsx index cf4e0450c18..519a0f0c338 100644 --- a/public/app/core/components/OptionsUI/registry.tsx +++ b/public/app/core/components/OptionsUI/registry.tsx @@ -27,6 +27,7 @@ import { FieldNamePickerConfigSettings, booleanOverrideProcessor, Action, + DataLinksFieldConfigSettings, } from '@grafana/data'; import { actionsOverrideProcessor } from '@grafana/data/src/field/overrides/processors'; import { config } from '@grafana/runtime'; @@ -350,7 +351,7 @@ export const getAllStandardFieldConfigs = () => { const dataLinksCategory = config.featureToggles.vizActions ? 'Data links and actions' : 'Data links'; - const links: FieldConfigPropertyItem = { + const links: FieldConfigPropertyItem = { id: 'links', path: 'links', name: 'Data links', @@ -358,14 +359,14 @@ export const getAllStandardFieldConfigs = () => { override: standardEditorsRegistry.get('links').editor, process: dataLinksOverrideProcessor, settings: { - placeholder: '-', + showOneClick: false, }, shouldApply: () => true, category: [dataLinksCategory], getItemsCount: (value) => (value ? value.length : 0), }; - const actions: FieldConfigPropertyItem = { + const actions: FieldConfigPropertyItem = { id: 'actions', path: 'actions', name: 'Actions', @@ -373,12 +374,13 @@ export const getAllStandardFieldConfigs = () => { override: standardEditorsRegistry.get('actions').editor, process: actionsOverrideProcessor, settings: { - placeholder: '-', + showOneClick: false, }, shouldApply: () => true, category: [dataLinksCategory], getItemsCount: (value) => (value ? value.length : 0), showIf: () => config.featureToggles.vizActions, + hideFromDefaults: true, }; const color: FieldConfigPropertyItem = { diff --git a/public/app/features/actions/ActionsInlineEditor.tsx b/public/app/features/actions/ActionsInlineEditor.tsx index 7cdb1ff34a4..9cc9550db1f 100644 --- a/public/app/features/actions/ActionsInlineEditor.tsx +++ b/public/app/features/actions/ActionsInlineEditor.tsx @@ -9,7 +9,12 @@ type DataLinksInlineEditorProps = Omit, ' getSuggestions: () => VariableSuggestion[]; }; -export const ActionsInlineEditor = ({ actions, getSuggestions, showOneClick, ...rest }: DataLinksInlineEditorProps) => ( +export const ActionsInlineEditor = ({ + actions, + getSuggestions, + showOneClick = false, + ...rest +}: DataLinksInlineEditorProps) => ( type="action" items={actions} {...rest}> {(item, index, onSave, onCancel) => ( )} diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index 5b5ae542531..c31c17f5a81 100644 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -32,6 +32,14 @@ export const plugin = new PanelPlugin(BarChartPanel) mode: FieldColorModeId.PaletteClassic, }, }, + [FieldConfigProperty.Links]: { + settings: { + showOneClick: true, + }, + }, + [FieldConfigProperty.Actions]: { + hideFromDefaults: false, + }, }, useCustomConfig: (builder) => { const cfg = defaultFieldConfig; diff --git a/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx b/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx index 9849b35d368..16af0cd074b 100644 --- a/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx @@ -20,7 +20,7 @@ export function DataLinksEditor({ value, onChange, item, context }: Props) { }} getSuggestions={() => (context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : [])} data={[]} - showOneClick={false} + showOneClick={true} /> ); } diff --git a/public/app/plugins/panel/canvas/module.tsx b/public/app/plugins/panel/canvas/module.tsx index 19b9ce04ded..9fd22ccc8c2 100644 --- a/public/app/plugins/panel/canvas/module.tsx +++ b/public/app/plugins/panel/canvas/module.tsx @@ -59,7 +59,15 @@ export const plugin = new PanelPlugin(CanvasPanel) }, }, [FieldConfigProperty.Links]: { + settings: { + showOneClick: false, + }, + }, + [FieldConfigProperty.Actions]: { hideFromDefaults: true, + settings: { + showOneClick: false, + }, }, }, }) diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index f710858e0ec..6ec45e27d9a 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -23,6 +23,13 @@ import { Options, defaultOptions, HeatmapColorMode, HeatmapColorScale } from './ export const plugin = new PanelPlugin(HeatmapPanel) .useFieldConfig({ disableStandardOptions: Object.values(FieldConfigProperty).filter((v) => v !== FieldConfigProperty.Links), + standardOptions: { + [FieldConfigProperty.Links]: { + settings: { + showOneClick: true, + }, + }, + }, useCustomConfig: (builder) => { builder.addCustomEditor({ id: 'scaleDistribution', diff --git a/public/app/plugins/panel/histogram/module.tsx b/public/app/plugins/panel/histogram/module.tsx index c73870cf739..3be418ceb87 100644 --- a/public/app/plugins/panel/histogram/module.tsx +++ b/public/app/plugins/panel/histogram/module.tsx @@ -81,6 +81,11 @@ export const plugin = new PanelPlugin(HistogramPanel) mode: FieldColorModeId.PaletteClassic, }, }, + [FieldConfigProperty.Links]: { + settings: { + showOneClick: true, + }, + }, }, useCustomConfig: (builder) => { const cfg = defaultFieldConfig; diff --git a/public/app/plugins/panel/state-timeline/module.tsx b/public/app/plugins/panel/state-timeline/module.tsx index 024d1e883ac..7d84934e74a 100644 --- a/public/app/plugins/panel/state-timeline/module.tsx +++ b/public/app/plugins/panel/state-timeline/module.tsx @@ -29,6 +29,14 @@ export const plugin = new PanelPlugin(StateTimelinePanel) mode: FieldColorModeId.ContinuousGrYlRd, }, }, + [FieldConfigProperty.Links]: { + settings: { + showOneClick: true, + }, + }, + [FieldConfigProperty.Actions]: { + hideFromDefaults: false, + }, }, useCustomConfig: (builder) => { builder diff --git a/public/app/plugins/panel/status-history/module.tsx b/public/app/plugins/panel/status-history/module.tsx index 722f8936199..d556c52ccc0 100644 --- a/public/app/plugins/panel/status-history/module.tsx +++ b/public/app/plugins/panel/status-history/module.tsx @@ -17,6 +17,14 @@ export const plugin = new PanelPlugin(StatusHistoryPanel) mode: FieldColorModeId.Thresholds, }, }, + [FieldConfigProperty.Links]: { + settings: { + showOneClick: true, + }, + }, + [FieldConfigProperty.Actions]: { + hideFromDefaults: false, + }, }, useCustomConfig: (builder) => { builder diff --git a/public/app/plugins/panel/timeseries/config.ts b/public/app/plugins/panel/timeseries/config.ts index f749b1f6e6c..39e6a283d01 100644 --- a/public/app/plugins/panel/timeseries/config.ts +++ b/public/app/plugins/panel/timeseries/config.ts @@ -59,6 +59,14 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig, isTime = true): SetFi mode: FieldColorModeId.PaletteClassic, }, }, + [FieldConfigProperty.Links]: { + settings: { + showOneClick: true, + }, + }, + [FieldConfigProperty.Actions]: { + hideFromDefaults: false, + }, }, useCustomConfig: (builder) => { builder diff --git a/public/app/plugins/panel/xychart/config.ts b/public/app/plugins/panel/xychart/config.ts index c3e958c8c12..bff3ffb8774 100644 --- a/public/app/plugins/panel/xychart/config.ts +++ b/public/app/plugins/panel/xychart/config.ts @@ -35,7 +35,6 @@ export function getScatterFieldConfig(cfg: FieldConfig): SetFieldConfigOptionsAr [FieldConfigProperty.DisplayName]: { hideFromDefaults: true, }, - // TODO: this still leaves Color series by: [ Last | Min | Max ] // because item.settings?.bySeriesSupport && colorMode.isByValue [FieldConfigProperty.Color]: { @@ -48,6 +47,14 @@ export function getScatterFieldConfig(cfg: FieldConfig): SetFieldConfigOptionsAr mode: FieldColorModeId.PaletteClassic, }, }, + [FieldConfigProperty.Links]: { + settings: { + showOneClick: true, + }, + }, + [FieldConfigProperty.Actions]: { + hideFromDefaults: false, + }, }, useCustomConfig: (builder) => { From a34e7e176dd2a7a8b86b39e9a1eae5c77c31716d Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Wed, 12 Feb 2025 15:22:18 -0700 Subject: [PATCH 543/894] Unified Storage: Sprinkles latency metric (#100542) * add sprinkles latency metric * fixes failing tests - forgot to register metric only once --- .../unified/resource/bleve_index_metrics.go | 35 +++++++++++++++++-- pkg/storage/unified/sql/server.go | 5 +++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/pkg/storage/unified/resource/bleve_index_metrics.go b/pkg/storage/unified/resource/bleve_index_metrics.go index caa0499bcb8..0cb52455cff 100644 --- a/pkg/storage/unified/resource/bleve_index_metrics.go +++ b/pkg/storage/unified/resource/bleve_index_metrics.go @@ -11,8 +11,9 @@ import ( ) var ( - onceIndex sync.Once - IndexMetrics *BleveIndexMetrics + onceIndex sync.Once + IndexMetrics *BleveIndexMetrics + SprinklesIndexMetrics *SprinklesMetrics ) type BleveIndexMetrics struct { @@ -28,8 +29,30 @@ type BleveIndexMetrics struct { IndexTenants *prometheus.CounterVec } +type SprinklesMetrics struct { + SprinklesLatency prometheus.Histogram +} + var IndexCreationBuckets = []float64{1, 5, 10, 25, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} +func NewSprinklesMetrics() *SprinklesMetrics { + onceIndex.Do(func() { + SprinklesIndexMetrics = &SprinklesMetrics{ + SprinklesLatency: prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: "index_server", + Name: "sprinkles_latency_seconds", + Help: "Time (in seconds) it takes until sprinkles are fetched", + Buckets: instrument.DefBuckets, + NativeHistogramBucketFactor: 1.1, // enable native histograms + NativeHistogramMaxBucketNumber: 160, + NativeHistogramMinResetDuration: time.Hour, + }), + } + }) + + return SprinklesIndexMetrics +} + func NewIndexMetrics(indexDir string, searchBackend SearchBackend) *BleveIndexMetrics { onceIndex.Do(func() { IndexMetrics = &BleveIndexMetrics{ @@ -79,6 +102,14 @@ func NewIndexMetrics(indexDir string, searchBackend SearchBackend) *BleveIndexMe return IndexMetrics } +func (s *SprinklesMetrics) Collect(ch chan<- prometheus.Metric) { + s.SprinklesLatency.Collect(ch) +} + +func (s *SprinklesMetrics) Describe(ch chan<- *prometheus.Desc) { + s.SprinklesLatency.Describe(ch) +} + func (s *BleveIndexMetrics) Collect(ch chan<- prometheus.Metric) { s.IndexLatency.Collect(ch) s.IndexCreationTime.Collect(ch) diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index 2c1f1ffd602..b2cade286f5 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -83,10 +83,15 @@ func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg, InitMinCount: cfg.IndexMinCount, } + // Register indexer metrics err = reg.Register(resource.NewIndexMetrics(cfg.IndexPath, opts.Search.Backend)) if err != nil { slog.Warn("Failed to register indexer metrics", "error", err) } + err = reg.Register(resource.NewSprinklesMetrics()) + if err != nil { + slog.Warn("Failed to register sprinkles metrics", "error", err) + } } rs, err := resource.NewResourceServer(opts) From 0a88cb528abb7ec823b0db315e64dd5b7162a697 Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Wed, 12 Feb 2025 16:51:58 -0600 Subject: [PATCH 544/894] Explore metrics: Show the native histogram banner once (#99857) * use local storage to show the native histogram banner has been loaded * remove banner logic from datatrail * set banner shown in local storage on closing the banner --- .../trails/banners/NativeHistogramBanner.test.tsx | 7 +++++++ .../trails/banners/NativeHistogramBanner.tsx | 12 +++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/public/app/features/trails/banners/NativeHistogramBanner.test.tsx b/public/app/features/trails/banners/NativeHistogramBanner.test.tsx index d5191037dae..23d4d4f21d1 100644 --- a/public/app/features/trails/banners/NativeHistogramBanner.test.tsx +++ b/public/app/features/trails/banners/NativeHistogramBanner.test.tsx @@ -51,4 +51,11 @@ describe('NativeHistogramBanner', () => { fireEvent.click(histogramButton); expect(mockTrail.publishEvent).toHaveBeenCalledWith(new MetricSelectedEvent('histogram1'), true); }); + + test('Set that the banner has been shown in local storage when a user closes the banner', () => { + render(); + // click the button with aria label "Close alert" + fireEvent.click(screen.getByLabelText('Close alert')); + expect(localStorage.getItem('nativeHistogramBanner')).toBe('true'); + }); }); diff --git a/public/app/features/trails/banners/NativeHistogramBanner.tsx b/public/app/features/trails/banners/NativeHistogramBanner.tsx index 68ca35c0c67..d32375e054e 100644 --- a/public/app/features/trails/banners/NativeHistogramBanner.tsx +++ b/public/app/features/trails/banners/NativeHistogramBanner.tsx @@ -21,7 +21,7 @@ export function NativeHistogramBanner(props: NativeHistogramInfoProps) { const [showHistogramExamples, setShowHistogramExamples] = useState(false); const styles = useStyles2(getStyles, 0); - if (!histogramsLoaded || nativeHistograms.length === 0 || !histogramMessage) { + if (bannerHasBeenShown() || !histogramsLoaded || nativeHistograms.length === 0 || !histogramMessage) { return null; } @@ -32,6 +32,8 @@ export function NativeHistogramBanner(props: NativeHistogramInfoProps) { title={'Native Histogram Support'} severity={'info'} onRemove={() => { + // when a user explicitly closes the banner, save that it has been closed in local storage to not show again + setBannerHasBeenShown(); setHistogramMessage(false); }} className={styles.banner} @@ -275,3 +277,11 @@ function getStyles(theme: GrafanaTheme2, _chromeHeaderHeight: number) { }), }; } + +export function setBannerHasBeenShown() { + localStorage.setItem('nativeHistogramBanner', 'true'); +} + +export function bannerHasBeenShown() { + return localStorage.getItem('nativeHistogramBanner') ?? false; +} From 2b2b19478a45bfaaea748fa04ad99121216072ed Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Thu, 13 Feb 2025 08:19:51 +0100 Subject: [PATCH 545/894] Dashboard V0->V1 Migration: Schema migration v41 (#100554) --- .../migration/schemaversion/migrations.go | 3 +- .../dashboard/migration/schemaversion/v41.go | 11 ++ .../migration/schemaversion/v41_test.go | 38 +++++ .../input/36.legend_normalization.json | 4 +- .../37.timeseries_table_display_mode.json | 4 +- .../input/38.transform_timeseries_table.json | 4 +- .../testdata/input/39.refresh_true.json | 4 +- .../input/40.time_picker_time_options.json | 136 ++++++++++++++++++ .../output/36.legend_normalization.37.json | 14 +- .../output/36.legend_normalization.38.json | 14 +- .../output/36.legend_normalization.39.json | 14 +- .../output/36.legend_normalization.40.json | 14 +- .../output/36.legend_normalization.41.json | 132 +++++++++++++++++ .../37.timeseries_table_display_mode.38.json | 14 +- .../37.timeseries_table_display_mode.39.json | 14 +- .../37.timeseries_table_display_mode.40.json | 14 +- ... 37.timeseries_table_display_mode.41.json} | 33 +++-- .../38.transform_timeseries_table.39.json | 14 +- .../38.transform_timeseries_table.40.json | 14 +- ... => 38.transform_timeseries_table.41.json} | 12 +- .../testdata/output/39.refresh_true.40.json | 14 +- ...h_true.39.json => 39.refresh_true.41.json} | 4 +- .../40.time_picker_time_options.41.json | 134 +++++++++++++++++ 23 files changed, 629 insertions(+), 30 deletions(-) create mode 100644 pkg/apis/dashboard/migration/schemaversion/v41.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/v41_test.go create mode 100644 pkg/apis/dashboard/migration/testdata/input/40.time_picker_time_options.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.41.json rename pkg/apis/dashboard/migration/testdata/output/{37.timeseries_table_display_mode.37.json => 37.timeseries_table_display_mode.41.json} (91%) rename pkg/apis/dashboard/migration/testdata/output/{38.transform_timeseries_table.38.json => 38.transform_timeseries_table.41.json} (95%) rename pkg/apis/dashboard/migration/testdata/output/{39.refresh_true.39.json => 39.refresh_true.41.json} (98%) create mode 100644 pkg/apis/dashboard/migration/testdata/output/40.time_picker_time_options.41.json diff --git a/pkg/apis/dashboard/migration/schemaversion/migrations.go b/pkg/apis/dashboard/migration/schemaversion/migrations.go index 6d3955c329f..ef46439a591 100644 --- a/pkg/apis/dashboard/migration/schemaversion/migrations.go +++ b/pkg/apis/dashboard/migration/schemaversion/migrations.go @@ -6,7 +6,7 @@ type SchemaVersionMigrationFunc func(map[string]interface{}) error const ( MINIUM_VERSION = 36 - LATEST_VERSION = 40 + LATEST_VERSION = 41 ) var Migrations = map[int]SchemaVersionMigrationFunc{ @@ -14,6 +14,7 @@ var Migrations = map[int]SchemaVersionMigrationFunc{ 38: V38, 39: V39, 40: V40, + 41: V41, } func GetSchemaVersion(dash map[string]interface{}) int { diff --git a/pkg/apis/dashboard/migration/schemaversion/v41.go b/pkg/apis/dashboard/migration/schemaversion/v41.go new file mode 100644 index 00000000000..1faafea8285 --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/v41.go @@ -0,0 +1,11 @@ +package schemaversion + +func V41(dash map[string]interface{}) error { + dash["schemaVersion"] = int(41) + if timepicker, ok := dash["timepicker"].(map[string]interface{}); ok { + // time_options is a legacy property that was not used since grafana version 5 + // therefore deprecating this property from the schema + delete(timepicker, "time_options") + } + return nil +} diff --git a/pkg/apis/dashboard/migration/schemaversion/v41_test.go b/pkg/apis/dashboard/migration/schemaversion/v41_test.go new file mode 100644 index 00000000000..d0f11d227f3 --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/v41_test.go @@ -0,0 +1,38 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" +) + +func TestV41(t *testing.T) { + tests := []migrationTestCase{ + { + name: "time_options is removed", + input: map[string]interface{}{ + "title": "Test Dashboard", + "timepicker": map[string]interface{}{ + "time_options": []string{"1m", "5m", "15m", "1h", "6h", "12h", "24h"}, + }, + }, + expected: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 41, + "timepicker": map[string]interface{}{}, + }, + }, + { + name: "timepicker is not set", + input: map[string]interface{}{ + "title": "Test Dashboard", + }, + expected: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 41, + }, + }, + } + + runMigrationTests(t, tests, schemaversion.V41) +} diff --git a/pkg/apis/dashboard/migration/testdata/input/36.legend_normalization.json b/pkg/apis/dashboard/migration/testdata/input/36.legend_normalization.json index 93ddfecb078..7776ccdf0cb 100644 --- a/pkg/apis/dashboard/migration/testdata/input/36.legend_normalization.json +++ b/pkg/apis/dashboard/migration/testdata/input/36.legend_normalization.json @@ -115,7 +115,9 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/input/37.timeseries_table_display_mode.json b/pkg/apis/dashboard/migration/testdata/input/37.timeseries_table_display_mode.json index e800450fd51..20fa6fc0371 100644 --- a/pkg/apis/dashboard/migration/testdata/input/37.timeseries_table_display_mode.json +++ b/pkg/apis/dashboard/migration/testdata/input/37.timeseries_table_display_mode.json @@ -352,7 +352,9 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/input/38.transform_timeseries_table.json b/pkg/apis/dashboard/migration/testdata/input/38.transform_timeseries_table.json index e5f08f17c69..9afbe33c607 100644 --- a/pkg/apis/dashboard/migration/testdata/input/38.transform_timeseries_table.json +++ b/pkg/apis/dashboard/migration/testdata/input/38.transform_timeseries_table.json @@ -145,7 +145,9 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json b/pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json index 4ea2be531f9..844bd81eb23 100644 --- a/pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json +++ b/pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json @@ -124,7 +124,9 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/input/40.time_picker_time_options.json b/pkg/apis/dashboard/migration/testdata/input/40.time_picker_time_options.json new file mode 100644 index 00000000000..5b6071de054 --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/input/40.time_picker_time_options.json @@ -0,0 +1,136 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "preload": false, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "", + "refresh": "", + "schemaVersion": 40 + } \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.37.json b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.37.json index 1e6e0484aad..27e61667230 100644 --- a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.37.json +++ b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.37.json @@ -124,7 +124,19 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.38.json b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.38.json index 14c5c32071f..de4220f3b61 100644 --- a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.38.json +++ b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.38.json @@ -124,7 +124,19 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.39.json b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.39.json index 5e3239e89fa..472dbdd70a7 100644 --- a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.39.json +++ b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.39.json @@ -124,7 +124,19 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.40.json b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.40.json index 91d625264b0..e097b1e8402 100644 --- a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.40.json +++ b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.40.json @@ -124,7 +124,19 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.41.json b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.41.json new file mode 100644 index 00000000000..fda6883a462 --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/36.legend_normalization.41.json @@ -0,0 +1,132 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": {}, + "title": "No Legend Config", + "type": "graph" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "displayMode": "list", + "showLegend": true + } + }, + "title": "Boolean Legend True" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Boolean Legend False" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Hidden DisplayMode" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "ShowLegend False" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "legend": { + "displayMode": "table", + "showLegend": true + } + }, + "title": "Visible Legend" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.38.json b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.38.json index 30f64b0bc8e..5a17d602e82 100644 --- a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.38.json +++ b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.38.json @@ -367,7 +367,19 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.39.json b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.39.json index 1173963dc58..dee3d62af57 100644 --- a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.39.json +++ b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.39.json @@ -367,7 +367,19 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.40.json b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.40.json index 843cc6284f7..d5ddc2a55a8 100644 --- a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.40.json +++ b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.40.json @@ -367,7 +367,19 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.37.json b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.41.json similarity index 91% rename from pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.37.json rename to pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.41.json index 4b7c2fa4572..2d1d083a874 100644 --- a/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.37.json +++ b/pkg/apis/dashboard/migration/testdata/output/37.timeseries_table_display_mode.41.json @@ -31,7 +31,10 @@ "mode": "palette-classic" }, "custom": { - "displayMode": "basic" + "cellOptions": { + "mode": "basic", + "type": "gauge" + } }, "mappings": [], "thresholds": { @@ -84,7 +87,10 @@ "mode": "palette-classic" }, "custom": { - "displayMode": "gradient-gauge" + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } }, "mappings": [], "thresholds": { @@ -137,7 +143,10 @@ "mode": "palette-classic" }, "custom": { - "displayMode": "lcd-gauge" + "cellOptions": { + "mode": "lcd", + "type": "gauge" + } }, "mappings": [], "thresholds": { @@ -190,7 +199,10 @@ "mode": "palette-classic" }, "custom": { - "displayMode": "color-background" + "cellOptions": { + "mode": "gradient", + "type": "color-background" + } }, "mappings": [], "thresholds": { @@ -243,7 +255,10 @@ "mode": "palette-classic" }, "custom": { - "displayMode": "color-background-solid" + "cellOptions": { + "mode": "basic", + "type": "color-background" + } }, "mappings": [], "thresholds": { @@ -296,7 +311,9 @@ "mode": "palette-classic" }, "custom": { - "displayMode": "some-other-mode" + "cellOptions": { + "type": "some-other-mode" + } }, "mappings": [], "thresholds": { @@ -340,8 +357,8 @@ } ], "preload": false, - "refresh": true, - "schemaVersion": 37, + "refresh": "", + "schemaVersion": 41, "tags": [], "templating": { "list": [] diff --git a/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.39.json b/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.39.json index 136a2fb9d40..19b3b5d79f8 100644 --- a/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.39.json +++ b/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.39.json @@ -147,7 +147,19 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.40.json b/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.40.json index 4217f847e8c..63b0959daa1 100644 --- a/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.40.json +++ b/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.40.json @@ -147,7 +147,19 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.38.json b/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.41.json similarity index 95% rename from pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.38.json rename to pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.41.json index 081cb14634f..be300a117c6 100644 --- a/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.38.json +++ b/pkg/apis/dashboard/migration/testdata/output/38.transform_timeseries_table.41.json @@ -124,9 +124,11 @@ { "id": "timeSeriesTable", "options": { - "refIdToStat": { - "A": "mean", - "B": "max" + "A": { + "stat": "mean" + }, + "B": { + "stat": "max" } } } @@ -135,8 +137,8 @@ } ], "preload": false, - "refresh": true, - "schemaVersion": 38, + "refresh": "", + "schemaVersion": 41, "tags": [], "templating": { "list": [] diff --git a/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json b/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json index a8c67a1e80d..5c4ad99f35c 100644 --- a/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json +++ b/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json @@ -126,7 +126,19 @@ "from": "now-6h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, "timezone": "utc", "title": "New dashboard", "version": 0, diff --git a/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.39.json b/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.41.json similarity index 98% rename from pkg/apis/dashboard/migration/testdata/output/39.refresh_true.39.json rename to pkg/apis/dashboard/migration/testdata/output/39.refresh_true.41.json index 6c0cb4d1974..8c6dfe8ba06 100644 --- a/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.39.json +++ b/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.41.json @@ -116,8 +116,8 @@ } ], "preload": false, - "refresh": true, - "schemaVersion": 39, + "refresh": "", + "schemaVersion": 41, "tags": [], "templating": { "list": [] diff --git a/pkg/apis/dashboard/migration/testdata/output/40.time_picker_time_options.41.json b/pkg/apis/dashboard/migration/testdata/output/40.time_picker_time_options.41.json new file mode 100644 index 00000000000..8c6dfe8ba06 --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/40.time_picker_time_options.41.json @@ -0,0 +1,134 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file From 2dee9ccbbcd0a01c2f0afcc89c5e0f5f03aa602a Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Thu, 13 Feb 2025 08:54:58 +0100 Subject: [PATCH 546/894] APIServer: Cancel forked context after handler returns (#100504) We currently cancel the context when the adapter function is done. We should wait for the entire handler we're wrapping to finish before cancelling our context. --- pkg/apiserver/endpoints/responsewriter/responsewriter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/apiserver/endpoints/responsewriter/responsewriter.go b/pkg/apiserver/endpoints/responsewriter/responsewriter.go index a076340d7bb..824cc05a17e 100644 --- a/pkg/apiserver/endpoints/responsewriter/responsewriter.go +++ b/pkg/apiserver/endpoints/responsewriter/responsewriter.go @@ -37,11 +37,12 @@ func WrapHandler(handler http.Handler) func(req *http.Request) (*http.Response, if err != nil { return nil, err } - defer cancel() + // The cancel happens in the goroutine we spawn, so as to not cancel it too early. req = req.WithContext(ctx) // returns a shallow copy, so we can't do it as part of the adapter. w := NewAdapter(req) go func() { + defer cancel() handler.ServeHTTP(w, req) if err := w.CloseWriter(); err != nil { klog.Errorf("error closing writer: %v", err) From df64dd076243e25808b4d6384b96d6ddb338e59a Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 13 Feb 2025 09:17:16 +0100 Subject: [PATCH 547/894] LibraryElements: Propagate service identity in context when searching for dashboards (#100220) * Propagate service identity in context when searching for dashboards --- pkg/services/libraryelements/database.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 306352fce84..d133c36e650 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -250,14 +250,17 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn return err } - // then find the dashboards that were supposed to be connected to this element - _, requester := identity.WithServiceIdentity(c, signedInUser.GetOrgID()) - dashs, err := l.dashboardsService.FindDashboards(c, &dashboards.FindPersistedDashboardsQuery{ + // then find the dashboards that were supposed to be connected to this element. + // A identity may be able to delete a library element but not read all dashboards so we fetch then as the + // service user so we can prevent deletion of those connections + serviceCtx, serviceIdent := identity.WithServiceIdentity(c, signedInUser.GetOrgID()) + dashs, err := l.dashboardsService.FindDashboards(serviceCtx, &dashboards.FindPersistedDashboardsQuery{ Type: searchstore.TypeDashboard, - OrgId: signedInUser.GetOrgID(), + OrgId: serviceIdent.GetOrgID(), DashboardIds: dashboardIDs, - SignedInUser: requester, // a user may be able to delete a library element but not read all dashboards. We still need to run this check, so we don't allow deleting elements if dashboards are connected + SignedInUser: serviceIdent, }) + if err != nil { return err } From fbf96916aa23a83346c3955c38914032300b9a29 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 13 Feb 2025 09:20:45 +0100 Subject: [PATCH 548/894] Alerting: Use alerting-specific error boundary for page components (#99980) Use alerting-specific error boundary for page components --- .../ErrorBoundary/ErrorBoundary.tsx | 15 ++++-- .../features/alerting/unified/AlertGroups.tsx | 15 +++--- .../alerting/unified/AlertingNotEnabled.tsx | 6 ++- .../alerting/unified/NewSilencePage.tsx | 5 +- .../unified/NotificationPoliciesPage.tsx | 17 +++--- .../alerting/unified/RedirectToRuleViewer.tsx | 5 +- .../features/alerting/unified/RuleList.tsx | 3 +- .../features/alerting/unified/RuleViewer.tsx | 5 +- .../features/alerting/unified/Settings.tsx | 5 +- .../features/alerting/unified/Templates.tsx | 41 +++++++------- .../contact-points/ContactPoints.tsx | 4 +- .../DuplicateMessageTemplate.tsx | 14 ++++- .../contact-points/EditContactPoint.tsx | 5 +- .../contact-points/EditMessageTemplate.tsx | 14 ++++- .../contact-points/NewMessageTemplate.tsx | 20 +++---- .../components/GlobalConfig.tsx | 11 ++-- .../export/ExportNewGrafanaRule.tsx | 21 ++------ .../components/export/GrafanaModifyExport.tsx | 54 ++++++++----------- .../mute-timings/EditMuteTiming.tsx | 27 +++++----- .../components/mute-timings/NewMuteTiming.tsx | 28 +++++----- .../components/receivers/NewReceiverView.tsx | 4 +- .../CentralAlertHistoryPage.tsx | 10 ++-- .../components/silences/SilencesEditor.tsx | 5 +- .../components/silences/SilencesTable.tsx | 5 +- .../features/alerting/unified/home/Home.tsx | 5 +- .../unified/rule-editor/RuleEditor.tsx | 6 ++- .../unified/rule-list/RuleList.v1.tsx | 4 +- .../unified/rule-list/RuleList.v2.tsx | 39 +++++++------- .../unified/withPageErrorBoundary.tsx | 25 +++++++++ 29 files changed, 238 insertions(+), 180 deletions(-) create mode 100644 public/app/features/alerting/unified/withPageErrorBoundary.tsx diff --git a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx index 312d6e76029..8c50da47478 100644 --- a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -21,6 +21,8 @@ interface Props { onError?: (error: Error) => void; /** Callback error state is cleared due to recover props change */ onRecover?: () => void; + /** Default error logger - Faro by default */ + errorLogger?: (error: Error) => void; } interface State { @@ -35,7 +37,12 @@ export class ErrorBoundary extends PureComponent { }; componentDidCatch(error: Error, errorInfo: ErrorInfo) { - faro?.api?.pushError(error); + const logger = this.props.errorLogger ?? faro?.api?.pushError; + + if (logger) { + logger(error); + } + this.setState({ error, errorInfo }); if (this.props.onError) { @@ -89,6 +96,8 @@ export interface ErrorBoundaryAlertProps { /** Will re-render children after error if recover values changes */ dependencies?: unknown[]; + /** Default error logger - Faro by default */ + errorLogger?: (error: Error) => void; } export class ErrorBoundaryAlert extends PureComponent { @@ -98,10 +107,10 @@ export class ErrorBoundaryAlert extends PureComponent { }; render() { - const { title, children, style, dependencies } = this.props; + const { title, children, style, dependencies, errorLogger } = this.props; return ( - + {({ error, errorInfo }) => { if (!errorInfo) { return children; diff --git a/public/app/features/alerting/unified/AlertGroups.tsx b/public/app/features/alerting/unified/AlertGroups.tsx index 3422a093990..2d5863f7aea 100644 --- a/public/app/features/alerting/unified/AlertGroups.tsx +++ b/public/app/features/alerting/unified/AlertGroups.tsx @@ -19,6 +19,7 @@ import { NOTIFICATIONS_POLL_INTERVAL_MS } from './utils/constants'; import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; import { getFiltersFromUrlParams } from './utils/misc'; import { initialAsyncRequestState } from './utils/redux'; +import { withPageErrorBoundary } from './withPageErrorBoundary'; const AlertGroups = () => { const { selectedAlertmanager } = useAlertmanager(); @@ -89,10 +90,12 @@ const AlertGroups = () => { ); }; -const AlertGroupsPage = () => ( - - - -); +function AlertGroupsPage() { + return ( + + + + ); +} -export default AlertGroupsPage; +export default withPageErrorBoundary(AlertGroupsPage); diff --git a/public/app/features/alerting/unified/AlertingNotEnabled.tsx b/public/app/features/alerting/unified/AlertingNotEnabled.tsx index ca8a10aa487..9250fafed2c 100644 --- a/public/app/features/alerting/unified/AlertingNotEnabled.tsx +++ b/public/app/features/alerting/unified/AlertingNotEnabled.tsx @@ -1,7 +1,9 @@ import { NavModel } from '@grafana/data'; import { Page } from 'app/core/components/Page/Page'; -export default function FeatureTogglePage() { +import { withPageErrorBoundary } from './withPageErrorBoundary'; + +function FeatureTogglePage() { const navModel: NavModel = { node: { text: 'Alerting is not enabled', @@ -25,3 +27,5 @@ enabled = true ); } + +export default withPageErrorBoundary(FeatureTogglePage); diff --git a/public/app/features/alerting/unified/NewSilencePage.tsx b/public/app/features/alerting/unified/NewSilencePage.tsx index 859b9c3875e..2f2dc4e79cd 100644 --- a/public/app/features/alerting/unified/NewSilencePage.tsx +++ b/public/app/features/alerting/unified/NewSilencePage.tsx @@ -1,6 +1,5 @@ import { useLocation } from 'react-router-dom-v5-compat'; -import { withErrorBoundary } from '@grafana/ui'; import { defaultsFromQuery, getDefaultSilenceFormValues, @@ -12,6 +11,7 @@ import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning'; import { SilencesEditor } from './components/silences/SilencesEditor'; import { useAlertmanager } from './state/AlertmanagerContext'; +import { withPageErrorBoundary } from './withPageErrorBoundary'; const SilencesEditorComponent = () => { const location = useLocation(); @@ -48,4 +48,5 @@ function NewSilencePage() { ); } -export default withErrorBoundary(NewSilencePage, { style: 'page' }); + +export default withPageErrorBoundary(NewSilencePage); diff --git a/public/app/features/alerting/unified/NotificationPoliciesPage.tsx b/public/app/features/alerting/unified/NotificationPoliciesPage.tsx index 6b4d1b9b5e7..25d2ef8cbf8 100644 --- a/public/app/features/alerting/unified/NotificationPoliciesPage.tsx +++ b/public/app/features/alerting/unified/NotificationPoliciesPage.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { useState } from 'react'; import { GrafanaTheme2, UrlQueryMap } from '@grafana/data'; -import { Tab, TabContent, TabsBar, useStyles2, withErrorBoundary } from '@grafana/ui'; +import { Tab, TabContent, TabsBar, useStyles2 } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useMuteTimings } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings'; import { NotificationPoliciesList } from 'app/features/alerting/unified/components/notification-policies/NotificationPoliciesList'; @@ -12,6 +12,7 @@ import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning'; import { MuteTimingsTable } from './components/mute-timings/MuteTimingsTable'; import { useAlertmanager } from './state/AlertmanagerContext'; +import { withPageErrorBoundary } from './withPageErrorBoundary'; enum ActiveTab { NotificationPolicies = 'notification_policies', @@ -104,10 +105,12 @@ function getActiveTabFromUrl(queryParams: UrlQueryMap, defaultTab: ActiveTab): Q }; } -const NotificationPoliciesPage = () => ( - - - -); +function NotificationPoliciesPage() { + return ( + + + + ); +} -export default withErrorBoundary(NotificationPoliciesPage, { style: 'page' }); +export default withPageErrorBoundary(NotificationPoliciesPage); diff --git a/public/app/features/alerting/unified/RedirectToRuleViewer.tsx b/public/app/features/alerting/unified/RedirectToRuleViewer.tsx index e5375ad9133..bb032b5a9fd 100644 --- a/public/app/features/alerting/unified/RedirectToRuleViewer.tsx +++ b/public/app/features/alerting/unified/RedirectToRuleViewer.tsx @@ -5,7 +5,7 @@ import { useLocation } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; import { config, isFetchError } from '@grafana/runtime'; -import { Alert, Card, Icon, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui'; +import { Alert, Card, Icon, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { AlertLabels } from './components/AlertLabels'; import { RuleViewerLayout } from './components/rule-viewer/RuleViewerLayout'; @@ -13,6 +13,7 @@ import { useCloudCombinedRulesMatching } from './hooks/useCombinedRule'; import { getRulesSourceByName } from './utils/datasource'; import { createViewLink } from './utils/misc'; import { unescapePathSeparators } from './utils/rule-id'; +import { withPageErrorBoundary } from './withPageErrorBoundary'; const pageTitle = 'Find rule'; const subUrl = config.appSubUrl; @@ -153,4 +154,4 @@ function getStyles(theme: GrafanaTheme2) { }; } -export default withErrorBoundary(RedirectToRuleViewer, { style: 'page' }); +export default withPageErrorBoundary(RedirectToRuleViewer); diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index 9c38d4bf049..f51f440bd14 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -3,6 +3,7 @@ import { Suspense, lazy } from 'react'; import { config } from '@grafana/runtime'; import RuleListV1 from './rule-list/RuleList.v1'; +import { withPageErrorBoundary } from './withPageErrorBoundary'; const RuleListV2 = lazy(() => import('./rule-list/RuleList.v2')); const RuleList = () => { @@ -11,4 +12,4 @@ const RuleList = () => { return {newView ? : }; }; -export default RuleList; +export default withPageErrorBoundary(RuleList); diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 9584539e41b..9df84a41b18 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom-v5-compat'; import { NavModelItem } from '@grafana/data'; import { isFetchError } from '@grafana/runtime'; -import { Alert, withErrorBoundary } from '@grafana/ui'; +import { Alert } from '@grafana/ui'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; @@ -12,6 +12,7 @@ import DetailView, { ActiveTab, useActiveTab } from './components/rule-viewer/Ru import { useCombinedRule } from './hooks/useCombinedRule'; import { stringifyErrorLike } from './utils/misc'; import { getRuleIdFromPathname, parse as parseRuleId } from './utils/rule-id'; +import { withPageErrorBoundary } from './withPageErrorBoundary'; const RuleViewer = (): JSX.Element => { const params = useParams(); @@ -86,4 +87,4 @@ function ErrorMessage({ error }: ErrorMessageProps) { return {stringifyErrorLike(error)}; } -export default withErrorBoundary(RuleViewer, { style: 'page' }); +export default withPageErrorBoundary(RuleViewer); diff --git a/public/app/features/alerting/unified/Settings.tsx b/public/app/features/alerting/unified/Settings.tsx index 099bde572b0..7d3a91dafdf 100644 --- a/public/app/features/alerting/unified/Settings.tsx +++ b/public/app/features/alerting/unified/Settings.tsx @@ -6,8 +6,9 @@ import { useEditConfigurationDrawer } from './components/settings/ConfigurationD import { ExternalAlertmanagers } from './components/settings/ExternalAlertmanagers'; import InternalAlertmanager from './components/settings/InternalAlertmanager'; import { SettingsProvider, useSettings } from './components/settings/SettingsContext'; +import { withPageErrorBoundary } from './withPageErrorBoundary'; -export default function SettingsPage() { +function SettingsPage() { return ( @@ -47,3 +48,5 @@ function SettingsContent() { ); } + +export default withPageErrorBoundary(SettingsPage); diff --git a/public/app/features/alerting/unified/Templates.tsx b/public/app/features/alerting/unified/Templates.tsx index d32ea0541b4..f3b8b011f36 100644 --- a/public/app/features/alerting/unified/Templates.tsx +++ b/public/app/features/alerting/unified/Templates.tsx @@ -1,28 +1,29 @@ import { Route, Routes } from 'react-router-dom-v5-compat'; -import { withErrorBoundary } from '@grafana/ui'; - 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'; +import { withPageErrorBoundary } from './withPageErrorBoundary'; -const NotificationTemplates = (): JSX.Element => ( - - - } /> - } /> - } /> - - -); +function NotificationTemplates() { + return ( + + + } /> + } /> + } /> + + + ); +} -export default withErrorBoundary(NotificationTemplates, { style: 'page' }); +export default withPageErrorBoundary(NotificationTemplates); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx index 5ea42c97584..0d2f6cf447e 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx @@ -12,7 +12,6 @@ import { TabContent, TabsBar, Text, - withErrorBoundary, } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { Trans, t } from 'app/core/internationalization'; @@ -25,6 +24,7 @@ import { usePagination } from '../../hooks/usePagination'; import { useURLSearchParams } from '../../hooks/useURLSearchParams'; import { useAlertmanager } from '../../state/AlertmanagerContext'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning'; @@ -270,4 +270,4 @@ function ContactPointsPage() { ); } -export default withErrorBoundary(ContactPointsPage, { style: 'page' }); +export default withPageErrorBoundary(ContactPointsPage); 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 3ded1052b5d..bc89effea98 100644 --- a/public/app/features/alerting/unified/components/contact-points/DuplicateMessageTemplate.tsx +++ b/public/app/features/alerting/unified/components/contact-points/DuplicateMessageTemplate.tsx @@ -8,13 +8,15 @@ import { useAlertmanager } from '../../state/AlertmanagerContext'; import { generateCopiedName } from '../../utils/duplicate'; import { stringifyErrorLike } from '../../utils/misc'; import { updateDefinesWithUniqueValue } from '../../utils/templates'; +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; +import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { TemplateForm } from '../receivers/TemplateForm'; import { useGetNotificationTemplate, useNotificationTemplates } from './useNotificationTemplates'; const notFoundComponent = ; -const DuplicateMessageTemplate = () => { +const DuplicateMessageTemplateComponent = () => { const { selectedAlertmanager } = useAlertmanager(); const { name } = useParams<{ name: string }>(); const templateUid = name ? decodeURIComponent(name) : undefined; @@ -63,4 +65,12 @@ const DuplicateMessageTemplate = () => { ); }; -export default DuplicateMessageTemplate; +function DuplicateMessageTemplate() { + return ( + + + + ); +} + +export default withPageErrorBoundary(DuplicateMessageTemplate); diff --git a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.tsx b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.tsx index 96a00fc1d33..5e9deb4b3da 100644 --- a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.tsx +++ b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.tsx @@ -1,10 +1,11 @@ import { useParams } from 'react-router-dom-v5-compat'; -import { Alert, LoadingPlaceholder, withErrorBoundary } from '@grafana/ui'; +import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { useGetContactPoint } from 'app/features/alerting/unified/components/contact-points/useContactPoints'; import { stringifyErrorLike } from 'app/features/alerting/unified/utils/misc'; import { useAlertmanager } from '../../state/AlertmanagerContext'; +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { EditReceiverView } from '../receivers/EditReceiverView'; @@ -50,4 +51,4 @@ function EditContactPointPage() { ); } -export default withErrorBoundary(EditContactPointPage, { style: 'page' }); +export default withPageErrorBoundary(EditContactPointPage); 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 6c7df2a7fad..926e1c5404b 100644 --- a/public/app/features/alerting/unified/components/contact-points/EditMessageTemplate.tsx +++ b/public/app/features/alerting/unified/components/contact-points/EditMessageTemplate.tsx @@ -6,13 +6,15 @@ import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound' import { isNotFoundError } from '../../api/util'; import { useAlertmanager } from '../../state/AlertmanagerContext'; import { stringifyErrorLike } from '../../utils/misc'; +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; +import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { TemplateForm } from '../receivers/TemplateForm'; import { useGetNotificationTemplate } from './useNotificationTemplates'; const notFoundComponent = ; -const EditMessageTemplate = () => { +const EditMessageTemplateComponent = () => { const { name } = useParams<{ name: string }>(); const templateUid = name ? decodeURIComponent(name) : undefined; @@ -47,4 +49,12 @@ const EditMessageTemplate = () => { return ; }; -export default EditMessageTemplate; +function EditMessageTemplate() { + return ( + + + + ); +} + +export default withPageErrorBoundary(EditMessageTemplate); 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 e11ac531390..7cdd2286be1 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,16 @@ -import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; - import { useAlertmanager } from '../../state/AlertmanagerContext'; +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; +import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { TemplateForm } from '../receivers/TemplateForm'; -const NewMessageTemplate = () => { +function NewMessageTemplate() { const { selectedAlertmanager } = useAlertmanager(); - if (!selectedAlertmanager) { - return ; - } + return ( + + + + ); +} - return ; -}; - -export default NewMessageTemplate; +export default withPageErrorBoundary(NewMessageTemplate); diff --git a/public/app/features/alerting/unified/components/contact-points/components/GlobalConfig.tsx b/public/app/features/alerting/unified/components/contact-points/components/GlobalConfig.tsx index e5514b01d66..42ec581c529 100644 --- a/public/app/features/alerting/unified/components/contact-points/components/GlobalConfig.tsx +++ b/public/app/features/alerting/unified/components/contact-points/components/GlobalConfig.tsx @@ -1,11 +1,12 @@ -import { Alert, withErrorBoundary } from '@grafana/ui'; +import { Alert } from '@grafana/ui'; import { useAlertmanagerConfig } from '../../../hooks/useAlertmanagerConfig'; import { useAlertmanager } from '../../../state/AlertmanagerContext'; +import { withPageErrorBoundary } from '../../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../../AlertingPageWrapper'; import { GlobalConfigForm } from '../../receivers/GlobalConfigForm'; -const NewMessageTemplate = () => { +const GlobalConfig = () => { const { selectedAlertmanager } = useAlertmanager(); const { data, isLoading, error } = useAlertmanagerConfig(selectedAlertmanager); @@ -28,12 +29,12 @@ const NewMessageTemplate = () => { return ; }; -function NewMessageTemplatePage() { +function GlobalConfigPage() { return ( - + ); } -export default withErrorBoundary(NewMessageTemplatePage, { style: 'page' }); +export default withPageErrorBoundary(GlobalConfigPage); diff --git a/public/app/features/alerting/unified/components/export/ExportNewGrafanaRule.tsx b/public/app/features/alerting/unified/components/export/ExportNewGrafanaRule.tsx index 2e99bc0dc41..614f74eaa2a 100644 --- a/public/app/features/alerting/unified/components/export/ExportNewGrafanaRule.tsx +++ b/public/app/features/alerting/unified/components/export/ExportNewGrafanaRule.tsx @@ -1,21 +1,8 @@ -import * as React from 'react'; - +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertingPageWrapper } from '../AlertingPageWrapper'; import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm'; -export default function ExportNewGrafanaRule() { - return ( - - - - ); -} - -interface ExportNewGrafanaRuleWrapperProps { - children: React.ReactNode; -} - -function ExportNewGrafanaRuleWrapper({ children }: ExportNewGrafanaRuleWrapperProps) { +function ExportNewGrafanaRulePage() { return ( - {children} + ); } + +export default withPageErrorBoundary(ExportNewGrafanaRulePage); diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx index 5aca1d77824..1c4241fe519 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx @@ -1,4 +1,3 @@ -import * as React from 'react'; import { useMemo } from 'react'; import { useParams } from 'react-router-dom-v5-compat'; @@ -12,10 +11,11 @@ import { stringifyErrorLike } from '../../utils/misc'; import * as ruleId from '../../utils/rule-id'; import { isGrafanaRulerRule } from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertingPageWrapper } from '../AlertingPageWrapper'; import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm'; -export default function GrafanaModifyExport() { +function GrafanaModifyExport() { const { id } = useParams(); const ruleIdentifier = useMemo(() => { return ruleId.tryParse(id, true); @@ -23,38 +23,13 @@ export default function GrafanaModifyExport() { if (!ruleIdentifier) { return ( - - - The rule UID in the page URL is invalid. Please check the URL and try again. - - + + The rule UID in the page URL is invalid. Please check the URL and try again. + ); } - return ( - - - - ); -} - -interface ModifyExportWrapperProps { - children: React.ReactNode; -} - -function ModifyExportWrapper({ children }: ModifyExportWrapperProps) { - return ( - - {children} - - ); + return ; } function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }) { @@ -105,3 +80,20 @@ function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier } return ; } + +function GrafanaModifyExportPage() { + return ( + + + + ); +} + +export default withPageErrorBoundary(GrafanaModifyExportPage); diff --git a/public/app/features/alerting/unified/components/mute-timings/EditMuteTiming.tsx b/public/app/features/alerting/unified/components/mute-timings/EditMuteTiming.tsx index f73b1a1d1af..f697b935408 100644 --- a/public/app/features/alerting/unified/components/mute-timings/EditMuteTiming.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/EditMuteTiming.tsx @@ -1,10 +1,10 @@ import { Navigate } from 'react-router-dom-v5-compat'; -import { withErrorBoundary } from '@grafana/ui'; import { useGetMuteTiming } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings'; import { useURLSearchParams } from 'app/features/alerting/unified/hooks/useURLSearchParams'; import { useAlertmanager } from '../../state/AlertmanagerContext'; +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import MuteTimingForm from './MuteTimingForm'; @@ -38,17 +38,16 @@ const EditTimingRoute = () => { ); }; -const EditMuteTimingPage = () => ( - - - -); +function EditMuteTimingPage() { + return ( + + + + ); +} -export default withErrorBoundary(EditMuteTimingPage, { style: 'page' }); +export default withPageErrorBoundary(EditMuteTimingPage); diff --git a/public/app/features/alerting/unified/components/mute-timings/NewMuteTiming.tsx b/public/app/features/alerting/unified/components/mute-timings/NewMuteTiming.tsx index b73dda0e94f..c60e6d0f990 100644 --- a/public/app/features/alerting/unified/components/mute-timings/NewMuteTiming.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/NewMuteTiming.tsx @@ -1,20 +1,18 @@ -import { withErrorBoundary } from '@grafana/ui'; - +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import MuteTimingForm from './MuteTimingForm'; -const NewMuteTimingPage = () => ( - - - -); +function NewMuteTimingPage() { + return ( + + + + ); +} -export default withErrorBoundary(NewMuteTimingPage, { style: 'page' }); +export default withPageErrorBoundary(NewMuteTimingPage); diff --git a/public/app/features/alerting/unified/components/receivers/NewReceiverView.tsx b/public/app/features/alerting/unified/components/receivers/NewReceiverView.tsx index 4f2a3322da4..9258dddfc89 100644 --- a/public/app/features/alerting/unified/components/receivers/NewReceiverView.tsx +++ b/public/app/features/alerting/unified/components/receivers/NewReceiverView.tsx @@ -1,7 +1,7 @@ -import { withErrorBoundary } from '@grafana/ui'; import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { CloudReceiverForm } from './form/CloudReceiverForm'; @@ -24,4 +24,4 @@ function NewReceiverViewPage() { ); } -export default withErrorBoundary(NewReceiverViewPage, { style: 'page' }); +export default withPageErrorBoundary(NewReceiverViewPage); diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage.tsx index eb767a1df2f..91463309afd 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage.tsx +++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage.tsx @@ -1,14 +1,14 @@ -import { withErrorBoundary } from '@grafana/ui'; - +import { withPageErrorBoundary } from '../../../withPageErrorBoundary'; import { AlertingPageWrapper } from '../../AlertingPageWrapper'; import { CentralAlertHistoryScene } from './CentralAlertHistoryScene'; -const HistoryPage = () => { +function HistoryPage() { return ( ); -}; -export default withErrorBoundary(HistoryPage, { style: 'page' }); +} + +export default withPageErrorBoundary(HistoryPage); diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx index c72cb1e5521..2a1c1be335c 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx @@ -25,7 +25,6 @@ import { Stack, TextArea, useStyles2, - withErrorBoundary, } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import { SilenceCreatedResponse, alertSilencesApi } from 'app/features/alerting/unified/api/alertSilencesApi'; @@ -38,6 +37,7 @@ import { useAlertmanager } from '../../state/AlertmanagerContext'; import { SilenceFormFields } from '../../types/silence-form'; import { matcherFieldToMatcher } from '../../utils/alertmanager'; import { makeAMLink } from '../../utils/misc'; +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning'; @@ -296,4 +296,5 @@ function ExistingSilenceEditorPage() { ); } -export default withErrorBoundary(ExistingSilenceEditorPage, { style: 'page' }); + +export default withPageErrorBoundary(ExistingSilenceEditorPage); diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index 6eaf819d11a..2faa5a2b1c8 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -12,7 +12,6 @@ import { LoadingPlaceholder, Stack, useStyles2, - withErrorBoundary, } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { Trans } from 'app/core/internationalization'; @@ -27,6 +26,7 @@ import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbili import { useAlertmanager } from '../../state/AlertmanagerContext'; import { parsePromQLStyleMatcherLooseSafe } from '../../utils/matchers'; import { getSilenceFiltersFromUrlParams, makeAMLink, stringifyErrorLike } from '../../utils/misc'; +import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { Authorize } from '../Authorize'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; @@ -393,4 +393,5 @@ function SilencesTablePage() { ); } -export default withErrorBoundary(SilencesTablePage, { style: 'page' }); + +export default withPageErrorBoundary(SilencesTablePage); diff --git a/public/app/features/alerting/unified/home/Home.tsx b/public/app/features/alerting/unified/home/Home.tsx index 898bfdebefb..023b6a237a3 100644 --- a/public/app/features/alerting/unified/home/Home.tsx +++ b/public/app/features/alerting/unified/home/Home.tsx @@ -5,12 +5,13 @@ import { Box, Stack, Tab, TabContent, TabsBar } from '@grafana/ui'; import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; import { isLocalDevEnv } from '../utils/misc'; +import { withPageErrorBoundary } from '../withPageErrorBoundary'; import GettingStarted, { WelcomeHeader } from './GettingStarted'; import { getInsightsScenes, insightsIsAvailable } from './Insights'; import { PluginIntegrations } from './PluginIntegrations'; -export default function Home() { +function Home() { const insightsEnabled = (insightsIsAvailable() || isLocalDevEnv()) && Boolean(config.featureToggles.alertingInsights); const [activeTab, setActiveTab] = useState<'insights' | 'overview'>(insightsEnabled ? 'insights' : 'overview'); @@ -51,3 +52,5 @@ export default function Home() { ); } + +export default withPageErrorBoundary(Home); diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx index b5adba57cf2..42585600052 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx @@ -2,7 +2,6 @@ import { useCallback } from 'react'; import { useParams } from 'react-router-dom-v5-compat'; import { NavModelItem } from '@grafana/data'; -import { withErrorBoundary } from '@grafana/ui'; import { RuleIdentifier } from 'app/types/unified-alerting'; import { AlertWarning } from '../AlertWarning'; @@ -11,6 +10,7 @@ import { AlertRuleForm } from '../components/rule-editor/alert-rule-form/AlertRu import { useURLSearchParams } from '../hooks/useURLSearchParams'; import { useRulesAccess } from '../utils/accessControlHooks'; import * as ruleId from '../utils/rule-id'; +import { withPageErrorBoundary } from '../withPageErrorBoundary'; import { CloneRuleEditor } from './CloneRuleEditor'; import { ExistingRuleEditor } from './ExistingRuleEditor'; @@ -78,7 +78,9 @@ const RuleEditor = () => { ); }; -export default withErrorBoundary(RuleEditor, { style: 'page' }); +// The pageNav property makes it difficult to only rely on AlertingPageWrapper +// to catch errors. +export default withPageErrorBoundary(RuleEditor); function useRuleEditorPathParams() { const params = useParams(); diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx index d90c4b238c0..759e7b48474 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx @@ -4,7 +4,7 @@ import { useAsyncFn, useInterval } from 'react-use'; import { urlUtil } from '@grafana/data'; import { logInfo } from '@grafana/runtime'; -import { Button, LinkButton, Stack, withErrorBoundary } from '@grafana/ui'; +import { Button, LinkButton, Stack } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { Trans } from 'app/core/internationalization'; import { useDispatch } from 'app/types'; @@ -155,7 +155,7 @@ const RuleListV1 = () => { ); }; -export default withErrorBoundary(RuleListV1, { style: 'page' }); +export default RuleListV1; export function CreateAlertButton() { const [createRuleSupported, createRuleAllowed] = useAlertingAbility(AlertingAction.CreateAlertRule); diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx index b8ccbb2fa8f..86949386e3a 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx @@ -1,5 +1,3 @@ -import { withErrorBoundary } from '@grafana/ui'; - import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; import RulesFilter from '../components/rules/Filter/RulesFilter'; import { SupportedView } from '../components/rules/Filter/RulesViewModeSelector'; @@ -9,24 +7,25 @@ import { useURLSearchParams } from '../hooks/useURLSearchParams'; import { FilterView } from './FilterView'; import { GroupedView } from './GroupedView'; -const RuleList = withErrorBoundary( - () => { - const [queryParams] = useURLSearchParams(); - const { filterState, hasActiveFilters } = useRulesFilter(); +function RuleList() { + const [queryParams] = useURLSearchParams(); + const { filterState, hasActiveFilters } = useRulesFilter(); - const view: SupportedView = queryParams.get('view') === 'list' ? 'list' : 'grouped'; - const showListView = hasActiveFilters || view === 'list'; + const view: SupportedView = queryParams.get('view') === 'list' ? 'list' : 'grouped'; + const showListView = hasActiveFilters || view === 'list'; - return ( - // We don't want to show the Loading... indicator for the whole page. - // We show separate indicators for Grafana-managed and Cloud rules - - {}} /> - {showListView ? : } - - ); - }, - { style: 'page' } -); + return ( + <> + {}} /> + {showListView ? : } + + ); +} -export default RuleList; +export default function RuleListPage() { + return ( + + + + ); +} diff --git a/public/app/features/alerting/unified/withPageErrorBoundary.tsx b/public/app/features/alerting/unified/withPageErrorBoundary.tsx new file mode 100644 index 00000000000..e53d5bb2e0d --- /dev/null +++ b/public/app/features/alerting/unified/withPageErrorBoundary.tsx @@ -0,0 +1,25 @@ +import { ComponentType } from 'react'; + +import { ErrorBoundaryAlertProps, withErrorBoundary } from '@grafana/ui'; + +import { logError } from './Analytics'; + +/** + * HOC for wrapping alerting page in an error boundary. + * It provides alerting-specific error handling. + * + * @param Component - the react component to wrap in error boundary + * @param errorBoundaryProps - error boundary options + * + * @public + */ +export function withPageErrorBoundary

          ( + Component: ComponentType

          , + errorBoundaryProps: Omit = {} +): ComponentType

          { + return withErrorBoundary(Component, { + ...errorBoundaryProps, + style: 'page', + errorLogger: logError, + }); +} From 8a8e47fceac329abb9eebbf6dfde06893497d197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Thu, 13 Feb 2025 10:18:55 +0100 Subject: [PATCH 549/894] PluginExtensions: Added support for sharing functions (#98888) * feat: add generic plugin extension functions * updated betterer. * Fixed type issues after sync with main. * Remved extensions from datasource and panel. * Added validation for extension function registry. * Added tests and validation logic for function extensions registry. * removed prop already existing on base. * fixed lint error. --------- Co-authored-by: Marcus Andersson --- .betterer.results | 6 + packages/grafana-data/src/index.ts | 2 + packages/grafana-data/src/types/app.ts | 12 + packages/grafana-data/src/types/plugin.ts | 2 + .../src/types/pluginExtensions.ts | 19 +- .../grafana-runtime/src/services/index.ts | 3 + .../pluginExtensions/getPluginExtensions.ts | 17 +- .../pluginExtensions/usePluginFunctions.ts | 20 + .../manager/loader/finder/local_test.go | 43 +- pkg/plugins/manager/loader/loader_test.go | 25 +- pkg/plugins/models.go | 8 + pkg/plugins/plugins.go | 4 + pkg/plugins/plugins_test.go | 34 +- .../pluginsintegration/loader/loader_test.go | 91 ++- public/app/app.ts | 3 + .../unified/mocks/server/handlers/plugins.ts | 1 + .../alerting/unified/testSetup/plugins.ts | 1 + .../alerting/unified/utils/rules.test.ts | 1 + .../plugins/components/AppRootPage.test.tsx | 2 + .../plugins/components/AppRootPage.tsx | 3 + .../extensions/ExtensionRegistriesContext.tsx | 18 +- .../app/features/plugins/extensions/errors.ts | 5 + .../plugins/extensions/getPluginExtensions.ts | 2 +- .../registry/AddedComponentsRegistry.test.ts | 1 + .../registry/AddedFunctionsRegistry.test.ts | 677 ++++++++++++++++++ .../registry/AddedFunctionsRegistry.ts | 87 +++ .../registry/AddedLinksRegistry.test.ts | 1 + .../ExposedComponentsRegistry.test.ts | 1 + .../plugins/extensions/registry/setup.ts | 3 + .../plugins/extensions/registry/types.ts | 2 + .../extensions/usePluginComponent.test.tsx | 4 + .../extensions/usePluginComponents.test.tsx | 3 + .../extensions/usePluginExtensions.test.tsx | 2 + .../plugins/extensions/usePluginFunctions.tsx | 82 +++ .../extensions/usePluginLinks.test.tsx | 3 + .../plugins/extensions/utils.test.tsx | 13 + .../plugins/extensions/validators.test.tsx | 4 + .../features/plugins/extensions/validators.ts | 33 + .../app/features/plugins/importPanelPlugin.ts | 1 - public/app/features/plugins/plugin_loader.ts | 12 +- 40 files changed, 1182 insertions(+), 69 deletions(-) create mode 100644 packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts create mode 100644 public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts create mode 100644 public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.ts create mode 100644 public/app/features/plugins/extensions/usePluginFunctions.tsx diff --git a/.betterer.results b/.betterer.results index ca4b89c9605..89b413d6023 100644 --- a/.betterer.results +++ b/.betterer.results @@ -491,6 +491,9 @@ exports[`better eslint`] = { "packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -5602,6 +5605,9 @@ exports[`better eslint`] = { [0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], + "public/app/features/plugins/extensions/usePluginFunctions.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/plugins/extensions/usePluginLinks.tsx:5381": [ [0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] ], diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 76195d7d8c0..6aa49febd08 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -549,6 +549,7 @@ export { type PluginExtensionLink, type PluginExtensionComponent, type PluginExtensionConfig, + type PluginExtensionFunction, type PluginExtensionLinkConfig, type PluginExtensionComponentConfig, type PluginExtensionEventHelpers, @@ -559,6 +560,7 @@ export { type PluginExtensionExposedComponentConfig, type PluginExtensionAddedComponentConfig, type PluginExtensionAddedLinkConfig, + type PluginExtensionAddedFunctionConfig, } from './types/pluginExtensions'; export { type ScopeDashboardBindingSpec, diff --git a/packages/grafana-data/src/types/app.ts b/packages/grafana-data/src/types/app.ts index 3b33452bd03..23e52f913b8 100644 --- a/packages/grafana-data/src/types/app.ts +++ b/packages/grafana-data/src/types/app.ts @@ -9,6 +9,7 @@ import { PluginExtensionExposedComponentConfig, PluginExtensionAddedComponentConfig, PluginExtensionAddedLinkConfig, + PluginExtensionAddedFunctionConfig, } from './pluginExtensions'; /** @@ -60,6 +61,7 @@ export class AppPlugin extends GrafanaPlugin>; @@ -113,6 +115,10 @@ export class AppPlugin extends GrafanaPlugin(linkConfig: PluginExtensionAddedLinkConfig) { this._addedLinkConfigs.push(linkConfig as PluginExtensionAddedLinkConfig); @@ -125,6 +131,12 @@ export class AppPlugin extends GrafanaPlugin(addedFunctionConfig: PluginExtensionAddedFunctionConfig) { + this._addedFunctionConfigs.push(addedFunctionConfig); + + return this; + } + exposeComponent(componentConfig: PluginExtensionExposedComponentConfig) { this._exposedComponentConfigs.push(componentConfig as PluginExtensionExposedComponentConfig); diff --git a/packages/grafana-data/src/types/plugin.ts b/packages/grafana-data/src/types/plugin.ts index ebb3da684f6..8e1be814c96 100644 --- a/packages/grafana-data/src/types/plugin.ts +++ b/packages/grafana-data/src/types/plugin.ts @@ -130,6 +130,8 @@ export interface PluginExtensions { // The component extensions that the plugin registers addedComponents: ExtensionInfo[]; + addedFunctions: ExtensionInfo[]; + // The link extensions that the plugin registers addedLinks: ExtensionInfo[]; diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index fcbbecf5f41..5b10f80f143 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -14,6 +14,7 @@ import { RawTimeRange, TimeZone } from './time'; export enum PluginExtensionTypes { link = 'link', component = 'component', + function = 'function', } type PluginExtensionBase = { @@ -36,7 +37,12 @@ export type PluginExtensionComponent = PluginExtensionBase & { component: React.ComponentType; }; -export type PluginExtension = PluginExtensionLink | PluginExtensionComponent; +export type PluginExtensionFunction void> = PluginExtensionBase & { + type: PluginExtensionTypes.function; + fn: Signature; +}; + +export type PluginExtension = PluginExtensionLink | PluginExtensionComponent | PluginExtensionFunction; // Objects used for registering extensions (in app plugins) // -------------------------------------------------------- @@ -74,6 +80,17 @@ export type PluginExtensionAddedComponentConfig = PluginExtensionCon */ component: React.ComponentType; }; +export type PluginExtensionAddedFunctionConfig = PluginExtensionConfigBase & { + /** + * The target extension points where the component will be added + */ + targets: string | string[]; + + /** + * The function to be executed + */ + fn: Signature; +}; export type PluginAddedLinksConfigureFunc = (context: Readonly | undefined) => | Partial<{ diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts index 3e02e0d2863..5e8892c5cc7 100644 --- a/packages/grafana-runtime/src/services/index.ts +++ b/packages/grafana-runtime/src/services/index.ts @@ -22,6 +22,8 @@ export { type UsePluginExtensions, type UsePluginExtensionsResult, type UsePluginComponentResult, + type UsePluginFunctionsOptions, + type UsePluginFunctionsResult, } from './pluginExtensions/getPluginExtensions'; export { setPluginExtensionsHook, @@ -33,6 +35,7 @@ export { export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent'; export { setPluginComponentsHook, usePluginComponents } from './pluginExtensions/usePluginComponents'; export { setPluginLinksHook, usePluginLinks } from './pluginExtensions/usePluginLinks'; +export { setPluginFunctionsHook, usePluginFunctions } from './pluginExtensions/usePluginFunctions'; export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils'; export { setCurrentUser } from './user'; diff --git a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts index be151b05858..2f70132d5fe 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts @@ -1,4 +1,9 @@ -import type { PluginExtension, PluginExtensionLink, PluginExtensionComponent } from '@grafana/data'; +import type { + PluginExtension, + PluginExtensionLink, + PluginExtensionComponent, + PluginExtensionFunction, +} from '@grafana/data'; import { isPluginExtensionComponent, isPluginExtensionLink } from './utils'; @@ -52,6 +57,16 @@ export type UsePluginLinksResult = { links: PluginExtensionLink[]; }; +export type UsePluginFunctionsOptions = { + extensionPointId: string; + limitPerPlugin?: number; +}; + +export type UsePluginFunctionsResult = { + isLoading: boolean; + functions: Array>; +}; + let singleton: GetPluginExtensions | undefined; export function setPluginExtensionGetter(instance: GetPluginExtensions): void { diff --git a/packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts b/packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts new file mode 100644 index 00000000000..1eb86b70e14 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts @@ -0,0 +1,20 @@ +import { UsePluginFunctionsOptions, UsePluginFunctionsResult } from './getPluginExtensions'; + +export type UsePluginFunctions = (options: UsePluginFunctionsOptions) => UsePluginFunctionsResult; + +let singleton: UsePluginFunctions | undefined; + +export function setPluginFunctionsHook(hook: UsePluginFunctions): void { + // We allow overriding the registry in tests + if (singleton && process.env.NODE_ENV !== 'test') { + throw new Error('setUsePluginFunctionsHook() function should only be called once, when Grafana is starting.'); + } + singleton = hook; +} + +export function usePluginFunctions(options: UsePluginFunctionsOptions): UsePluginFunctionsResult { + if (!singleton) { + throw new Error('usePluginFunctions(options) can only be used after the Grafana instance has started.'); + } + return singleton(options) as UsePluginFunctionsResult; +} diff --git a/pkg/plugins/manager/loader/finder/local_test.go b/pkg/plugins/manager/loader/finder/local_test.go index 9664f824186..18946548c56 100644 --- a/pkg/plugins/manager/loader/finder/local_test.go +++ b/pkg/plugins/manager/loader/finder/local_test.go @@ -57,6 +57,7 @@ func TestFinder_Find(t *testing.T) { Extensions: plugins.Extensions{ AddedLinks: []plugins.AddedLink{}, AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -96,8 +97,10 @@ func TestFinder_Find(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -127,8 +130,10 @@ func TestFinder_Find(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -200,8 +205,10 @@ func TestFinder_Find(t *testing.T) { {Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Action: "plugins.app:access"}, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -238,8 +245,10 @@ func TestFinder_Find(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -269,8 +278,10 @@ func TestFinder_Find(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -300,8 +311,10 @@ func TestFinder_Find(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -340,8 +353,10 @@ func TestFinder_Find(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index d39c9c21303..e5d1b260199 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -106,6 +106,7 @@ func TestLoader_Load(t *testing.T) { Extensions: plugins.Extensions{ AddedLinks: []plugins.AddedLink{}, AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -201,8 +202,10 @@ func TestLoader_Load(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -249,8 +252,10 @@ func TestLoader_Load(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -304,8 +309,10 @@ func TestLoader_Load(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -398,8 +405,10 @@ func TestLoader_Load(t *testing.T) { {Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"}, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 6b9b8e05ad2..8440cb37a11 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -63,6 +63,7 @@ type ExtensionsV2 struct { AddedComponents []AddedComponent `json:"addedComponents"` ExposedComponents []ExposedComponent `json:"exposedComponents"` ExtensionPoints []ExtensionPoint `json:"extensionPoints"` + AddedFunctions []AddedFunction `json:"addedFunctions"` } type Extensions ExtensionsV2 @@ -76,6 +77,7 @@ func (e *Extensions) UnmarshalJSON(data []byte) error { e.AddedLinks = extensionsV2.AddedLinks e.ExposedComponents = extensionsV2.ExposedComponents e.ExtensionPoints = extensionsV2.ExtensionPoints + e.AddedFunctions = extensionsV2.AddedFunctions return nil } @@ -123,6 +125,11 @@ type AddedComponent struct { Description string `json:"description"` } +type AddedFunction struct { + Targets []string `json:"targets"` + Title string `json:"title"` +} + type ExposedComponent struct { Id string `json:"id"` Title string `json:"title"` @@ -267,6 +274,7 @@ type PluginMetaDTO struct { Angular AngularMeta `json:"angular"` MultiValueFilterOperators bool `json:"multiValueFilterOperators"` LoadingStrategy LoadingStrategy `json:"loadingStrategy"` + Extensions Extensions `json:"extensions"` } type DataSourceDTO struct { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 3239927747b..f815d07c6b1 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -167,6 +167,10 @@ func ReadPluginJSON(reader io.Reader) (JSONData, error) { plugin.Extensions.AddedComponents = []AddedComponent{} } + if plugin.Extensions.AddedFunctions == nil { + plugin.Extensions.AddedFunctions = []AddedFunction{} + } + if plugin.Extensions.ExposedComponents == nil { plugin.Extensions.ExposedComponents = []ExposedComponent{} } diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index b3922755603..2a6f85dee07 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -56,6 +56,7 @@ func Test_ReadPluginJSON(t *testing.T) { Extensions: Extensions{ AddedLinks: []AddedLink{}, AddedComponents: []AddedComponent{}, + AddedFunctions: []AddedFunction{}, ExposedComponents: []ExposedComponent{}, ExtensionPoints: []ExtensionPoint{}, }, @@ -108,8 +109,10 @@ func Test_ReadPluginJSON(t *testing.T) { Name: "Pie Chart (old)", Extensions: Extensions{ - AddedLinks: []AddedLink{}, - AddedComponents: []AddedComponent{}, + AddedLinks: []AddedLink{}, + AddedComponents: []AddedComponent{}, + AddedFunctions: []AddedFunction{}, + ExposedComponents: []ExposedComponent{}, ExtensionPoints: []ExtensionPoint{}, }, @@ -143,8 +146,10 @@ func Test_ReadPluginJSON(t *testing.T) { Type: TypeDataSource, Extensions: Extensions{ - AddedLinks: []AddedLink{}, - AddedComponents: []AddedComponent{}, + AddedLinks: []AddedLink{}, + AddedComponents: []AddedComponent{}, + AddedFunctions: []AddedFunction{}, + ExposedComponents: []ExposedComponent{}, ExtensionPoints: []ExtensionPoint{}, }, @@ -188,6 +193,9 @@ func Test_ReadPluginJSON(t *testing.T) { "id": "myorg-extensions-app/component-1/v1" } ], + "addedFunctions": [ + {"targets": ["foo/bar"], "title":"some hook"} + ], "extensionPoints": [ { "title": "Extension point 1", @@ -209,6 +217,7 @@ func Test_ReadPluginJSON(t *testing.T) { {Title: "Added link 1", Description: "Added link 1 description", Targets: []string{"grafana/dashboard/panel/menu"}}, }, AddedComponents: []AddedComponent{ + {Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}}, }, ExposedComponents: []ExposedComponent{ @@ -217,6 +226,9 @@ func Test_ReadPluginJSON(t *testing.T) { ExtensionPoints: []ExtensionPoint{ {Id: "myorg-extensions-app/extensions-point-1/v1", Title: "Extension point 1", Description: "Extension points 1 description"}, }, + AddedFunctions: []AddedFunction{ + {Targets: []string{"foo/bar"}, Title: "some hook"}, + }, }, Dependencies: Dependencies{ @@ -271,6 +283,7 @@ func Test_ReadPluginJSON(t *testing.T) { AddedComponents: []AddedComponent{ {Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}}, }, + AddedFunctions: []AddedFunction{}, ExposedComponents: []ExposedComponent{}, ExtensionPoints: []ExtensionPoint{}, }, @@ -301,8 +314,10 @@ func Test_ReadPluginJSON(t *testing.T) { Type: TypeApp, Extensions: Extensions{ - AddedLinks: []AddedLink{}, - AddedComponents: []AddedComponent{}, + AddedLinks: []AddedLink{}, + AddedComponents: []AddedComponent{}, + AddedFunctions: []AddedFunction{}, + ExposedComponents: []ExposedComponent{}, ExtensionPoints: []ExtensionPoint{}, }, @@ -332,8 +347,10 @@ func Test_ReadPluginJSON(t *testing.T) { Type: TypeApp, Extensions: Extensions{ - AddedLinks: []AddedLink{}, - AddedComponents: []AddedComponent{}, + AddedLinks: []AddedLink{}, + AddedComponents: []AddedComponent{}, + AddedFunctions: []AddedFunction{}, + ExposedComponents: []ExposedComponent{}, ExtensionPoints: []ExtensionPoint{}, }, @@ -371,6 +388,7 @@ func Test_ReadPluginJSON(t *testing.T) { Extensions: Extensions{ AddedLinks: []AddedLink{}, AddedComponents: []AddedComponent{}, + AddedFunctions: []AddedFunction{}, ExposedComponents: []ExposedComponent{}, ExtensionPoints: []ExtensionPoint{}, }, diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index ca6fef68b50..644058909d4 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -105,6 +105,7 @@ func TestLoader_Load(t *testing.T) { Extensions: plugins.Extensions{ AddedLinks: []plugins.AddedLink{}, AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -200,8 +201,10 @@ func TestLoader_Load(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -248,8 +251,10 @@ func TestLoader_Load(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -309,8 +314,10 @@ func TestLoader_Load(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -423,8 +430,10 @@ func TestLoader_Load(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -504,8 +513,10 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -615,8 +626,10 @@ func TestLoader_Load_CustomSource(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -696,8 +709,10 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -801,8 +816,10 @@ func TestLoader_Load_RBACReady(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -883,8 +900,10 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { ExposedComponents: []string{}, }}, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -964,8 +983,10 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -1060,8 +1081,10 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { {Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-datasource"}, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -1272,8 +1295,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -1314,8 +1339,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -1463,8 +1490,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, @@ -1512,8 +1541,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, }, Extensions: plugins.Extensions{ - AddedLinks: []plugins.AddedLink{}, - AddedComponents: []plugins.AddedComponent{}, + AddedLinks: []plugins.AddedLink{}, + AddedComponents: []plugins.AddedComponent{}, + AddedFunctions: []plugins.AddedFunction{}, + ExposedComponents: []plugins.ExposedComponent{}, ExtensionPoints: []plugins.ExtensionPoint{}, }, diff --git a/public/app/app.ts b/public/app/app.ts index f4c4fd5d77c..cc3a2e5e5c5 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -40,6 +40,7 @@ import { setChromeHeaderHeightHook, setPluginLinksHook, setCorrelationsService, + setPluginFunctionsHook, } from '@grafana/runtime'; import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView'; import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; @@ -89,6 +90,7 @@ import { pluginExtensionRegistries } from './features/plugins/extensions/registr import { usePluginComponent } from './features/plugins/extensions/usePluginComponent'; import { usePluginComponents } from './features/plugins/extensions/usePluginComponents'; import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions'; +import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions'; import { usePluginLinks } from './features/plugins/extensions/usePluginLinks'; import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils'; import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin'; @@ -229,6 +231,7 @@ export class GrafanaApp { setPluginLinksHook(usePluginLinks); setPluginComponentHook(usePluginComponent); setPluginComponentsHook(usePluginComponents); + setPluginFunctionsHook(usePluginFunctions); // initialize chrome service const queryParams = locationService.getSearchObject(); diff --git a/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts b/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts index 1d30b05ad50..e0a0142993f 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts @@ -24,6 +24,7 @@ export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => { addedComponents: [], extensionPoints: [], exposedComponents: [], + addedFunctions: [], }, dependencies: { grafanaVersion: '', diff --git a/public/app/features/alerting/unified/testSetup/plugins.ts b/public/app/features/alerting/unified/testSetup/plugins.ts index dd3fb82ee76..72a94187ef0 100644 --- a/public/app/features/alerting/unified/testSetup/plugins.ts +++ b/public/app/features/alerting/unified/testSetup/plugins.ts @@ -163,6 +163,7 @@ export function pluginMetaToPluginConfig(pluginMeta: PluginMeta): AppPluginConfi addedComponents: [], extensionPoints: [], exposedComponents: [], + addedFunctions: [], }, }; } diff --git a/public/app/features/alerting/unified/utils/rules.test.ts b/public/app/features/alerting/unified/utils/rules.test.ts index 4b4036c40d2..63c0cb62f04 100644 --- a/public/app/features/alerting/unified/utils/rules.test.ts +++ b/public/app/features/alerting/unified/utils/rules.test.ts @@ -55,6 +55,7 @@ describe('getRuleOrigin', () => { addedComponents: [], extensionPoints: [], exposedComponents: [], + addedFunctions: [], }, dependencies: { grafanaVersion: '', diff --git a/public/app/features/plugins/components/AppRootPage.test.tsx b/public/app/features/plugins/components/AppRootPage.test.tsx index 84736aed8de..c9e5ec616be 100644 --- a/public/app/features/plugins/components/AppRootPage.test.tsx +++ b/public/app/features/plugins/components/AppRootPage.test.tsx @@ -12,6 +12,7 @@ import { Echo } from 'app/core/services/echo/Echo'; import { ExtensionRegistriesProvider } from '../extensions/ExtensionRegistriesContext'; import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsRegistry'; +import { AddedFunctionsRegistry } from '../extensions/registry/AddedFunctionsRegistry'; import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry'; import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry'; import { getPluginSettings } from '../pluginSettings'; @@ -93,6 +94,7 @@ function renderUnderRouter(page = '') { addedComponentsRegistry: new AddedComponentsRegistry(), exposedComponentsRegistry: new ExposedComponentsRegistry(), addedLinksRegistry: new AddedLinksRegistry(), + addedFunctionsRegistry: new AddedFunctionsRegistry(), }; const pagePath = page ? `/${page}` : ''; const route = { diff --git a/public/app/features/plugins/components/AppRootPage.tsx b/public/app/features/plugins/components/AppRootPage.tsx index f0d6bd5329e..95a280027df 100644 --- a/public/app/features/plugins/components/AppRootPage.tsx +++ b/public/app/features/plugins/components/AppRootPage.tsx @@ -29,6 +29,7 @@ import { useAddedLinksRegistry, useAddedComponentsRegistry, useExposedComponentsRegistry, + useAddedFunctionsRegistry, } from '../extensions/ExtensionRegistriesContext'; import { getPluginSettings } from '../pluginSettings'; import { importAppPlugin } from '../plugin_loader'; @@ -60,6 +61,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) { const addedLinksRegistry = useAddedLinksRegistry(); const addedComponentsRegistry = useAddedComponentsRegistry(); const exposedComponentsRegistry = useExposedComponentsRegistry(); + const addedFunctionsRegistry = useAddedFunctionsRegistry(); const location = useLocation(); const [state, dispatch] = useReducer(stateSlice.reducer, initialState); const currentUrl = config.appSubUrl + location.pathname + location.search; @@ -104,6 +106,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) { addedLinksRegistry: addedLinksRegistry.readOnly(), addedComponentsRegistry: addedComponentsRegistry.readOnly(), exposedComponentsRegistry: exposedComponentsRegistry.readOnly(), + addedFunctionsRegistry: addedFunctionsRegistry.readOnly(), }} > (undefined); export const AddedComponentsRegistryContext = createContext(undefined); +export const AddedFunctionsRegistryContext = createContext(undefined); export const ExposedComponentsRegistryContext = createContext(undefined); export function useAddedLinksRegistry(): AddedLinksRegistry { @@ -31,6 +33,14 @@ export function useAddedComponentsRegistry(): AddedComponentsRegistry { return context; } +export function useAddedFunctionsRegistry(): AddedFunctionsRegistry { + const context = useContext(AddedFunctionsRegistryContext); + if (!context) { + throw new Error('No `AddedFunctionsRegistry` found.'); + } + return context; +} + export function useExposedComponentsRegistry(): ExposedComponentsRegistry { const context = useContext(ExposedComponentsRegistryContext); if (!context) { @@ -46,9 +56,11 @@ export const ExtensionRegistriesProvider = ({ return ( - - {children} - + + + {children} + + ); diff --git a/public/app/features/plugins/extensions/errors.ts b/public/app/features/plugins/extensions/errors.ts index 39c65df0f8e..cb9d22e7b46 100644 --- a/public/app/features/plugins/extensions/errors.ts +++ b/public/app/features/plugins/extensions/errors.ts @@ -8,6 +8,8 @@ export const TITLE_MISSING = 'Title is missing.'; export const DESCRIPTION_MISSING = 'Description is missing.'; +export const INVALID_EXTENSION_FUNCTION = 'The "fn" argument is invalid, it should be a function.'; + export const INVALID_CONFIGURE_FUNCTION = 'The "configure" function is invalid. It should be a function.'; export const INVALID_PATH_OR_ON_CLICK = 'Either "path" or "onClick" is required.'; @@ -33,6 +35,9 @@ export const TITLE_NOT_MATCHING_META_INFO = 'The "title" doesn\'t match the titl export const ADDED_LINK_META_INFO_MISSING = 'The extension was not recorded in the plugin.json. Added link extensions must be listed in the section "extensions.addedLinks[]". Currently, this is only required in development but will be enforced also in production builds in the future.'; +export const ADDED_FUNCTION_META_INFO_MISSING = + 'The extension was not recorded in the plugin.json. Added function extensions must be listed in the section "extensions.addedFunction[]". Currently, this is only required in development but will be enforced also in production builds in the future.'; + export const DESCRIPTION_NOT_MATCHING_META_INFO = 'The "description" doesn\'t match the description recorded in plugin.json.'; diff --git a/public/app/features/plugins/extensions/getPluginExtensions.ts b/public/app/features/plugins/extensions/getPluginExtensions.ts index a47292c8497..30f5865b259 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.ts @@ -1,8 +1,8 @@ import { isString } from 'lodash'; import { - type PluginExtension, PluginExtensionTypes, + type PluginExtension, type PluginExtensionLink, type PluginExtensionComponent, } from '@grafana/data'; diff --git a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts index e82c48dcee0..5a18e222431 100644 --- a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts @@ -52,6 +52,7 @@ describe('AddedComponentsRegistry', () => { extensions: { addedLinks: [], addedComponents: [], + addedFunctions: [], exposedComponents: [], extensionPoints: [], }, diff --git a/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts new file mode 100644 index 00000000000..ae0addc52c7 --- /dev/null +++ b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts @@ -0,0 +1,677 @@ +import { firstValueFrom } from 'rxjs'; + +import { PluginLoadingStrategy } from '@grafana/data'; +import { config } from '@grafana/runtime'; + +import { log } from '../logs/log'; +import { resetLogMock } from '../logs/testUtils'; +import { isGrafanaDevMode } from '../utils'; + +import { AddedFunctionsRegistry } from './AddedFunctionsRegistry'; +import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry'; + +jest.mock('../utils', () => ({ + ...jest.requireActual('../utils'), + + // Manually set the dev mode to false + // (to make sure that by default we are testing a production scneario) + isGrafanaDevMode: jest.fn().mockReturnValue(false), +})); + +jest.mock('../logs/log', () => { + const { createLogMock } = jest.requireActual('../logs/testUtils'); + const original = jest.requireActual('../logs/log'); + + return { + ...original, + log: createLogMock(), + }; +}); + +describe('addedFunctionsRegistry', () => { + const originalApps = config.apps; + const pluginId = 'grafana-basic-app'; + const appPluginConfig = { + id: pluginId, + path: '', + version: '', + preload: false, + angular: { + detected: false, + hideDeprecation: false, + }, + loadingStrategy: PluginLoadingStrategy.fetch, + dependencies: { + grafanaVersion: '8.0.0', + plugins: [], + extensions: { + exposedComponents: [], + }, + }, + extensions: { + addedFunctions: [], + addedLinks: [], + addedComponents: [], + exposedComponents: [], + extensionPoints: [], + }, + }; + + beforeEach(() => { + resetLogMock(log); + jest.mocked(isGrafanaDevMode).mockReturnValue(false); + config.apps = { + [pluginId]: appPluginConfig, + }; + }); + + afterEach(() => { + config.apps = originalApps; + }); + + it('should return empty registry when no extensions registered', async () => { + const addedFunctionsRegistry = new AddedFunctionsRegistry(); + const observable = addedFunctionsRegistry.asObservable(); + const registry = await firstValueFrom(observable); + expect(registry).toEqual({}); + }); + + it('should be possible to register function extensions in the registry', async () => { + const addedFunctionsRegistry = new AddedFunctionsRegistry(); + + addedFunctionsRegistry.register({ + pluginId, + configs: [ + { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn(), + }, + { + title: 'Function 2', + description: 'Function 2 description', + targets: 'plugins/myorg-basic-app/start', + fn: jest.fn(), + }, + ], + }); + + const registry = await addedFunctionsRegistry.getState(); + + expect(registry).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId, + title: 'Function 1', + description: 'Function 1 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + ], + 'plugins/myorg-basic-app/start': [ + { + pluginId: pluginId, + title: 'Function 2', + description: 'Function 2 description', + extensionPointId: 'plugins/myorg-basic-app/start', + fn: expect.any(Function), + }, + ], + }); + }); + it('should be possible to asynchronously register function extensions for the same placement (different plugins)', async () => { + const pluginId1 = 'grafana-basic-app'; + const pluginId2 = 'grafana-basic-app2'; + const reactiveRegistry = new AddedFunctionsRegistry(); + + // Register extensions for the first plugin + reactiveRegistry.register({ + pluginId: pluginId1, + configs: [ + { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry1 = await reactiveRegistry.getState(); + + expect(registry1).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId1, + title: 'Function 1', + description: 'Function 1 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + ], + }); + + // Register extensions for the second plugin to a different placement + reactiveRegistry.register({ + pluginId: pluginId2, + configs: [ + { + title: 'Function 2', + description: 'Function 2 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry2 = await reactiveRegistry.getState(); + + expect(registry2).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId1, + title: 'Function 1', + description: 'Function 1 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + { + pluginId: pluginId2, + title: 'Function 2', + description: 'Function 2 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + ], + }); + }); + + it('should be possible to asynchronously register function extensions for a different placement (different plugin)', async () => { + const pluginId1 = 'grafana-basic-app'; + const pluginId2 = 'grafana-basic-app2'; + const reactiveRegistry = new AddedFunctionsRegistry(); + + // Register extensions for the first plugin + reactiveRegistry.register({ + pluginId: pluginId1, + configs: [ + { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry1 = await reactiveRegistry.getState(); + + expect(registry1).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId1, + + title: 'Function 1', + description: 'Function 1 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + ], + }); + + // Register extensions for the second plugin to a different placement + reactiveRegistry.register({ + pluginId: pluginId2, + configs: [ + { + title: 'Function 2', + description: 'Function 2 description', + targets: 'plugins/myorg-basic-app/start', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry2 = await reactiveRegistry.getState(); + + expect(registry2).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId1, + title: 'Function 1', + description: 'Function 1 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + ], + 'plugins/myorg-basic-app/start': [ + { + pluginId: pluginId2, + title: 'Function 2', + description: 'Function 2 description', + extensionPointId: 'plugins/myorg-basic-app/start', + fn: expect.any(Function), + }, + ], + }); + }); + + it('should be possible to asynchronously register function extensions for the same placement (same plugin)', async () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new AddedFunctionsRegistry(); + + // Register extensions for the first extension point + reactiveRegistry.register({ + pluginId: pluginId, + configs: [ + { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + // Register extensions to a different extension point + reactiveRegistry.register({ + pluginId: pluginId, + configs: [ + { + title: 'Function 2', + description: 'Function 2 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry2 = await reactiveRegistry.getState(); + + expect(registry2).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId, + + title: 'Function 1', + description: 'Function 1 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + { + pluginId: pluginId, + + title: 'Function 2', + description: 'Function 2 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + ], + }); + }); + + it('should be possible to asynchronously register function extensions for a different placement (same plugin)', async () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new AddedFunctionsRegistry(); + + // Register extensions for the first extension point + reactiveRegistry.register({ + pluginId: pluginId, + configs: [ + { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + // Register extensions to a different extension point + reactiveRegistry.register({ + pluginId: pluginId, + configs: [ + { + title: 'Function 2', + description: 'Function 2 description', + targets: 'plugins/myorg-basic-app/start', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + const registry2 = await reactiveRegistry.getState(); + + expect(registry2).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId, + + title: 'Function 1', + description: 'Function 1 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + ], + 'plugins/myorg-basic-app/start': [ + { + pluginId: pluginId, + + title: 'Function 2', + description: 'Function 2 description', + extensionPointId: 'plugins/myorg-basic-app/start', + fn: expect.any(Function), + }, + ], + }); + }); + + it('should notify subscribers when the registry changes', async () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new AddedFunctionsRegistry(); + const observable = reactiveRegistry.asObservable(); + const subscribeCallback = jest.fn(); + + observable.subscribe(subscribeCallback); + + // Register extensions for the first plugin + reactiveRegistry.register({ + pluginId: pluginId, + configs: [ + { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + expect(subscribeCallback).toHaveBeenCalledTimes(2); + + // Register extensions for the first plugin + reactiveRegistry.register({ + pluginId: 'another-plugin', + configs: [ + { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + expect(subscribeCallback).toHaveBeenCalledTimes(3); + + const registry = subscribeCallback.mock.calls[2][0]; + + expect(registry).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId, + title: 'Function 1', + description: 'Function 1 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + { + pluginId: 'another-plugin', + title: 'Function 1', + description: 'Function 1 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + ], + }); + }); + + it('should give the last version of the registry for new subscribers', async () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new AddedFunctionsRegistry(); + const observable = reactiveRegistry.asObservable(); + const subscribeCallback = jest.fn(); + + reactiveRegistry.register({ + pluginId: pluginId, + configs: [ + { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + observable.subscribe(subscribeCallback); + expect(subscribeCallback).toHaveBeenCalledTimes(1); + + const registry = subscribeCallback.mock.calls[0][0]; + + expect(registry).toEqual({ + 'grafana/dashboard/panel/menu': [ + { + pluginId: pluginId, + title: 'Function 1', + description: 'Function 1 description', + extensionPointId: 'grafana/dashboard/panel/menu', + fn: expect.any(Function), + }, + ], + }); + }); + + it('should not register a function extension if it has an invalid fn function', () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new AddedFunctionsRegistry(); + const observable = reactiveRegistry.asObservable(); + const subscribeCallback = jest.fn(); + + reactiveRegistry.register({ + pluginId: pluginId, + configs: [ + { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + //@ts-ignore + fn: '...', + }, + ], + }); + + expect(log.error).toHaveBeenCalled(); + + observable.subscribe(subscribeCallback); + expect(subscribeCallback).toHaveBeenCalledTimes(1); + + const registry = subscribeCallback.mock.calls[0][0]; + expect(registry).toEqual({}); + }); + + it('should not register a function extension if it has invalid properties (empty title)', () => { + const pluginId = 'grafana-basic-app'; + const reactiveRegistry = new AddedFunctionsRegistry(); + const observable = reactiveRegistry.asObservable(); + const subscribeCallback = jest.fn(); + + reactiveRegistry.register({ + pluginId: pluginId, + configs: [ + { + title: '', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + expect(log.error).toHaveBeenCalled(); + + observable.subscribe(subscribeCallback); + expect(subscribeCallback).toHaveBeenCalledTimes(1); + + const registry = subscribeCallback.mock.calls[0][0]; + expect(registry).toEqual({}); + }); + + it('should not be possible to register a function on a read-only registry', async () => { + const pluginId = 'grafana-basic-app'; + const registry = new AddedFunctionsRegistry(); + const readOnlyRegistry = registry.readOnly(); + + expect(() => { + readOnlyRegistry.register({ + pluginId, + configs: [ + { + title: 'Function 2', + description: 'Function 2 description', + targets: 'plugins/myorg-basic-app/start', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + }).toThrow(MSG_CANNOT_REGISTER_READ_ONLY); + + const currentState = await readOnlyRegistry.getState(); + expect(Object.keys(currentState)).toHaveLength(0); + }); + + it('should pass down fresh registrations to the read-only version of the registry', async () => { + const pluginId = 'grafana-basic-app'; + const registry = new AddedFunctionsRegistry(); + const readOnlyRegistry = registry.readOnly(); + const subscribeCallback = jest.fn(); + let readOnlyState; + + // Should have no extensions registered in the beginning + readOnlyState = await readOnlyRegistry.getState(); + expect(Object.keys(readOnlyState)).toHaveLength(0); + + readOnlyRegistry.asObservable().subscribe(subscribeCallback); + + // Register an extension to the original (writable) registry + registry.register({ + pluginId, + configs: [ + { + title: 'Function 2', + description: 'Function 2 description', + targets: 'plugins/myorg-basic-app/start', + fn: jest.fn().mockReturnValue({}), + }, + ], + }); + + // The read-only registry should have received the new extension + readOnlyState = await readOnlyRegistry.getState(); + expect(Object.keys(readOnlyState)).toHaveLength(1); + + expect(subscribeCallback).toHaveBeenCalledTimes(2); + expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual(['plugins/myorg-basic-app/start']); + }); + + it('should not register a function added by a plugin in dev-mode if the meta-info is missing from the plugin.json', async () => { + // Enabling dev mode + jest.mocked(isGrafanaDevMode).mockReturnValue(true); + + const registry = new AddedFunctionsRegistry(); + const fnConfig = { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }; + + // Make sure that the meta-info is empty + config.apps[pluginId].extensions.addedFunctions = []; + + registry.register({ + pluginId, + configs: [fnConfig], + }); + + const currentState = await registry.getState(); + + expect(Object.keys(currentState)).toHaveLength(0); + expect(log.error).toHaveBeenCalled(); + }); + + it('should register a function added by core Grafana in dev-mode even if the meta-info is missing', async () => { + // Enabling dev mode + jest.mocked(isGrafanaDevMode).mockReturnValue(true); + + const registry = new AddedFunctionsRegistry(); + const fnConfig = { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }; + + registry.register({ + pluginId: 'grafana', + configs: [fnConfig], + }); + + const currentState = await registry.getState(); + + expect(Object.keys(currentState)).toHaveLength(1); + expect(log.error).not.toHaveBeenCalled(); + }); + + it('should register a function added by a plugin in production mode even if the meta-info is missing', async () => { + // Production mode + jest.mocked(isGrafanaDevMode).mockReturnValue(false); + + const registry = new AddedFunctionsRegistry(); + const fnConfig = { + title: 'Function 1', + description: 'Function 1 description', + targets: 'grafana/dashboard/panel/menu', + fn: jest.fn().mockReturnValue({}), + }; + + // Make sure that the meta-info is empty + config.apps[pluginId].extensions.addedFunctions = []; + + registry.register({ + pluginId, + configs: [fnConfig], + }); + + const currentState = await registry.getState(); + + expect(Object.keys(currentState)).toHaveLength(1); + expect(log.error).not.toHaveBeenCalled(); + }); + + it('should register a function added by a plugin in dev-mode if the meta-info is present', async () => { + // Enabling dev mode + jest.mocked(isGrafanaDevMode).mockReturnValue(true); + + const registry = new AddedFunctionsRegistry(); + const fnConfig = { + title: 'Function 1', + description: 'Function 1 description', + targets: ['grafana/dashboard/panel/menu'], + fn: jest.fn().mockReturnValue({}), + }; + + // Make sure that the meta-info is empty + config.apps[pluginId].extensions.addedFunctions = [fnConfig]; + + registry.register({ + pluginId, + configs: [fnConfig], + }); + + const currentState = await registry.getState(); + + expect(Object.keys(currentState)).toHaveLength(1); + expect(log.error).not.toHaveBeenCalled(); + }); +}); diff --git a/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.ts b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.ts new file mode 100644 index 00000000000..d23fe5e78b2 --- /dev/null +++ b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.ts @@ -0,0 +1,87 @@ +import { isFunction } from 'lodash'; +import { ReplaySubject } from 'rxjs'; + +import { PluginExtensionAddedFunctionConfig } from '@grafana/data'; + +import * as errors from '../errors'; +import { isGrafanaDevMode } from '../utils'; +import { isAddedFunctionMetaInfoMissing } from '../validators'; + +import { PluginExtensionConfigs, Registry, RegistryType } from './Registry'; + +const logPrefix = 'Could not register function extension. Reason:'; + +export type AddedFunctionsRegistryItem = { + pluginId: string; + title: string; + fn: unknown; + description?: string; +}; + +export class AddedFunctionsRegistry extends Registry { + constructor( + options: { + registrySubject?: ReplaySubject>; + initialState?: RegistryType; + } = {} + ) { + super(options); + } + + mapToRegistry( + registry: RegistryType, + item: PluginExtensionConfigs + ): RegistryType { + const { pluginId, configs } = item; + for (const config of configs) { + const configLog = this.logger.child({ + title: config.title, + pluginId, + }); + + if (!config.title) { + configLog.error(`${logPrefix} ${errors.TITLE_MISSING}`); + continue; + } + + if (!isFunction(config.fn)) { + configLog.error(`${logPrefix} ${errors.INVALID_EXTENSION_FUNCTION}`); + continue; + } + + if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedFunctionMetaInfoMissing(pluginId, config, configLog)) { + continue; + } + + const extensionPointIds = Array.isArray(config.targets) ? config.targets : [config.targets]; + for (const extensionPointId of extensionPointIds) { + const pointIdLog = configLog.child({ extensionPointId }); + + const result = { + pluginId, + fn: config.fn, + description: config.description, + title: config.title, + extensionPointId, + }; + + pointIdLog.debug('Added function extension successfully registered'); + + if (!(extensionPointId in registry)) { + registry[extensionPointId] = [result]; + } else { + registry[extensionPointId].push(result); + } + } + } + + return registry; + } + + // Returns a read-only version of the registry. + readOnly() { + return new AddedFunctionsRegistry({ + registrySubject: this.registrySubject, + }); + } +} diff --git a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts index 4d5ab5c084f..d3586240276 100644 --- a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts @@ -51,6 +51,7 @@ describe('AddedLinksRegistry', () => { extensions: { addedLinks: [], addedComponents: [], + addedFunctions: [], exposedComponents: [], extensionPoints: [], }, diff --git a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts index 0a7036894d1..863c89f7b40 100644 --- a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts @@ -52,6 +52,7 @@ describe('ExposedComponentsRegistry', () => { extensions: { addedLinks: [], addedComponents: [], + addedFunctions: [], exposedComponents: [], extensionPoints: [], }, diff --git a/public/app/features/plugins/extensions/registry/setup.ts b/public/app/features/plugins/extensions/registry/setup.ts index 6c2fd1e6a5f..91b7badc4eb 100644 --- a/public/app/features/plugins/extensions/registry/setup.ts +++ b/public/app/features/plugins/extensions/registry/setup.ts @@ -1,6 +1,7 @@ import { getCoreExtensionConfigurations } from '../getCoreExtensionConfigurations'; import { AddedComponentsRegistry } from './AddedComponentsRegistry'; +import { AddedFunctionsRegistry } from './AddedFunctionsRegistry'; import { AddedLinksRegistry } from './AddedLinksRegistry'; import { ExposedComponentsRegistry } from './ExposedComponentsRegistry'; import { PluginExtensionRegistries } from './types'; @@ -8,10 +9,12 @@ import { PluginExtensionRegistries } from './types'; export const addedComponentsRegistry = new AddedComponentsRegistry(); export const exposedComponentsRegistry = new ExposedComponentsRegistry(); export const addedLinksRegistry = new AddedLinksRegistry(); +export const addedFunctionsRegistry = new AddedFunctionsRegistry(); export const pluginExtensionRegistries: PluginExtensionRegistries = { addedComponentsRegistry, exposedComponentsRegistry, addedLinksRegistry, + addedFunctionsRegistry, }; // Registering core extensions diff --git a/public/app/features/plugins/extensions/registry/types.ts b/public/app/features/plugins/extensions/registry/types.ts index 115e859b7d9..1927bf31b75 100644 --- a/public/app/features/plugins/extensions/registry/types.ts +++ b/public/app/features/plugins/extensions/registry/types.ts @@ -1,9 +1,11 @@ import { AddedComponentsRegistry } from './AddedComponentsRegistry'; +import { AddedFunctionsRegistry } from './AddedFunctionsRegistry'; import { AddedLinksRegistry } from './AddedLinksRegistry'; import { ExposedComponentsRegistry } from './ExposedComponentsRegistry'; export type PluginExtensionRegistries = { addedComponentsRegistry: AddedComponentsRegistry; exposedComponentsRegistry: ExposedComponentsRegistry; + addedFunctionsRegistry: AddedFunctionsRegistry; addedLinksRegistry: AddedLinksRegistry; }; diff --git a/public/app/features/plugins/extensions/usePluginComponent.test.tsx b/public/app/features/plugins/extensions/usePluginComponent.test.tsx index 6385c022027..b2a3d3d3435 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.test.tsx @@ -7,6 +7,7 @@ import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; import { log } from './logs/log'; import { resetLogMock } from './logs/testUtils'; import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry'; +import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry'; import { AddedLinksRegistry } from './registry/AddedLinksRegistry'; import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry'; import { PluginExtensionRegistries } from './registry/types'; @@ -78,6 +79,7 @@ describe('usePluginComponent()', () => { extensions: { addedLinks: [], addedComponents: [], + addedFunctions: [], // This is necessary, so we can register exposed components to the registry during the tests // (Otherwise the registry would reject it in the imitated production mode) exposedComponents: [exposedComponentConfig], @@ -90,6 +92,7 @@ describe('usePluginComponent()', () => { addedComponentsRegistry: new AddedComponentsRegistry(), exposedComponentsRegistry: new ExposedComponentsRegistry(), addedLinksRegistry: new AddedLinksRegistry(), + addedFunctionsRegistry: new AddedFunctionsRegistry(), }; jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false }); jest.mocked(isGrafanaDevMode).mockReturnValue(false); @@ -122,6 +125,7 @@ describe('usePluginComponent()', () => { addedComponents: [], exposedComponents: [], extensionPoints: [], + addedFunctions: [], }, dependencies: { grafanaVersion: '8.0.0', diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index 75adda33f75..fcb729fdd64 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -6,6 +6,7 @@ import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; import { log } from './logs/log'; import { resetLogMock } from './logs/testUtils'; import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry'; +import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry'; import { AddedLinksRegistry } from './registry/AddedLinksRegistry'; import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry'; import { PluginExtensionRegistries } from './registry/types'; @@ -60,6 +61,7 @@ describe('usePluginComponents()', () => { addedComponentsRegistry: new AddedComponentsRegistry(), exposedComponentsRegistry: new ExposedComponentsRegistry(), addedLinksRegistry: new AddedLinksRegistry(), + addedFunctionsRegistry: new AddedFunctionsRegistry(), }; jest.mocked(wrapWithPluginContext).mockClear(); @@ -89,6 +91,7 @@ describe('usePluginComponents()', () => { addedComponents: [], exposedComponents: [], extensionPoints: [], + addedFunctions: [], }, dependencies: { grafanaVersion: '8.0.0', diff --git a/public/app/features/plugins/extensions/usePluginExtensions.test.tsx b/public/app/features/plugins/extensions/usePluginExtensions.test.tsx index 739906dd815..479e5cd4c6c 100644 --- a/public/app/features/plugins/extensions/usePluginExtensions.test.tsx +++ b/public/app/features/plugins/extensions/usePluginExtensions.test.tsx @@ -1,6 +1,7 @@ import { act, renderHook } from '@testing-library/react'; import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry'; +import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry'; import { AddedLinksRegistry } from './registry/AddedLinksRegistry'; import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry'; import { PluginExtensionRegistries } from './registry/types'; @@ -19,6 +20,7 @@ describe('usePluginExtensions()', () => { addedComponentsRegistry: new AddedComponentsRegistry(), addedLinksRegistry: new AddedLinksRegistry(), exposedComponentsRegistry: new ExposedComponentsRegistry(), + addedFunctionsRegistry: new AddedFunctionsRegistry(), }; jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false }); }); diff --git a/public/app/features/plugins/extensions/usePluginFunctions.tsx b/public/app/features/plugins/extensions/usePluginFunctions.tsx new file mode 100644 index 00000000000..68acee76221 --- /dev/null +++ b/public/app/features/plugins/extensions/usePluginFunctions.tsx @@ -0,0 +1,82 @@ +import { useMemo } from 'react'; +import { useObservable } from 'react-use'; + +import { usePluginContext, PluginExtensionFunction, PluginExtensionTypes } from '@grafana/data'; +import { UsePluginFunctionsOptions, UsePluginFunctionsResult } from '@grafana/runtime'; + +import { useAddedFunctionsRegistry } from './ExtensionRegistriesContext'; +import * as errors from './errors'; +import { log } from './logs/log'; +import { useLoadAppPlugins } from './useLoadAppPlugins'; +import { generateExtensionId, getExtensionPointPluginDependencies, isGrafanaDevMode } from './utils'; +import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators'; + +// Returns an array of component extensions for the given extension point +export function usePluginFunctions({ + limitPerPlugin, + extensionPointId, +}: UsePluginFunctionsOptions): UsePluginFunctionsResult { + const registry = useAddedFunctionsRegistry(); + const registryState = useObservable(registry.asObservable()); + const pluginContext = usePluginContext(); + const deps = getExtensionPointPluginDependencies(extensionPointId); + const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(deps); + + return useMemo(() => { + // For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana. + const enableRestrictions = isGrafanaDevMode() && pluginContext; + const results: Array> = []; + const extensionsByPlugin: Record = {}; + const pluginId = pluginContext?.meta.id ?? ''; + const pointLog = log.child({ + pluginId, + extensionPointId, + }); + if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) { + pointLog.error(errors.INVALID_EXTENSION_POINT_ID); + } + + if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { + pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING); + return { + isLoading: false, + functions: [], + }; + } + + if (isLoadingAppPlugins) { + return { + isLoading: true, + functions: [], + }; + } + + for (const registryItem of registryState?.[extensionPointId] ?? []) { + const { pluginId } = registryItem; + + // Only limit if the `limitPerPlugin` is set + if (limitPerPlugin && extensionsByPlugin[pluginId] >= limitPerPlugin) { + continue; + } + + if (extensionsByPlugin[pluginId] === undefined) { + extensionsByPlugin[pluginId] = 0; + } + + results.push({ + id: generateExtensionId(pluginId, extensionPointId, registryItem.title), + type: PluginExtensionTypes.function, + title: registryItem.title, + description: registryItem.description ?? '', + pluginId: pluginId, + fn: registryItem.fn as Signature, + }); + extensionsByPlugin[pluginId] += 1; + } + + return { + isLoading: false, + functions: results, + }; + }, [extensionPointId, limitPerPlugin, pluginContext, registryState, isLoadingAppPlugins]); +} diff --git a/public/app/features/plugins/extensions/usePluginLinks.test.tsx b/public/app/features/plugins/extensions/usePluginLinks.test.tsx index 9186ca0ddf6..f9fb623f42a 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.test.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.test.tsx @@ -6,6 +6,7 @@ import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; import { log } from './logs/log'; import { resetLogMock } from './logs/testUtils'; import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry'; +import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry'; import { AddedLinksRegistry } from './registry/AddedLinksRegistry'; import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry'; import { PluginExtensionRegistries } from './registry/types'; @@ -57,6 +58,7 @@ describe('usePluginLinks()', () => { addedComponentsRegistry: new AddedComponentsRegistry(), exposedComponentsRegistry: new ExposedComponentsRegistry(), addedLinksRegistry: new AddedLinksRegistry(), + addedFunctionsRegistry: new AddedFunctionsRegistry(), }; resetLogMock(log); @@ -85,6 +87,7 @@ describe('usePluginLinks()', () => { addedComponents: [], exposedComponents: [], extensionPoints: [], + addedFunctions: [], }, dependencies: { grafanaVersion: '8.0.0', diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index 32ad6215875..0e2d98f44b3 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -475,6 +475,7 @@ describe('Plugin Extensions / Utils', () => { extensions: { addedLinks: [], addedComponents: [], + addedFunctions: [], exposedComponents: [], extensionPoints: [], }, @@ -553,6 +554,7 @@ describe('Plugin Extensions / Utils', () => { extensions: { addedLinks: [], addedComponents: [], + addedFunctions: [], exposedComponents: [], extensionPoints: [], }, @@ -584,6 +586,7 @@ describe('Plugin Extensions / Utils', () => { addedComponents: [], exposedComponents: [], extensionPoints: [], + addedFunctions: [], }, }, 'myorg-third-app': { @@ -623,6 +626,7 @@ describe('Plugin Extensions / Utils', () => { ], exposedComponents: [], extensionPoints: [], + addedFunctions: [], }, }, }; @@ -679,6 +683,7 @@ describe('Plugin Extensions / Utils', () => { ], exposedComponents: [], extensionPoints: [], + addedFunctions: [], }, dependencies: { ...genereicAppPluginConfig.dependencies, @@ -705,6 +710,7 @@ describe('Plugin Extensions / Utils', () => { }, ], extensionPoints: [], + addedFunctions: [], }, dependencies: { ...genereicAppPluginConfig.dependencies, @@ -726,6 +732,7 @@ describe('Plugin Extensions / Utils', () => { }, ], extensionPoints: [], + addedFunctions: [], }, }, 'myorg-sixth-app': { @@ -763,6 +770,7 @@ describe('Plugin Extensions / Utils', () => { addedComponents: [], exposedComponents: [], extensionPoints: [], + addedFunctions: [], }, }; @@ -791,6 +799,7 @@ describe('Plugin Extensions / Utils', () => { }, ], extensionPoints: [], + addedFunctions: [], }, }, 'myorg-third-app': { @@ -825,6 +834,7 @@ describe('Plugin Extensions / Utils', () => { }, ], extensionPoints: [], + addedFunctions: [], }, dependencies: { ...genereicAppPluginConfig.dependencies, @@ -850,6 +860,7 @@ describe('Plugin Extensions / Utils', () => { }, ], extensionPoints: [], + addedFunctions: [], }, dependencies: { ...genereicAppPluginConfig.dependencies, @@ -871,6 +882,7 @@ describe('Plugin Extensions / Utils', () => { }, ], extensionPoints: [], + addedFunctions: [], }, }, }; @@ -902,6 +914,7 @@ describe('Plugin Extensions / Utils', () => { extensions: { addedLinks: [], addedComponents: [], + addedFunctions: [], exposedComponents: [], extensionPoints: [], }, diff --git a/public/app/features/plugins/extensions/validators.test.tsx b/public/app/features/plugins/extensions/validators.test.tsx index 12b146bbf5c..f083ff738ea 100644 --- a/public/app/features/plugins/extensions/validators.test.tsx +++ b/public/app/features/plugins/extensions/validators.test.tsx @@ -271,6 +271,7 @@ describe('Plugin Extension Validators', () => { addedComponents: [], exposedComponents: [], extensionPoints: [], + addedFunctions: [], }, }; const extensionConfig = { @@ -387,6 +388,7 @@ describe('Plugin Extension Validators', () => { addedComponents: [], exposedComponents: [], extensionPoints: [], + addedFunctions: [], }, }; const extensionConfig = { @@ -503,6 +505,7 @@ describe('Plugin Extension Validators', () => { addedComponents: [], exposedComponents: [], extensionPoints: [], + addedFunctions: [], }, }; const exposedComponentConfig = { @@ -688,6 +691,7 @@ describe('Plugin Extension Validators', () => { addedComponents: [], exposedComponents: [], extensionPoints: [], + addedFunctions: [], }, dependencies: { grafanaVersion: '8.0.0', diff --git a/public/app/features/plugins/extensions/validators.ts b/public/app/features/plugins/extensions/validators.ts index b1dbd5e9af8..cbdaf81e935 100644 --- a/public/app/features/plugins/extensions/validators.ts +++ b/public/app/features/plugins/extensions/validators.ts @@ -5,6 +5,7 @@ import type { PluginContextType, PluginExtensionAddedComponentConfig, PluginExtensionExposedComponentConfig, + PluginExtensionAddedFunctionConfig, } from '@grafana/data'; import { PluginAddedLinksConfigureFunc, PluginExtensionPoints } from '@grafana/data/src/types/pluginExtensions'; import { config, isPluginExtensionLink } from '@grafana/runtime'; @@ -160,6 +161,38 @@ export const isAddedLinkMetaInfoMissing = ( return false; }; +export const isAddedFunctionMetaInfoMissing = ( + pluginId: string, + metaInfo: PluginExtensionAddedFunctionConfig, + log: ExtensionsLog +) => { + const logPrefix = 'Could not register function extension. Reason:'; + const app = config.apps[pluginId]; + const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.find(({ title }) => title === metaInfo.title) : null; + + if (!app) { + log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`); + return true; + } + + if (!pluginJsonMetaInfo) { + log.error(`${logPrefix} ${errors.ADDED_FUNCTION_META_INFO_MISSING}`); + return true; + } + + const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets]; + if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) { + log.error(`${logPrefix} ${errors.TARGET_NOT_MATCHING_META_INFO}`); + return true; + } + + if (pluginJsonMetaInfo.description !== metaInfo.description) { + log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO); + } + + return false; +}; + export const isAddedComponentMetaInfoMissing = ( pluginId: string, metaInfo: PluginExtensionAddedComponentConfig, diff --git a/public/app/features/plugins/importPanelPlugin.ts b/public/app/features/plugins/importPanelPlugin.ts index 45732de7fb1..f6d9c1ac2a6 100644 --- a/public/app/features/plugins/importPanelPlugin.ts +++ b/public/app/features/plugins/importPanelPlugin.ts @@ -82,7 +82,6 @@ function getPanelPlugin(meta: PanelPluginMeta): Promise { if (!plugin.panel && plugin.angularPanelCtrl) { plugin.panel = getAngularPanelReactWrapper(plugin); } - return plugin; }) .catch((err) => { diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index aa20ae5881f..98915c40f6d 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -13,7 +13,12 @@ import { DataQuery } from '@grafana/schema'; import { GenericDataSourcePlugin } from '../datasources/types'; import builtInPlugins from './built_in_plugins'; -import { addedComponentsRegistry, addedLinksRegistry, exposedComponentsRegistry } from './extensions/registry/setup'; +import { + addedComponentsRegistry, + addedFunctionsRegistry, + addedLinksRegistry, + exposedComponentsRegistry, +} from './extensions/registry/setup'; import { getPluginFromCache, registerPluginInCache } from './loader/cache'; // SystemJS has to be imported before the sharedDependenciesMap import { SystemJS } from './loader/systemjs'; @@ -153,7 +158,6 @@ export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise, @@ -205,6 +209,10 @@ export async function importAppPlugin(meta: PluginMeta): Promise { pluginId, configs: plugin.addedLinkConfigs || [], }); + addedFunctionsRegistry.register({ + pluginId, + configs: plugin.addedFunctionConfigs || [], + }); importedAppPlugins[pluginId] = plugin; From 5a6d2f2e49b8d7c3cb2032c3fe5e88eeed28d524 Mon Sep 17 00:00:00 2001 From: Misi Date: Thu, 13 Feb 2025 10:46:19 +0100 Subject: [PATCH 550/894] Auth: Add early return if `auth_token` is in the URL for JWT auth (#100539) * Add early return * Update public/app/app.ts Co-authored-by: Victor Cinaglia --------- Co-authored-by: Victor Cinaglia --- public/app/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/app.ts b/public/app/app.ts index cc3a2e5e5c5..86b2386dc6c 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -409,6 +409,7 @@ function handleRedirectTo(): void { if (queryParams.has('auth_token')) { // URL Login should not be redirected window.sessionStorage.removeItem(RedirectToUrlKey); + return; } if (queryParams.has(redirectToParamKey) && window.location.pathname !== '/') { From 293f514854294555670e76f910ed63c7c5514a24 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 13 Feb 2025 11:58:28 +0200 Subject: [PATCH 551/894] Dashboard: Fix removing row repeats having indexes ending with 0 (#100487) --- .../RowRepeaterBehavior.test.tsx | 45 +++++++++++++++++++ .../RowItemRepeaterBehavior.test.tsx | 45 +++++++++++++++++++ .../dashboard-scene/utils/clone.test.ts | 14 ++++++ .../features/dashboard-scene/utils/clone.ts | 2 +- 4 files changed, 105 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.test.tsx index 2e728f25a39..843f58b5640 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.test.tsx @@ -160,6 +160,51 @@ describe('RowRepeaterBehavior', () => { }); }); + describe('Given scene with variable with 15 values', () => { + let scene: DashboardScene, grid: SceneGridLayout; + let gridStateUpdates: unknown[]; + + beforeEach(async () => { + ({ scene, grid } = buildScene({ variableQueryTime: 0 }, [ + { label: 'A', value: 'A1' }, + { label: 'B', value: 'B1' }, + { label: 'C', value: 'C1' }, + { label: 'D', value: 'D1' }, + { label: 'E', value: 'E1' }, + { label: 'F', value: 'F1' }, + { label: 'G', value: 'G1' }, + { label: 'H', value: 'H1' }, + { label: 'I', value: 'I1' }, + { label: 'J', value: 'J1' }, + { label: 'K', value: 'K1' }, + { label: 'L', value: 'L1' }, + { label: 'M', value: 'M1' }, + { label: 'N', value: 'N1' }, + { label: 'O', value: 'O1' }, + ])); + + gridStateUpdates = []; + grid.subscribeToState((state) => gridStateUpdates.push(state)); + + activateFullSceneTree(scene); + await new Promise((r) => setTimeout(r, 1)); + }); + + it('Should handle second repeat cycle and update remove old repeats', async () => { + // should have 15 repeated rows (and the panel above + the row at the bottom) + expect(grid.state.children.length).toBe(17); + + // trigger another repeat cycle by changing the variable + const variable = scene.state.$variables!.state.variables[0] as TestVariable; + variable.changeValueTo(['B1', 'C1']); + + await new Promise((r) => setTimeout(r, 1)); + + // should now only have 2 repeated rows (and the panel above + the row at the bottom) + expect(grid.state.children.length).toBe(4); + }); + }); + describe('Given scene empty row', () => { let scene: DashboardScene; let grid: SceneGridLayout; diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.test.tsx index d152fed3a64..ec5b4710089 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeaterBehavior.test.tsx @@ -104,6 +104,51 @@ describe('RowItemRepeaterBehavior', () => { }); }); + describe('Given scene with variable with 15 values', () => { + let scene: DashboardScene, layout: RowsLayoutManager; + let layoutStateUpdates: unknown[]; + + beforeEach(async () => { + ({ scene, layout } = buildScene({ variableQueryTime: 0 }, [ + { label: 'A', value: 'A1' }, + { label: 'B', value: 'B1' }, + { label: 'C', value: 'C1' }, + { label: 'D', value: 'D1' }, + { label: 'E', value: 'E1' }, + { label: 'F', value: 'F1' }, + { label: 'G', value: 'G1' }, + { label: 'H', value: 'H1' }, + { label: 'I', value: 'I1' }, + { label: 'J', value: 'J1' }, + { label: 'K', value: 'K1' }, + { label: 'L', value: 'L1' }, + { label: 'M', value: 'M1' }, + { label: 'N', value: 'N1' }, + { label: 'O', value: 'O1' }, + ])); + + layoutStateUpdates = []; + layout.subscribeToState((state) => layoutStateUpdates.push(state)); + + activateFullSceneTree(scene); + await new Promise((r) => setTimeout(r, 1)); + }); + + it('Should handle second repeat cycle and update remove old repeats', async () => { + // should have 15 repeated rows (and the panel above) + expect(layout.state.rows.length).toBe(16); + + // trigger another repeat cycle by changing the variable + const variable = scene.state.$variables!.state.variables[0] as TestVariable; + variable.changeValueTo(['B1', 'C1']); + + await new Promise((r) => setTimeout(r, 1)); + + // should now only have 2 repeated rows (and the panel above) + expect(layout.state.rows.length).toBe(3); + }); + }); + describe('Given a scene with empty variable', () => { it('Should preserve repeat row', async () => { const { scene, layout } = buildScene({ variableQueryTime: 0 }, []); diff --git a/public/app/features/dashboard-scene/utils/clone.test.ts b/public/app/features/dashboard-scene/utils/clone.test.ts index 28441c4dbe1..58dcef6fb38 100644 --- a/public/app/features/dashboard-scene/utils/clone.test.ts +++ b/public/app/features/dashboard-scene/utils/clone.test.ts @@ -48,6 +48,20 @@ describe('clone', () => { expect(isClonedKey('tab-clone-1/row-clone-2/panel')).toBe(false); expect(isClonedKey('row-clone-1/panel')).toBe(false); }); + + it('should properly handle indexes containing 0', () => { + expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-0')).toBe(false); + expect(isClonedKey('row-clone-0/panel-clone-0')).toBe(false); + expect(isClonedKey('panel-clone-0')).toBe(false); + + expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-101')).toBe(true); + expect(isClonedKey('row-clone-0/panel-clone-101')).toBe(true); + expect(isClonedKey('panel-clone-1010')).toBe(true); + + expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-10')).toBe(true); + expect(isClonedKey('row-clone-0/panel-clone-100')).toBe(true); + expect(isClonedKey('panel-clone-1000')).toBe(true); + }); }); describe('isClonedKeyOf', () => { diff --git a/public/app/features/dashboard-scene/utils/clone.ts b/public/app/features/dashboard-scene/utils/clone.ts index 4e5d2b79fab..05d9459e0e2 100644 --- a/public/app/features/dashboard-scene/utils/clone.ts +++ b/public/app/features/dashboard-scene/utils/clone.ts @@ -1,7 +1,7 @@ const CLONE_KEY = '-clone-'; const CLONE_SEPARATOR = '/'; -const CLONED_KEY_REGEX = new RegExp(`${CLONE_KEY}[1-9]+$`); +const CLONED_KEY_REGEX = new RegExp(`${CLONE_KEY}[1-9][0-9]*$`); const ORIGINAL_REGEX = new RegExp(`${CLONE_KEY}\\d+$`); /** From 95ee93a0d8d3df333ffc64742c7b525a50b640a1 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Thu, 13 Feb 2025 11:07:24 +0100 Subject: [PATCH 552/894] Plugins: Improve plugin details UX for core plugins (#99830) --- public/app/features/plugins/admin/api.ts | 1 + .../components/PluginDetailsPage.test.tsx | 25 +++++++++++++++++++ .../plugins/admin/components/VersionList.tsx | 2 +- .../admin/hooks/usePluginDetailsTabs.tsx | 6 +++-- .../plugins/admin/hooks/usePluginInfo.tsx | 5 +++- public/app/features/plugins/admin/types.ts | 1 + 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts index ef9411bea9e..36d49c2dbd7 100644 --- a/public/app/features/plugins/admin/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -94,6 +94,7 @@ async function getPluginVersions(id: string, isPublished: boolean): Promise ({ version: v.version, createdAt: v.createdAt, + updatedAt: v.updatedAt, isCompatible: v.isCompatible, grafanaDependency: v.grafanaDependency, angularDetected: v.angularDetected, diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx index 4f1bde85393..ad97440a183 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx @@ -57,6 +57,7 @@ const plugin: CatalogPlugin = { ], grafanaDependency: '>=9.0.0', statusContext: 'stable', + changelog: 'Test changelog', }, angularDetected: false, isFullyInstalled: true, @@ -154,4 +155,28 @@ describe('PluginDetailsPage', () => { render(); expect(screen.getByRole('tab', { name: 'Data source connections' })).toBeVisible(); }); + + it('should not show version and changelog tabs when plugin is core', () => { + mockUseGetSingle.mockReturnValue({ ...plugin, isCore: true }); + render(); + expect(screen.queryByRole('tab', { name: 'Version history' })).not.toBeInTheDocument(); + expect(screen.queryByRole('tab', { name: 'Changelog' })).not.toBeInTheDocument(); + }); + + it('should not show last version in plugin details panel when plugin is core', () => { + config.featureToggles.pluginsDetailsRightPanel = true; + window.matchMedia = jest.fn().mockImplementation((query) => ({ + matches: query !== '(max-width: 600px)', + media: query, + onchange: null, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })); + + mockUseGetSingle.mockReturnValue({ ...plugin, isCore: true, latestVersion: '1.2.0' }); + + render(); + expect(screen.queryByText('Latest Version:')).not.toBeInTheDocument(); + }); }); diff --git a/public/app/features/plugins/admin/components/VersionList.tsx b/public/app/features/plugins/admin/components/VersionList.tsx index aaf98fd183b..2e79b0d43c1 100644 --- a/public/app/features/plugins/admin/components/VersionList.tsx +++ b/public/app/features/plugins/admin/components/VersionList.tsx @@ -96,7 +96,7 @@ export const VersionList = ({ pluginId, versions = [], installedVersion, disable {/* Last updated */} - {dateTimeFormatTimeAgo(version.createdAt)} + {dateTimeFormatTimeAgo(version.updatedAt || version.createdAt)} {/* Dependency */} {version.grafanaDependency || 'N/A'} diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx index e9c50269d15..d9f7c2e1375 100644 --- a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx @@ -42,7 +42,8 @@ export const usePluginDetailsTabs = ( const navModelChildren = useMemo(() => { const canConfigurePlugins = plugin && contextSrv.hasPermissionInMetadata(AccessControlAction.PluginsWrite, plugin); const navModelChildren: NavModelItem[] = []; - if (isPublished) { + // currently the versions available of core plugins are not consistent + if (isPublished && !plugin?.isCore) { navModelChildren.push({ text: PluginTabLabels.VERSIONS, id: PluginTabIds.VERSIONS, @@ -51,7 +52,8 @@ export const usePluginDetailsTabs = ( active: PluginTabIds.VERSIONS === currentPageId, }); } - if (isPublished && plugin?.details?.changelog) { + // currently there is not changelog available for core plugins + if (isPublished && plugin?.details?.changelog && !plugin.isCore) { navModelChildren.push({ text: PluginTabLabels.CHANGELOG, id: PluginTabIds.CHANGELOG, diff --git a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx index 1bb1334e1ef..2c9124f1860 100644 --- a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx @@ -53,7 +53,10 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => { latestVersionValue = latestVersion; } - addInfo('latestVersion', latestVersionValue); + // latest versions of core plugins are not consistent + if (!plugin.isCore) { + addInfo('latestVersion', latestVersionValue); + } } if (Boolean(plugin.orgName)) { diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 10e2636b2ef..031f6c1543c 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -216,6 +216,7 @@ export interface Build { export interface Version { version: string; createdAt: string; + updatedAt?: string; isCompatible: boolean; grafanaDependency: string | null; angularDetected?: boolean; From 0b4c622df8e3796d02b714f7d261215e23178eb6 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 13 Feb 2025 11:27:57 +0100 Subject: [PATCH 553/894] AuthN: Refetch user on "ErrUserAlreadyExists" (#100346) * AuthN: Refetch user on "ErrUserAlreadyExists" --- .../authn/authnimpl/sync/user_sync.go | 41 +++++++++++-------- .../authn/authnimpl/sync/user_sync_test.go | 30 ++++++++++++++ 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/pkg/services/authn/authnimpl/sync/user_sync.go b/pkg/services/authn/authnimpl/sync/user_sync.go index 23e8d579b4d..22ec16fa51a 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync.go +++ b/pkg/services/authn/authnimpl/sync/user_sync.go @@ -86,29 +86,38 @@ func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *auth } // Does user exist in the database? - usr, userAuth, errUserInDB := s.getUser(ctx, id) - if errUserInDB != nil && !errors.Is(errUserInDB, user.ErrUserNotFound) { - s.log.FromContext(ctx).Error("Failed to fetch user", "error", errUserInDB, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) + usr, userAuth, err := s.getUser(ctx, id) + if err != nil && !errors.Is(err, user.ErrUserNotFound) { + s.log.FromContext(ctx).Error("Failed to fetch user", "error", err, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) return errSyncUserInternal.Errorf("unable to retrieve user") } - if errors.Is(errUserInDB, user.ErrUserNotFound) { + if errors.Is(err, user.ErrUserNotFound) { if !id.ClientParams.AllowSignUp { s.log.FromContext(ctx).Warn("Failed to create user, signup is not allowed for module", "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) return errUserSignupDisabled.Errorf("%w", errSignupNotAllowed) } // create user - var errCreate error - usr, errCreate = s.createUser(ctx, id) - if errCreate != nil { - s.log.FromContext(ctx).Error("Failed to create user", "error", errCreate, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) - return errSyncUserInternal.Errorf("unable to create user: %w", errCreate) + usr, err = s.createUser(ctx, id) + + // There is a possibility for a race condition when creating a user. Most clients will probably not hit this + // case but others will. The one we have seen this issue for is auth proxy. First time a new user loads grafana + // several requests can get "user.ErrUserNotFound" at the same time but only one of the request will be allowed + // to actually create the user, resulting in all other requests getting "user.ErrUserAlreadyExists". So we can + // just try to fetch the user one more to make the other request work. + if errors.Is(err, user.ErrUserAlreadyExists) { + usr, _, err = s.getUser(ctx, id) + } + + if err != nil { + s.log.FromContext(ctx).Error("Failed to create user", "error", err, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) + return errSyncUserInternal.Errorf("unable to create user: %w", err) } } else { // update user - if errUpdate := s.updateUserAttributes(ctx, usr, id, userAuth); errUpdate != nil { - s.log.FromContext(ctx).Error("Failed to update user", "error", errUpdate, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) + if err := s.updateUserAttributes(ctx, usr, id, userAuth); err != nil { + s.log.FromContext(ctx).Error("Failed to update user", "error", err, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) return errSyncUserInternal.Errorf("unable to update user") } } @@ -311,6 +320,7 @@ func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.User, error) { ctx, span := s.tracer.Start(ctx, "user.sync.createUser") defer span.End() + // FIXME(jguer): this should be done in the user service // quota check: we can have quotas on both global and org level // therefore we need to query check quota for both user and org services @@ -330,19 +340,18 @@ func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.Us isAdmin = *id.IsGrafanaAdmin } - usr, errCreateUser := s.userService.Create(ctx, &user.CreateUserCommand{ + usr, err := s.userService.Create(ctx, &user.CreateUserCommand{ Login: id.Login, Email: id.Email, Name: id.Name, IsAdmin: isAdmin, SkipOrgSetup: len(id.OrgRoles) > 0, }) - if errCreateUser != nil { - return nil, errCreateUser + if err != nil { + return nil, err } - err := s.upsertAuthConnection(ctx, usr.ID, id, true) - if err != nil { + if err := s.upsertAuthConnection(ctx, usr.ID, id, true); err != nil { return nil, err } diff --git a/pkg/services/authn/authnimpl/sync/user_sync_test.go b/pkg/services/authn/authnimpl/sync/user_sync_test.go index dc6cab243b7..8999a4b6979 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/user_sync_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" claims "github.com/grafana/authlib/types" @@ -451,6 +452,35 @@ func TestUserSync_SyncUserHook(t *testing.T) { } } +func TestUserSync_SyncUserRetryFetch(t *testing.T) { + userSrv := usertest.NewMockService(t) + userSrv.On("GetByEmail", mock.Anything, mock.Anything).Return(nil, user.ErrUserNotFound).Once() + userSrv.On("Create", mock.Anything, mock.Anything).Return(nil, user.ErrUserAlreadyExists).Once() + userSrv.On("GetByEmail", mock.Anything, mock.Anything).Return(&user.User{ID: 1}, nil).Once() + + s := ProvideUserSync( + userSrv, + authinfoimpl.ProvideOSSUserProtectionService(), + &authinfotest.FakeService{}, + "atest.FakeQuotaService{}, + tracing.NewNoopTracerService(), + featuremgmt.WithFeatures(), + ) + + email := "test@test.com" + + err := s.SyncUserHook(context.Background(), &authn.Identity{ + ClientParams: authn.ClientParams{ + SyncUser: true, + AllowSignUp: true, + LookUpParams: login.UserLookupParams{ + Email: &email, + }, + }, + }, nil) + require.NoError(t, err) +} + func TestUserSync_FetchSyncedUserHook(t *testing.T) { type testCase struct { desc string From 6db155649c255fc673a9906476bebbf4813ab48f Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Thu, 13 Feb 2025 11:31:57 +0100 Subject: [PATCH 554/894] Plugins: Custom links for plugin details page (#97186) * Custom links with repository link, licence link, docs link and raise an issue link * run translation command * delete console log * delete console log * fix frontend tests * change UI with a new design * remove license, documentation, repository url calculation logic from grafana * remove unsused function from helpers * change repo icons and raise an issue icon * fix the build * remove logic for raiseAnIssueUrl * fix the build * fix lint * Delete Links title in the box of links --------- Co-authored-by: Timur Olzhabayev --- .betterer.results | 3 +- public/app/features/plugins/admin/api.ts | 2 + .../components/PluginDetailsPanel.test.tsx | 3 +- .../admin/components/PluginDetailsPanel.tsx | 260 ++++++++++++++---- .../features/plugins/admin/helpers.test.ts | 2 + public/app/features/plugins/admin/helpers.ts | 8 + public/app/features/plugins/admin/types.ts | 8 + public/locales/en-US/grafana.json | 15 +- public/locales/pseudo-LOCALE/grafana.json | 15 +- 9 files changed, 253 insertions(+), 63 deletions(-) diff --git a/.betterer.results b/.betterer.results index 89b413d6023..8db179df663 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5501,7 +5501,8 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], "public/app/features/plugins/admin/components/PluginDetailsPanel.tsx:5381": [ - [0, 0, 0, "\'@grafana/runtime/src/components/PluginPage\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] + [0, 0, 0, "\'@grafana/runtime/src/components/PluginPage\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/plugins/admin/components/PluginDetailsSignature.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts index 36d49c2dbd7..f79968d47ae 100644 --- a/public/app/features/plugins/admin/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -37,6 +37,8 @@ export async function getPluginDetails(id: string): Promise { it('should render report abuse section for non-core plugins', () => { render(); expect(screen.getByText('Report a concern')).toBeInTheDocument(); - expect(screen.getByText('Contact Grafana Labs')).toBeInTheDocument(); }); it('should not render report abuse section for core plugins', () => { @@ -117,6 +116,6 @@ describe('PluginDetailsPanel', () => { it('should respect custom width prop', () => { render(); const panel = screen.getByTestId('plugin-details-panel'); - expect(panel).toHaveStyle({ maxWidth: '300px' }); + expect(panel).toHaveStyle({ width: '300px' }); }); }); diff --git a/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx b/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx index 95b963097b7..7d7612dc814 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx @@ -1,8 +1,22 @@ import { css } from '@emotion/css'; +import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { PageInfoItem } from '@grafana/runtime/src/components/PluginPage'; -import { Stack, Text, LinkButton, Box, TextLink, useStyles2 } from '@grafana/ui'; +import { + Stack, + Text, + LinkButton, + Box, + TextLink, + CollapsableSection, + Tooltip, + Icon, + Modal, + Button, + useStyles2, +} from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import { formatDate } from 'app/core/internationalization/dates'; @@ -16,73 +30,203 @@ type Props = { export function PluginDetailsPanel(props: Props): React.ReactElement | null { const { pluginExtentionsInfo, plugin, width = '250px' } = props; + const [reportAbuseModalOpen, setReportAbuseModalOpen] = useState(false); + + const normalizeURL = (url: string | undefined) => url?.replace(/\/$/, ''); + + const customLinks = plugin.details?.links?.filter((link) => { + const customLinksFiltered = ![plugin.url, plugin.details?.licenseUrl, plugin.details?.documentationUrl] + .map(normalizeURL) + .includes(normalizeURL(link.url)); + return customLinksFiltered; + }); + const shouldRenderLinks = plugin.url || plugin.details?.licenseUrl || plugin.details?.documentationUrl; + const styles = useStyles2(getStyles); - return ( - - - - {pluginExtentionsInfo.map((infoItem, index) => { - return ( - - {infoItem.label + ':'} -

          {infoItem.value}
          - - ); - })} - {plugin.updatedAt && ( - - - Last updated: - {' '} - {formatDate(new Date(plugin.updatedAt), { day: 'numeric', month: 'short', year: 'numeric' })} - - )} - {plugin?.details?.lastCommitDate && ( - - - Last commit date: - {' '} - - {formatDate(new Date(plugin.details.lastCommitDate), { - day: 'numeric', - month: 'short', - year: 'numeric', - })} - - - )} - - + const onClickReportConcern = (pluginId: string) => { + setReportAbuseModalOpen(true); + reportInteraction('plugin_detail_report_concern', { + plugin_id: pluginId, + }); + }; - {plugin?.details?.links && plugin.details?.links?.length > 0 && ( + return ( + <> + - - Links - - {plugin.details.links.map((link, index) => ( - - {link.name} - - ))} + {pluginExtentionsInfo.map((infoItem, index) => { + return ( + + {infoItem.label + ':'} +
          {infoItem.value}
          +
          + ); + })} + {plugin.updatedAt && ( + + + Last updated: + {' '} + + {formatDate(new Date(plugin.updatedAt), { day: 'numeric', month: 'short', year: 'numeric' })} + + + )} + {plugin?.details?.lastCommitDate && ( + + + Last commit date: + {' '} + + {formatDate(new Date(plugin.details.lastCommitDate), { + day: 'numeric', + month: 'short', + year: 'numeric', + })} + + + )}
          - )} - - {!plugin?.isCore && ( - - - - Report a concern + {shouldRenderLinks && ( + <> + + + {plugin.url && ( + + Repository + + )} + {plugin.raiseAnIssueUrl && ( + + Raise an issue + + )} + {plugin.details?.licenseUrl && ( + + License + + )} + {plugin.details?.documentationUrl && ( + + Documentation + + )} + + + + )} + {customLinks && customLinks?.length > 0 && ( + + + + Custom links + + + These links are provided by the plugin developer to offer additional, developer-specific + resources and information +
          + } + placement="right-end" + > + + + + } + > + + {customLinks.map((link, index) => ( + + {link.name} + + ))} + + + + )} + {!plugin?.isCore && ( + + + + Report a concern + + + Report issues related to malicious or harmful plugins directly to Grafana Labs. +
          + } + placement="right-end" + > + + + + } + > + + + + + + )} + + {reportAbuseModalOpen && ( + Report a plugin concern
          } + isOpen + onDismiss={() => setReportAbuseModalOpen(false)} + > + + + + This feature is for reporting malicious or harmful behaviour within plugins. For plugin concerns, email + us at:{' '} + + integrations@grafana.com + + + + Note: For general plugin issues like bugs or feature requests, please contact the plugin author using + the provided links.{' '} + - - Contact Grafana Labs - - + + + + + )} - + ); } diff --git a/public/app/features/plugins/admin/helpers.test.ts b/public/app/features/plugins/admin/helpers.test.ts index b47c5bf774b..b1b109f2ca5 100644 --- a/public/app/features/plugins/admin/helpers.test.ts +++ b/public/app/features/plugins/admin/helpers.test.ts @@ -217,6 +217,7 @@ describe('Plugins/Helpers', () => { updatedAt: '2021-05-18T14:53:01.000Z', isFullyInstalled: false, angularDetected: false, + url: 'https://github.com/alexanderzobnin/grafana-zabbix', }); }); @@ -354,6 +355,7 @@ describe('Plugins/Helpers', () => { installedVersion: '4.2.2', isFullyInstalled: true, angularDetected: false, + url: 'https://github.com/alexanderzobnin/grafana-zabbix', }); }); diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 172a32e088f..2f7814cc729 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -121,6 +121,8 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C signatureType, versionSignatureType, versionSignedByOrgName, + url, + raiseAnIssueUrl, } = plugin; const isDisabled = !!error || isDisabledSecretsPlugin(typeCode); @@ -158,6 +160,8 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C angularDetected, isFullyInstalled: isDisabled, latestVersion: plugin.version, + url, + raiseAnIssueUrl, }; } @@ -174,6 +178,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat hasUpdate, accessControl, angularDetected, + raiseAnIssueUrl, } = plugin; const isDisabled = !!error || isDisabledSecretsPlugin(type); @@ -208,6 +213,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat isFullyInstalled: true, iam: plugin.iam, latestVersion: plugin.latestVersion, + raiseAnIssueUrl, }; } @@ -271,6 +277,8 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e isFullyInstalled: Boolean(local) || isDisabled, iam: local?.iam, latestVersion: local?.latestVersion || remote?.version || '', + url: remote?.url || '', + raiseAnIssueUrl: remote?.raiseAnIssueUrl || local?.raiseAnIssueUrl, }; } diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 031f6c1543c..f3d5783ae3c 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -64,6 +64,8 @@ export interface CatalogPlugin extends WithAccessControlMetadata { isUpdatingFromInstance?: boolean; iam?: IdentityAccessManagement; isProvisioned?: boolean; + url?: string; + raiseAnIssueUrl?: string; } export interface CatalogPluginDetails { @@ -79,6 +81,8 @@ export interface CatalogPluginDetails { iam?: IdentityAccessManagement; changelog?: string; lastCommitDate?: string; + licenseUrl?: string; + documentationUrl?: string; signatureType?: PluginSignatureType; signature?: PluginSignatureStatus; } @@ -143,6 +147,9 @@ export type RemotePlugin = { versionStatus: string; angularDetected?: boolean; lastCommitDate?: string; + licenseUrl?: string; + documentationUrl?: string; + raiseAnIssueUrl?: string; }; // The available status codes on GCOM are available here: @@ -190,6 +197,7 @@ export type LocalPlugin = WithAccessControlMetadata & { dependencies: PluginDependencies; angularDetected: boolean; iam?: IdentityAccessManagement; + raiseAnIssueUrl?: string; }; interface IdentityAccessManagement { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2c90bf29511..b7ea5026077 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2781,17 +2781,30 @@ }, "labels": { "contactGrafanaLabs": "Contact Grafana Labs", + "customLinks": "Custom links ", + "customLinksTooltip": "These links are provided by the plugin developer to offer additional, developer-specific resources and information", "dependencies": "Dependencies", + "documentation": "Documentation", "downloads": "Downloads", "from": "From", "installedVersion": "Installed Version", "lastCommitDate": "Last commit date:", "latestVersion": "Latest Version", - "links": "Links ", + "license": "License", + "raiseAnIssue": "Raise an issue", "reportAbuse": "Report a concern ", + "reportAbuseTooltip": "Report issues related to malicious or harmful plugins directly to Grafana Labs.", + "repository": "Repository", "signature": "Signature", "status": "Status", "updatedAt": "Last updated:" + }, + "modal": { + "cancel": "Cancel", + "copyEmail": "Copy email address", + "description": "This feature is for reporting malicious or harmful behaviour within plugins. For plugin concerns, email us at: ", + "node": "Note: For general plugin issues like bugs or feature requests, please contact the plugin author using the provided links. ", + "title": "Report a plugin concern" } }, "empty-state": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 61b47f9f9a5..9543a62f07e 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2781,17 +2781,30 @@ }, "labels": { "contactGrafanaLabs": "Cőʼnŧäčŧ Ğřäƒäʼnä Ŀäþş", + "customLinks": "Cūşŧőm ľįʼnĸş ", + "customLinksTooltip": "Ŧĥęşę ľįʼnĸş äřę přővįđęđ þy ŧĥę pľūģįʼn đęvęľőpęř ŧő őƒƒęř äđđįŧįőʼnäľ, đęvęľőpęř-şpęčįƒįč řęşőūřčęş äʼnđ įʼnƒőřmäŧįőʼn", "dependencies": "Đępęʼnđęʼnčįęş", + "documentation": "Đőčūmęʼnŧäŧįőʼn", "downloads": "Đőŵʼnľőäđş", "from": "Fřőm", "installedVersion": "Ĩʼnşŧäľľęđ Vęřşįőʼn", "lastCommitDate": "Ŀäşŧ čőmmįŧ đäŧę:", "latestVersion": "Ŀäŧęşŧ Vęřşįőʼn", - "links": "Ŀįʼnĸş ", + "license": "Ŀįčęʼnşę", + "raiseAnIssue": "Ŗäįşę äʼn įşşūę", "reportAbuse": "Ŗępőřŧ ä čőʼnčęřʼn ", + "reportAbuseTooltip": "Ŗępőřŧ įşşūęş řęľäŧęđ ŧő mäľįčįőūş őř ĥäřmƒūľ pľūģįʼnş đįřęčŧľy ŧő Ğřäƒäʼnä Ŀäþş.", + "repository": "Ŗępőşįŧőřy", "signature": "Ŝįģʼnäŧūřę", "status": "Ŝŧäŧūş", "updatedAt": "Ŀäşŧ ūpđäŧęđ:" + }, + "modal": { + "cancel": "Cäʼnčęľ", + "copyEmail": "Cőpy ęmäįľ äđđřęşş", + "description": "Ŧĥįş ƒęäŧūřę įş ƒőř řępőřŧįʼnģ mäľįčįőūş őř ĥäřmƒūľ þęĥävįőūř ŵįŧĥįʼn pľūģįʼnş. Főř pľūģįʼn čőʼnčęřʼnş, ęmäįľ ūş äŧ: ", + "node": "Ńőŧę: Főř ģęʼnęřäľ pľūģįʼn įşşūęş ľįĸę þūģş őř ƒęäŧūřę řęqūęşŧş, pľęäşę čőʼnŧäčŧ ŧĥę pľūģįʼn äūŧĥőř ūşįʼnģ ŧĥę přővįđęđ ľįʼnĸş. ", + "title": "Ŗępőřŧ ä pľūģįʼn čőʼnčęřʼn" } }, "empty-state": { From ae9837b793e97f683d1517d270b719c99656e556 Mon Sep 17 00:00:00 2001 From: Tito Lins Date: Thu, 13 Feb 2025 11:36:45 +0100 Subject: [PATCH 555/894] Alerting: Add alertmanager integration tests (#100106) --- .github/CODEOWNERS | 1 + Makefile | 8 + .../docker/blocks/stateful_webhook/Dockerfile | 12 + .../stateful_webhook/docker-compose.yaml | 5 + devenv/docker/blocks/stateful_webhook/main.go | 149 +++++++ go.mod | 2 + go.sum | 4 + go.work.sum | 1 + .../alertmanager/alertmanager_scenario.go | 386 ++++++++++++++++++ pkg/tests/alertmanager/alertmanager_test.go | 97 +++++ pkg/tests/alertmanager/grafana.go | 75 ++++ pkg/tests/alertmanager/loki.go | 152 +++++++ pkg/tests/alertmanager/postgres.go | 41 ++ pkg/tests/alertmanager/webhook.go | 85 ++++ ...na_alertmanager_integration_test_images.go | 41 ++ 15 files changed, 1059 insertions(+) create mode 100644 devenv/docker/blocks/stateful_webhook/Dockerfile create mode 100644 devenv/docker/blocks/stateful_webhook/docker-compose.yaml create mode 100644 devenv/docker/blocks/stateful_webhook/main.go create mode 100644 pkg/tests/alertmanager/alertmanager_scenario.go create mode 100644 pkg/tests/alertmanager/alertmanager_test.go create mode 100644 pkg/tests/alertmanager/grafana.go create mode 100644 pkg/tests/alertmanager/loki.go create mode 100644 pkg/tests/alertmanager/postgres.go create mode 100644 pkg/tests/alertmanager/webhook.go create mode 100644 tools/setup_grafana_alertmanager_integration_test_images.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cbb2fa1a54f..a349655800b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -241,6 +241,7 @@ /devenv/dev-dashboards/extensions/ @grafana/plugins-platform-frontend /devenv/docker/blocks/alert_webhook_listener/ @grafana/alerting-backend +/devenv/docker/blocks/stateful_webhook/ @grafana/alerting-backend /devenv/docker/blocks/caddy_tls/ @grafana/alerting-backend /devenv/docker/blocks/clickhouse/ @grafana/partner-datasources /devenv/docker/blocks/collectd/ @grafana/observability-metrics diff --git a/Makefile b/Makefile index 2721c2830b8..11cee0a4255 100644 --- a/Makefile +++ b/Makefile @@ -271,6 +271,14 @@ test-go-integration-alertmanager: ## Run integration tests for the remote alertm AM_URL=http://localhost:8080 AM_TENANT_ID=test \ $(GO) test $(GO_RACE_FLAG) -count=1 -run "^TestIntegrationRemoteAlertmanager" -covermode=atomic -timeout=5m ./pkg/services/ngalert/... +.PHONY: test-go-integration-grafana-alertmanager +test-go-integration-grafana-alertmanager: ## Run integration tests for the grafana alertmanager + @echo "test grafana alertmanager integration tests" + @export GRAFANA_VERSION=11.5.0-81938; \ + $(GO) run tools/setup_grafana_alertmanager_integration_test_images.go; \ + $(GO) clean -testcache; \ + $(GO) test $(GO_RACE_FLAG) -count=1 -run "^TestAlertmanagerIntegration" -covermode=atomic -timeout=10m ./pkg/tests/alertmanager/... + .PHONY: test-go-integration-postgres test-go-integration-postgres: devenv-postgres ## Run integration tests for postgres backend with flags. @echo "test backend integration postgres tests" diff --git a/devenv/docker/blocks/stateful_webhook/Dockerfile b/devenv/docker/blocks/stateful_webhook/Dockerfile new file mode 100644 index 00000000000..03b50db2135 --- /dev/null +++ b/devenv/docker/blocks/stateful_webhook/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.23.5 + +ADD main.go /go/src/webhook/main.go + +WORKDIR /go/src/webhook + +RUN mkdir /tmp/logs +RUN go build -o /bin main.go + +ENV PORT=8080 + +ENTRYPOINT [ "/bin/main" ] diff --git a/devenv/docker/blocks/stateful_webhook/docker-compose.yaml b/devenv/docker/blocks/stateful_webhook/docker-compose.yaml new file mode 100644 index 00000000000..7217516c4e9 --- /dev/null +++ b/devenv/docker/blocks/stateful_webhook/docker-compose.yaml @@ -0,0 +1,5 @@ + stateful_webhook: + build: + context: docker/blocks/stateful_webhook + ports: + - "8080:8080" diff --git a/devenv/docker/blocks/stateful_webhook/main.go b/devenv/docker/blocks/stateful_webhook/main.go new file mode 100644 index 00000000000..926cf4e593e --- /dev/null +++ b/devenv/docker/blocks/stateful_webhook/main.go @@ -0,0 +1,149 @@ +package main + +import ( + "encoding/json" + "io" + "log" + "net/http" + "strings" + "sync" + "time" +) + +type Event struct { + Status string `json:"status"` + TimeNow time.Time `json:"timeNow"` + StartsAt time.Time `json:"startsAt"` + Node string `json:"node"` + DeltaLastSeconds float64 `json:"deltaLastSeconds"` + DeltaStartSeconds float64 `json:"deltaStartSeconds"` +} + +type Notification struct { + Alerts []Alert `json:"alerts"` + CommonAnnotations map[string]string `json:"commonAnnotations"` + CommonLabels map[string]string `json:"commonLabels"` + ExternalURL string `json:"externalURL"` + GroupKey string `json:"groupKey"` + GroupLabels map[string]string `json:"groupLabels"` + Message string `json:"message"` + OrgID int `json:"orgId"` + Receiver string `json:"receiver"` + State string `json:"state"` + Status string `json:"status"` + Title string `json:"title"` + TruncatedAlerts int `json:"truncatedAlerts"` + Version string `json:"version"` +} + +type Alert struct { + Annotations map[string]string `json:"annotations"` + DashboardURL string `json:"dashboardURL"` + StartsAt time.Time `json:"startsAt"` + EndsAt time.Time `json:"endsAt"` + Fingerprint string `json:"fingerprint"` + GeneratorURL string `json:"generatorURL"` + Labels map[string]string `json:"labels"` + PanelURL string `json:"panelURL"` + SilenceURL string `json:"silenceURL"` + Status string `json:"status"` + ValueString string `json:"valueString"` + Values map[string]any `json:"values"` +} + +type NotificationHandler struct { + startedAt time.Time + stats map[string]int + hist []Event + m sync.Mutex +} + +func NewNotificationHandler() *NotificationHandler { + return &NotificationHandler{ + startedAt: time.Now(), + stats: make(map[string]int), + hist: make([]Event, 0), + } +} + +func (ah *NotificationHandler) Notify(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + return + } + n := Notification{} + if err := json.Unmarshal(b, &n); err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + return + } + log.Printf("got notification from: %s. a: %v", r.RemoteAddr, n) + + ah.m.Lock() + defer ah.m.Unlock() + + addr := r.RemoteAddr + if split := strings.Split(r.RemoteAddr, ":"); len(split) > 0 { + addr = split[0] + } + + a := n.Alerts[0] + + timeNow := time.Now() + + ah.stats[n.Status]++ + + var d time.Duration + if len(ah.hist) > 0 { + last := ah.hist[len(ah.hist)-1] + d = timeNow.Sub(last.TimeNow) + } + + ah.hist = append(ah.hist, Event{ + Status: n.Status, + StartsAt: a.StartsAt, + TimeNow: timeNow, + Node: addr, + DeltaLastSeconds: d.Seconds(), + DeltaStartSeconds: timeNow.Sub(ah.startedAt).Seconds(), + }) +} + +func (ah *NotificationHandler) GetNotifications(w http.ResponseWriter, _ *http.Request) { + ah.m.Lock() + defer ah.m.Unlock() + w.Header().Set("Content-Type", "application/json") + + res, err := json.MarshalIndent(map[string]any{"stats": ah.stats, "history": ah.hist}, "", "\t") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + //nolint:errcheck + w.Write([]byte(`{"error":"failed to marshal alerts"}`)) + log.Printf("failed to marshal alerts: %v\n", err) + return + } + + log.Printf("requested current state\n%v\n", string(res)) + + _, err = w.Write(res) + if err != nil { + log.Printf("failed to write response: %v\n", err) + } +} + +func main() { + ah := NewNotificationHandler() + + http.HandleFunc("/ready", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + http.HandleFunc("/notify", ah.Notify) + http.HandleFunc("/notifications", ah.GetNotifications) + + log.Println("Listening") + //nolint:errcheck + http.ListenAndServe("0.0.0.0:8080", nil) +} diff --git a/go.mod b/go.mod index 3ded590b054..9b05a5c2234 100644 --- a/go.mod +++ b/go.mod @@ -217,6 +217,8 @@ require ( github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-search-and-storage ) +require github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend + require ( cel.dev/expr v0.19.0 // indirect cloud.google.com/go v0.116.0 // indirect diff --git a/go.sum b/go.sum index f73dcbe3a18..8f98d32599f 100644 --- a/go.sum +++ b/go.sum @@ -1297,6 +1297,8 @@ github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I= github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= +github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= @@ -1527,6 +1529,8 @@ github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 h1:jxJJ5z0GxqhWFbQU github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447/go.mod h1:IxsY6mns6Q5sAnWcrptrgUrSglTZJXH/kXr9nbpb/9I= github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e h1:UlEET0InuoFautfaFp8lDrNF7rPHYXuBMrzwWx9XqFY= github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU= +github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= +github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= github.com/grafana/grafana-app-sdk v0.31.0 h1:/mFCcx+YqG8cWAi9hePDJQxIdtXDClDIDRgZwHkksFk= github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs= github.com/grafana/grafana-app-sdk/logging v0.30.0 h1:K/P/bm7Cp7Di4tqIJ3EQz2+842JozQGRaz62r95ApME= diff --git a/go.work.sum b/go.work.sum index 1be6908c9d8..832ae031f57 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1455,6 +1455,7 @@ github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDs github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= diff --git a/pkg/tests/alertmanager/alertmanager_scenario.go b/pkg/tests/alertmanager/alertmanager_scenario.go new file mode 100644 index 00000000000..719cccef4ab --- /dev/null +++ b/pkg/tests/alertmanager/alertmanager_scenario.go @@ -0,0 +1,386 @@ +package alertmanager + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/grafana/e2e" + gapi "github.com/grafana/grafana-api-golang-client" + "github.com/stretchr/testify/require" +) + +const ( + defaultNetworkName = "e2e-grafana-am" +) + +type AlertRuleConfig struct { + PendingPeriod string + GroupEvaluationIntervalSeconds int64 +} + +type NotificationPolicyCfg struct { + GroupWait string + GroupInterval string + RepeatInterval string +} + +type ProvisionCfg struct { + AlertRuleConfig + NotificationPolicyCfg +} + +// AlertmanagerScenario is a helper for writing tests which require some number of AM +// configured to communicate with some number of Grafana instances. +type AlertmanagerScenario struct { + *e2e.Scenario + + Grafanas map[string]*GrafanaService + Webhook *WebhookService + Postgres *PostgresService + Loki *LokiService +} + +func NewAlertmanagerScenario() (*AlertmanagerScenario, error) { + s, err := e2e.NewScenario(getNetworkName()) + if err != nil { + return nil, err + } + + return &AlertmanagerScenario{ + Scenario: s, + Grafanas: make(map[string]*GrafanaService), + }, nil +} + +// Setup starts a Grafana AM cluster of size n and all required dependencies +func (s *AlertmanagerScenario) Start(t *testing.T, n int, peerTimeout string, stopOnExtraDedup bool) { + is := getInstances(n) + ips := mapInstancePeers(is) + + // start dependencies in one go + require.NoError( + t, + s.StartAndWaitReady([]e2e.Service{ + s.NewWebhookService("webhook"), + s.NewLokiService("loki"), + s.NewPostgresService("postgres"), + }...), + ) + + for i, ps := range ips { + require.NoError(t, s.StartAndWaitReady(s.NewGrafanaService(i, ps, peerTimeout, stopOnExtraDedup))) + } + + // wait for instances to come online and cluster to be properly configured + time.Sleep(30 * time.Second) +} + +// Provision provisions all required resources for the test +func (s *AlertmanagerScenario) Provision(t *testing.T, cfg ProvisionCfg) { //}*GrafanaClient { + c, err := s.NewGrafanaClient("grafana-1", 1) + require.NoError(t, err) + + dsUID := "integration-testdata" + + // setup resources + _, err = c.NewDataSource(&gapi.DataSource{ + Name: "grafana-testdata-datasource", + Type: "grafana-testdata-datasource", + Access: "proxy", + UID: dsUID, + }) + require.NoError(t, err) + + // setup loki for state history + _, err = c.NewDataSource(&gapi.DataSource{ + Name: "loki", + Type: "loki", + URL: "http://loki:3100", + Access: "proxy", + }) + require.NoError(t, err) + + _, err = c.NewContactPoint(&gapi.ContactPoint{ + Name: "webhook", + Type: "webhook", + Settings: map[string]any{ + "url": "http://webhook:8080/notify", + }, + }) + require.NoError(t, err) + + require.NoError(t, c.SetNotificationPolicyTree(&gapi.NotificationPolicyTree{ + Receiver: "webhook", + GroupWait: cfg.GroupWait, + GroupInterval: cfg.GroupInterval, + RepeatInterval: cfg.RepeatInterval, + })) + + f, err := c.NewFolder("integration_test") + require.NoError(t, err) + + r := &gapi.AlertRule{ + Title: "integration rule", + Condition: "C", + FolderUID: f.UID, + ExecErrState: gapi.ErrError, + NoDataState: gapi.NoData, + For: cfg.PendingPeriod, + RuleGroup: "test", + Data: []*gapi.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: gapi.RelativeTimeRange{ + From: 600, + To: 0, + }, + DatasourceUID: dsUID, + Model: json.RawMessage(fmt.Sprintf(`{ + "refId":"A", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "%s" + }, + "hide":false, + "range":false, + "instant":true, + "intervalMs":1000, + "maxDataPoints":43200, + "pulseWave": { + "offCount": 6, + "offValue": 0, + "onCount": 10, + "onValue": 10, + "timeStep": 10 + }, + "refId": "A", + "scenarioId": "predictable_pulse", + "seriesCount": 1 + }`, dsUID)), + }, + { + RefID: "B", + RelativeTimeRange: gapi.RelativeTimeRange{ + From: 0, + To: 0, + }, + DatasourceUID: "__expr__", + Model: json.RawMessage(`{ + "conditions": [ + { + "evaluator": { + "params": [ + 0, + 0 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "datasource": { + "name": "Expression", + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "A", + "intervalMs": 1000, + "maxDataPoints": 43200, + "reducer": "last", + "refId": "B", + "type": "reduce" + }`), + }, + { + RefID: "C", + RelativeTimeRange: gapi.RelativeTimeRange{ + From: 0, + To: 0, + }, + DatasourceUID: "__expr__", + Model: json.RawMessage(`{ + "conditions": [ + { + "evaluator": { + "params": [ + 0, + 0 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "B" + ] + }, + "reducer": { + "params": [], + "type": "last" + }, + "type": "query" + } + ], + "datasource": { + "type": "__expr__", + "uid": "__expr__" + }, + "hide": false, + "isPaused": false, + "intervalMs": 1000, + "maxDataPoints": 43200, + "refId": "C", + "expression": "B", + "type": "threshold" + }`), + }, + }, + } + _, err = c.NewAlertRule(r) + require.NoError(t, err) + + require.NoError(t, c.SetAlertRuleGroup(gapi.RuleGroup{ + Title: "test", + FolderUID: f.UID, + Interval: cfg.GroupEvaluationIntervalSeconds, + Rules: []gapi.AlertRule{*r}, + })) +} + +// NewGrafanaService creates a new Grafana instance. +func (s *AlertmanagerScenario) NewGrafanaService(name string, peers []string, peerTimeout string, stopOnExtraDedup bool) *GrafanaService { + flags := map[string]string{} + + ft := []string{ + "alertStateHistoryLokiSecondary", + "alertStateHistoryLokiPrimary", + "alertStateHistoryLokiOnly", + "alertingAlertmanagerExtraDedupStage", + } + if stopOnExtraDedup { + ft = append(ft, "alertingAlertmanagerExtraDedupStageStopPipeline") + } + envVars := map[string]string{ + //"GF_LOG_MODE": "file", // disable console logging + "GF_LOG_LEVEL": "warn", + "GF_FEATURE_TOGGLES_ENABLE": strings.Join(ft, ","), + "GF_UNIFIED_ALERTING_ENABLED": "true", + "GF_UNIFIED_ALERTING_EXECUTE_ALERTS": "true", + "GF_UNIFIED_ALERTING_HA_PEER_TIMEOUT": peerTimeout, + "GF_UNIFIED_ALERTING_HA_RECONNECT_TIMEOUT": "2m", + "GF_UNIFIED_ALERTING_HA_LISTEN_ADDRESS": ":9094", + "GF_UNIFIED_ALERTING_HA_PEERS": strings.Join(peers, ","), + "GF_UNIFIED_ALERTING_STATE_HISTORY_ENABLED": "true", + "GF_UNIFIED_ALERTING_STATE_HISTORY_BACKEND": "loki", + "GF_UNIFIED_ALERTING_STATE_HISTORY_LOKI_REMOTE_URL": "http://loki:3100", + "GF_DATABASE_TYPE": "postgres", + "GF_DATABASE_HOST": "postgres:5432", + "GF_DATABASE_NAME": "grafana", + "GF_DATABASE_USER": "postgres", + "GF_DATABASE_PASSWORD": "password", + "GF_DATABASE_SSL_MODE": "disable", + } + + g := NewGrafanaService(name, flags, envVars) + + s.Grafanas[name] = g + return g +} + +// NewGrafanaService creates a new Grafana API client for the requested instance. +func (s *AlertmanagerScenario) NewGrafanaClient(grafanaName string, orgID int64) (*GrafanaClient, error) { + g, ok := s.Grafanas[grafanaName] + if !ok { + return nil, fmt.Errorf("unknown grafana instance: %s", grafanaName) + } + + return NewGrafanaClient(g.HTTPEndpoint(), orgID) +} + +func (s *AlertmanagerScenario) NewWebhookClient() (*WebhookClient, error) { + return NewWebhookClient("http://" + s.Webhook.HTTPEndpoint()) +} + +func (s *AlertmanagerScenario) NewWebhookService(name string) *WebhookService { + ws := NewWebhookService(name, nil, nil) + s.Webhook = ws + + return ws +} + +func (s *AlertmanagerScenario) NewLokiService(name string) *LokiService { + ls := NewLokiService(name, map[string]string{"--config.file": "/etc/loki/local-config.yaml"}, nil) + s.Loki = ls + + return ls +} + +func (s *AlertmanagerScenario) NewPostgresService(name string) *PostgresService { + ps := NewPostgresService(name, map[string]string{"POSTGRES_PASSWORD": "password", "POSTGRES_DB": "grafana"}) + s.Postgres = ps + + return ps +} + +func (s *AlertmanagerScenario) NewLokiClient() (*LokiClient, error) { + return NewLokiClient("http://" + s.Loki.HTTPEndpoint()) +} + +func getNetworkName() string { + // If the E2E_NETWORK_NAME is set, use that for the network name. + // Otherwise, return the default network name. + if os.Getenv("E2E_NETWORK_NAME") != "" { + return os.Getenv("E2E_NETWORK_NAME") + } + + return defaultNetworkName +} + +func getInstances(n int) []string { + is := make([]string, n) + + for i := 0; i < n; i++ { + is[i] = "grafana-" + strconv.Itoa(i+1) + } + + return is +} + +func getPeers(i string, is []string) []string { + peers := make([]string, 0, len(is)-1) + + for _, p := range is { + if p != i { + peers = append(peers, p+":9094") + } + } + + return peers +} + +func mapInstancePeers(is []string) map[string][]string { + mIs := make(map[string][]string, len(is)) + + for _, i := range is { + mIs[i] = getPeers(i, is) + } + + return mIs +} diff --git a/pkg/tests/alertmanager/alertmanager_test.go b/pkg/tests/alertmanager/alertmanager_test.go new file mode 100644 index 00000000000..0f127ea2ab6 --- /dev/null +++ b/pkg/tests/alertmanager/alertmanager_test.go @@ -0,0 +1,97 @@ +package alertmanager + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestAlertmanagerIntegration_ExtraDedupStage(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + t.Run("assert no flapping alerts when stopOnExtraDedup is enabled", func(t *testing.T) { + s, err := NewAlertmanagerScenario() + require.NoError(t, err) + defer s.Close() + + s.Start(t, 20, "15s", true) + s.Provision(t, ProvisionCfg{ + AlertRuleConfig: AlertRuleConfig{ + PendingPeriod: "30s", + GroupEvaluationIntervalSeconds: 10, + }, + NotificationPolicyCfg: NotificationPolicyCfg{ + GroupWait: "30s", + GroupInterval: "1m", + RepeatInterval: "30m", + }, + }) + + wc, err := s.NewWebhookClient() + require.NoError(t, err) + + lc, err := s.NewLokiClient() + require.NoError(t, err) + + // notifications only start arriving after 2 to 3 minutes so we wait for that + time.Sleep(time.Minute * 2) + + timeout := time.After(5 * time.Minute) + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + nr, err := wc.GetNotifications() + if err != nil { + t.Logf("failed to get alert notifications: %v\n", err) + continue + } + + // get the latest state for the alert from loki + st, err := lc.GetCurrentAlertState() + if err != nil { + t.Logf("failed to get alert state: %v\n", err) + continue + } + + // if the last state is not normal, ignore + // we might be missing other cases of flapping notifications but for now we are only interested in this one + // (alerting notification when state is already normal) + if st.State != AlertStateNormal { + continue + } + + // history is ordered - fetch the first notification that is after the last state change + var i int + for i = range nr.History { + if nr.History[i].TimeNow.After(st.Timestamp) { + break + } + } + + // if all notifications are from before the last state change, we can wait a bit more + if nr.History[i].TimeNow.Before(st.Timestamp) { + continue + } + + // for all notifications after the last state change, check if there is a firing one + for ; i < len(nr.History); i++ { + notification := nr.History[i] + if notification.Status == "firing" { + t.Errorf("flapping notifications - got firing notification when alert was resolved, state = %#v, notification = %#v", st, notification) + t.FailNow() + } + } + + case <-timeout: + // if after the timeout there are no such cases, we assume there are no flapping notifications + return + } + } + }) +} diff --git a/pkg/tests/alertmanager/grafana.go b/pkg/tests/alertmanager/grafana.go new file mode 100644 index 00000000000..7d67ffdf53c --- /dev/null +++ b/pkg/tests/alertmanager/grafana.go @@ -0,0 +1,75 @@ +package alertmanager + +import ( + _ "embed" + "fmt" + "net/url" + "os" + + "github.com/grafana/e2e" + gapi "github.com/grafana/grafana-api-golang-client" +) + +const ( + grafanaBinary = "/run.sh" + grafanaHTTPPort = 3000 +) + +// GetDefaultImage returns the Docker image to use to run the Grafana.. +func GetGrafanaImage() string { + if img := os.Getenv("GRAFANA_IMAGE"); img != "" { + return img + } + + if version := os.Getenv("GRAFANA_VERSION"); version != "" { + return "grafana/grafana-enterprise-dev:" + version + } + + panic("Provide GRAFANA_VERSION or GRAFANA_IMAGE") +} + +type GrafanaService struct { + *e2e.HTTPService +} + +func NewGrafanaService(name string, flags, envVars map[string]string) *GrafanaService { + svc := &GrafanaService{ + HTTPService: e2e.NewHTTPService( + name, + GetGrafanaImage(), + e2e.NewCommandWithoutEntrypoint(grafanaBinary, e2e.BuildArgs(flags)...), + e2e.NewHTTPReadinessProbe(grafanaHTTPPort, "/ready", 200, 299), + grafanaHTTPPort, + 9094, + ), + } + + svc.SetEnvVars(envVars) + + return svc +} + +type GrafanaClient struct { + *gapi.Client +} + +// NewGrafanaClient creates a client for using the Grafana API. Note we don't bother +// wrapping the client library, and just use it as-is, until we find a reason not to. +func NewGrafanaClient(host string, orgID int64) (*GrafanaClient, error) { + cfg := gapi.Config{ + BasicAuth: url.UserPassword("admin", "admin"), + OrgID: orgID, + HTTPHeaders: map[string]string{ + "X-Disable-Provenance": "true", + }, + } + + client, err := gapi.New(fmt.Sprintf("http://%s/", host), cfg) + if err != nil { + return nil, err + } + + return &GrafanaClient{ + Client: client, + }, nil +} diff --git a/pkg/tests/alertmanager/loki.go b/pkg/tests/alertmanager/loki.go new file mode 100644 index 00000000000..16c6fda0b90 --- /dev/null +++ b/pkg/tests/alertmanager/loki.go @@ -0,0 +1,152 @@ +package alertmanager + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "strconv" + "time" + + "github.com/grafana/e2e" +) + +const ( + defaultLokiImage = "grafana/loki:latest" + lokiBinary = "/usr/bin/loki" + lokiHTTPPort = 3100 +) + +// GetDefaultImage returns the Docker image to use to run the Loki.. +func GetLokiImage() string { + if img := os.Getenv("LOKI_IMAGE"); img != "" { + return img + } + + return defaultLokiImage +} + +type LokiService struct { + *e2e.HTTPService +} + +func NewLokiService(name string, flags, envVars map[string]string) *LokiService { + svc := &LokiService{ + HTTPService: e2e.NewHTTPService( + name, + GetLokiImage(), + e2e.NewCommandWithoutEntrypoint(lokiBinary, e2e.BuildArgs(flags)...), + e2e.NewHTTPReadinessProbe(lokiHTTPPort, "/ready", 200, 299), + lokiHTTPPort, + ), + } + + svc.SetEnvVars(envVars) + + return svc +} + +type LokiClient struct { + c http.Client + u *url.URL +} + +func NewLokiClient(u string) (*LokiClient, error) { + pu, err := url.Parse(u) + if err != nil { + return nil, err + } + + return &LokiClient{ + c: http.Client{}, + u: pu, + }, nil +} + +type LokiQueryResponse struct { + Status string `json:"status"` + Data struct { + ResultType string `json:"resultType"` + Result []struct { + Stream struct { + Condition string `json:"condition"` + Current string `json:"current"` + DashboardUID string `json:"dashboardUID"` + Fingerprint string `json:"fingerprint"` + FolderUID string `json:"folderUID"` + From string `json:"from"` + Group string `json:"group"` + LabelsAlertname string `json:"labels_alertname"` + LabelsGrafanaFolder string `json:"labels_grafana_folder"` + OrgID string `json:"orgID"` + PanelID string `json:"panelID"` + Previous string `json:"previous"` + RuleID string `json:"ruleID"` + RuleTitle string `json:"ruleTitle"` + RuleUID string `json:"ruleUID"` + SchemaVersion string `json:"schemaVersion"` + ServiceName string `json:"service_name"` + ValuesB string `json:"values_B"` + ValuesC string `json:"values_C"` + } `json:"stream"` + Values [][]string `json:"values"` + } `json:"result"` + } +} + +type AlertState string + +const ( + AlertStateNormal AlertState = "Normal" + AlertStatePending AlertState = "Pending" + AlertStateAlerting AlertState = "Alerting" +) + +type AlertStateResponse struct { + State AlertState + Timestamp time.Time +} + +// GetCurrentAlertState fetches the current alert state from loki +func (c *LokiClient) GetCurrentAlertState() (*AlertStateResponse, error) { + u := c.u.ResolveReference(&url.URL{Path: "/loki/api/v1/query_range"}) + + vs := url.Values{} + vs.Add("query", `{from="state-history"} | json`) + vs.Add("since", "60s") + + u.RawQuery = vs.Encode() + + resp, err := c.c.Get(u.String()) + if err != nil { + return nil, err + } + //nolint:errcheck + defer resp.Body.Close() + + res := LokiQueryResponse{} + + if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { + return nil, err + } + + if res.Status != "success" { + return nil, fmt.Errorf("failed to query state from loki") + } + + if len(res.Data.Result) == 0 { + return nil, fmt.Errorf("empty result from loki") + } + + r := res.Data.Result[0] + it, err := strconv.ParseInt(r.Values[0][0], 10, 0) + if err != nil { + return nil, fmt.Errorf("failed to parse timestamp: %v", err) + } + + return &AlertStateResponse{ + State: AlertState(r.Stream.Current), + Timestamp: time.Unix(0, it), + }, nil +} diff --git a/pkg/tests/alertmanager/postgres.go b/pkg/tests/alertmanager/postgres.go new file mode 100644 index 00000000000..3d3fe4577d9 --- /dev/null +++ b/pkg/tests/alertmanager/postgres.go @@ -0,0 +1,41 @@ +package alertmanager + +import ( + "os" + + "github.com/grafana/e2e" +) + +const ( + defaultPostgresImage = "postgres:16.4" + postgresHTTPPort = 5432 +) + +// GetDefaultImage returns the Docker image to use to run the Postgres.. +func GetPostgresImage() string { + if img := os.Getenv("POSTGRES_IMAGE"); img != "" { + return img + } + + return defaultPostgresImage +} + +type PostgresService struct { + *e2e.HTTPService +} + +func NewPostgresService(name string, envVars map[string]string) *PostgresService { + svc := &PostgresService{ + HTTPService: e2e.NewHTTPService( + name, + GetPostgresImage(), + nil, + nil, + postgresHTTPPort, + ), + } + + svc.SetEnvVars(envVars) + + return svc +} diff --git a/pkg/tests/alertmanager/webhook.go b/pkg/tests/alertmanager/webhook.go new file mode 100644 index 00000000000..c90c3f229eb --- /dev/null +++ b/pkg/tests/alertmanager/webhook.go @@ -0,0 +1,85 @@ +package alertmanager + +import ( + "encoding/json" + "net/http" + "net/url" + "time" + + "github.com/grafana/e2e" +) + +const ( + defaultWebhookImage = "webhook-receiver" + webhookBinary = "/bin/main" + webhookHTTPPort = 8080 +) + +type WebhookService struct { + *e2e.HTTPService +} + +func NewWebhookService(name string, flags, envVars map[string]string) *WebhookService { + svc := &WebhookService{ + HTTPService: e2e.NewHTTPService( + name, + "webhook-receiver", + e2e.NewCommandWithoutEntrypoint(webhookBinary, e2e.BuildArgs(flags)...), + e2e.NewHTTPReadinessProbe(webhookHTTPPort, "/ready", 200, 299), + webhookHTTPPort), + } + + svc.SetEnvVars(envVars) + + return svc +} + +type WebhookClient struct { + c http.Client + u *url.URL +} + +func NewWebhookClient(u string) (*WebhookClient, error) { + pu, err := url.Parse(u) + if err != nil { + return nil, err + } + + return &WebhookClient{ + c: http.Client{}, + u: pu, + }, nil +} + +type GetNotificationsResponse struct { + Stats map[string]int `json:"stats"` + History []struct { + Status string `json:"status"` + TimeNow time.Time `json:"timeNow"` + StartsAt time.Time `json:"startsAt"` + Node string `json:"node"` + DeltaLastSeconds float64 `json:"deltaLastSeconds"` + DeltaStartSeconds float64 `json:"deltaStartSeconds"` + } `json:"history"` +} + +// GetNotifications fetches notifications from the webhook server +func (c *WebhookClient) GetNotifications() (*GetNotificationsResponse, error) { + u := c.u.ResolveReference(&url.URL{Path: "/notifications"}) + + resp, err := c.c.Get(u.String()) + if err != nil { + return nil, err + } + //nolint:errcheck + defer resp.Body.Close() + + res := GetNotificationsResponse{} + + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return nil, err + } + + return &res, nil +} diff --git a/tools/setup_grafana_alertmanager_integration_test_images.go b/tools/setup_grafana_alertmanager_integration_test_images.go new file mode 100644 index 00000000000..3b6e1436446 --- /dev/null +++ b/tools/setup_grafana_alertmanager_integration_test_images.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "sync" + + amtests "github.com/grafana/grafana/pkg/tests/alertmanager" +) + +func docker(args []string) { + cmd := exec.Command("docker", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Printf("docker pull failed: %v\n", err) + os.Exit(1) + } +} + +func main() { + var wg sync.WaitGroup + + for _, cmd := range [][]string{ + {"pull", amtests.GetGrafanaImage()}, + {"pull", amtests.GetLokiImage()}, + {"pull", amtests.GetPostgresImage()}, + {"build", "-t", "webhook-receiver", "devenv/docker/blocks/stateful_webhook"}, + } { + wg.Add(1) + + go func(cmd []string) { + defer wg.Done() + + docker(cmd) + }(cmd) + } + + wg.Wait() +} From 1b1954de2887b1b447968e6df2e1d23ca04a06ce Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 13 Feb 2025 11:59:59 +0100 Subject: [PATCH 556/894] Authz: add support to use folder api to fetch folder tree (#100038) * Add FolderStore interface * Authz: add implementation to use folders api and use it inproc with loopback config * Add tracing and add rest.Config for talking with folder api using access tokens * Restructure test to get rid of circular dependencies in tests * use correct group version kind --------- Co-authored-by: gamab --- pkg/services/authz/{client.go => rbac.go} | 73 ++++++++ pkg/services/authz/rbac/service.go | 56 ++++--- pkg/services/authz/rbac/service_test.go | 3 +- pkg/services/authz/rbac/store/folder_store.go | 158 ++++++++++++++++++ pkg/services/authz/rbac/store/models.go | 18 -- pkg/services/authz/rbac/store/queries.go | 1 - pkg/services/authz/rbac/store/store.go | 39 ----- pkg/services/authz/server.go | 37 ---- pkg/storage/unified/apistore/go.mod | 28 +++- pkg/storage/unified/apistore/go.sum | 30 +++- pkg/storage/unified/apistore/prepare_test.go | 5 + pkg/storage/unified/apistore/store_test.go | 2 +- pkg/storage/unified/apistore/util.go | 40 ----- pkg/storage/unified/apistore/watcher_test.go | 49 +++++- 14 files changed, 367 insertions(+), 172 deletions(-) rename pkg/services/authz/{client.go => rbac.go} (75%) create mode 100644 pkg/services/authz/rbac/store/folder_store.go delete mode 100644 pkg/services/authz/server.go diff --git a/pkg/services/authz/client.go b/pkg/services/authz/rbac.go similarity index 75% rename from pkg/services/authz/client.go rename to pkg/services/authz/rbac.go index 506155df380..cd0465886d3 100644 --- a/pkg/services/authz/client.go +++ b/pkg/services/authz/rbac.go @@ -3,6 +3,8 @@ package authz import ( "context" "errors" + "fmt" + "net/http" "time" "github.com/fullstorydev/grpchan" @@ -11,6 +13,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "k8s.io/client-go/rest" authnlib "github.com/grafana/authlib/authn" authzlib "github.com/grafana/authlib/authz" @@ -22,6 +25,8 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/apiserver" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/rbac" "github.com/grafana/grafana/pkg/services/authz/rbac/store" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -64,6 +69,10 @@ func ProvideAuthZClient( // Register the server server := rbac.NewService( sql, + // When running in-proc we get a injection cycle between + // authz client, resource client and apiserver so we need to use + // package level function to get rest config + store.NewAPIFolderStore(tracer, apiserver.GetRestConfig), legacy.NewLegacySQLStores(sql), store.NewUnionPermissionStore( store.NewStaticPermissionStore(acService), @@ -201,3 +210,67 @@ func newCloudLegacyClient(authCfg *Cfg, tracer tracing.Tracer) (authlib.AccessCl return client, nil } + +func RegisterRBACAuthZService( + handler grpcserver.Provider, + db legacysql.LegacyDatabaseProvider, + tracer tracing.Tracer, + reg prometheus.Registerer, + cache cache.Cache, + exchangeClient authnlib.TokenExchanger, + folderAPIURL string, +) { + var folderStore store.FolderStore + // FIXME: for now we default to using database read proxy for folders if the api url is not configured. + // we should remove this and the sql implementation once we have verified that is works correctly + if folderAPIURL == "" { + folderStore = store.NewSQLFolderStore(db, tracer) + } else { + folderStore = store.NewAPIFolderStore(tracer, func(ctx context.Context) *rest.Config { + return &rest.Config{ + Host: folderAPIURL, + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { + return &tokenExhangeRoundTripper{te: exchangeClient, rt: rt} + }, + QPS: 50, + Burst: 100, + } + }) + } + + server := rbac.NewService( + db, + folderStore, + legacy.NewLegacySQLStores(db), + store.NewSQLPermissionStore(db, tracer), + log.New("authz-grpc-server"), + tracer, + reg, + cache, + ) + + srv := handler.GetServer() + authzv1.RegisterAuthzServiceServer(srv, server) + authzextv1.RegisterAuthzExtentionServiceServer(srv, server) +} + +var _ http.RoundTripper = tokenExhangeRoundTripper{} + +type tokenExhangeRoundTripper struct { + te authnlib.TokenExchanger + rt http.RoundTripper +} + +func (t tokenExhangeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + res, err := t.te.Exchange(r.Context(), authnlib.TokenExchangeRequest{ + Namespace: "*", + Audiences: []string{"folder.grafana.app"}, + }) + + if err != nil { + return nil, fmt.Errorf("create access token: %w", err) + } + + r.Header.Set("X-Access-Token", "Bearer "+res.Token) + return t.rt.RoundTrip(r) +} diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index 429b568a091..7a365c0d6f5 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -17,7 +17,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" "github.com/grafana/authlib/cache" - claims "github.com/grafana/authlib/types" + "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -41,6 +41,7 @@ type Service struct { authzextv1.UnimplementedAuthzExtentionServiceServer store store.Store + folderStore store.FolderStore permissionStore store.PermissionStore identityStore legacy.LegacyIdentityStore @@ -63,6 +64,7 @@ type Service struct { func NewService( sql legacysql.LegacyDatabaseProvider, + folderStore store.FolderStore, identityStore legacy.LegacyIdentityStore, permissionStore store.PermissionStore, logger log.Logger, @@ -72,6 +74,7 @@ func NewService( ) *Service { return &Service{ store: store.NewStore(sql, tracer), + folderStore: folderStore, permissionStore: permissionStore, identityStore: identityStore, logger: logger, @@ -209,40 +212,42 @@ func (s *Service) validateListRequest(ctx context.Context, req *authzv1.ListRequ return listReq, nil } -func validateNamespace(ctx context.Context, nameSpace string) (claims.NamespaceInfo, error) { +func validateNamespace(ctx context.Context, nameSpace string) (types.NamespaceInfo, error) { if nameSpace == "" { - return claims.NamespaceInfo{}, status.Error(codes.InvalidArgument, "namespace is required") + return types.NamespaceInfo{}, status.Error(codes.InvalidArgument, "namespace is required") } - authInfo, has := claims.AuthInfoFrom(ctx) + authInfo, has := types.AuthInfoFrom(ctx) if !has { - return claims.NamespaceInfo{}, status.Error(codes.Internal, "could not get auth info from context") + return types.NamespaceInfo{}, status.Error(codes.Internal, "could not get auth info from context") } - if !claims.NamespaceMatches(authInfo.GetNamespace(), nameSpace) { - return claims.NamespaceInfo{}, status.Error(codes.PermissionDenied, "namespace does not match") + if !types.NamespaceMatches(authInfo.GetNamespace(), nameSpace) { + return types.NamespaceInfo{}, status.Error(codes.PermissionDenied, "namespace does not match") } - ns, err := claims.ParseNamespace(nameSpace) + ns, err := types.ParseNamespace(nameSpace) if err != nil { - return claims.NamespaceInfo{}, err + return types.NamespaceInfo{}, err } return ns, nil } -func (s *Service) validateSubject(ctx context.Context, subject string) (string, claims.IdentityType, error) { +func (s *Service) validateSubject(ctx context.Context, subject string) (string, types.IdentityType, error) { if subject == "" { return "", "", status.Error(codes.InvalidArgument, "subject is required") } ctxLogger := s.logger.FromContext(ctx) - identityType, userUID, err := claims.ParseTypeID(subject) + identityType, userUID, err := types.ParseTypeID(subject) if err != nil { return "", "", err } + // Permission check currently only checks user, anonymous user, service account and renderer permissions - if !(identityType == claims.TypeUser || identityType == claims.TypeServiceAccount || identityType == claims.TypeAnonymous || identityType == claims.TypeRenderService) { + if !types.IsIdentityType(identityType, types.TypeUser, types.TypeServiceAccount, types.TypeAnonymous, types.TypeRenderService) { ctxLogger.Error("unsupported identity type", "type", identityType) return "", "", status.Error(codes.PermissionDenied, "unsupported identity type") } + return userUID, identityType, nil } @@ -264,30 +269,29 @@ func (s *Service) validateAction(ctx context.Context, group, resource, verb stri return action, nil } -func (s *Service) getIdentityPermissions(ctx context.Context, ns claims.NamespaceInfo, idType claims.IdentityType, userID, action string) (map[string]bool, error) { +func (s *Service) getIdentityPermissions(ctx context.Context, ns types.NamespaceInfo, idType types.IdentityType, userID, action string) (map[string]bool, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getIdentityPermissions") defer span.End() // When checking folder creation permissions, also check edit and admin action sets for folder, as the scoped folder create actions aren't stored in the DB separately var actionSets []string if action == "folders:create" { - actionSets = append(actionSets, "folders:edit") - actionSets = append(actionSets, "folders:admin") + actionSets = append(actionSets, "folders:edit", "folders:admin") } switch idType { - case claims.TypeAnonymous: + case types.TypeAnonymous: return s.getAnonymousPermissions(ctx, ns, action, actionSets) - case claims.TypeRenderService: + case types.TypeRenderService: return s.getRendererPermissions(ctx, action) - case claims.TypeUser, claims.TypeServiceAccount: + case types.TypeUser, types.TypeServiceAccount: return s.getUserPermissions(ctx, ns, userID, action, actionSets) default: return nil, fmt.Errorf("unsupported identity type: %s", idType) } } -func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInfo, userID, action string, actionSets []string) (map[string]bool, error) { +func (s *Service) getUserPermissions(ctx context.Context, ns types.NamespaceInfo, userID, action string, actionSets []string) (map[string]bool, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserPermissions") defer span.End() @@ -342,7 +346,7 @@ func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInf return res.(map[string]bool), nil } -func (s *Service) getAnonymousPermissions(ctx context.Context, ns claims.NamespaceInfo, action string, actionSets []string) (map[string]bool, error) { +func (s *Service) getAnonymousPermissions(ctx context.Context, ns types.NamespaceInfo, action string, actionSets []string) (map[string]bool, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getAnonymousPermissions") defer span.End() @@ -378,7 +382,7 @@ func (s *Service) getRendererPermissions(ctx context.Context, action string) (ma return map[string]bool{}, nil } -func (s *Service) GetUserIdentifiers(ctx context.Context, ns claims.NamespaceInfo, userUID string) (*store.UserIdentifiers, error) { +func (s *Service) GetUserIdentifiers(ctx context.Context, ns types.NamespaceInfo, userUID string) (*store.UserIdentifiers, error) { uidCacheKey := userIdentifierCacheKey(ns.Value, userUID) if cached, ok := s.idCache.Get(ctx, uidCacheKey); ok { return &cached, nil @@ -397,7 +401,7 @@ func (s *Service) GetUserIdentifiers(ctx context.Context, ns claims.NamespaceInf userIDQuery = store.UserIdentifierQuery{UserUID: userUID} } userIdentifiers, err := s.store.GetUserIdentifiers(ctx, userIDQuery) - if err != nil || userIdentifiers == nil { + if err != nil { return nil, fmt.Errorf("could not get user internal id: %w", err) } @@ -407,7 +411,7 @@ func (s *Service) GetUserIdentifiers(ctx context.Context, ns claims.NamespaceInf return userIdentifiers, nil } -func (s *Service) getUserTeams(ctx context.Context, ns claims.NamespaceInfo, userIdentifiers *store.UserIdentifiers) ([]int64, error) { +func (s *Service) getUserTeams(ctx context.Context, ns types.NamespaceInfo, userIdentifiers *store.UserIdentifiers) ([]int64, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserTeams") defer span.End() @@ -441,7 +445,7 @@ func (s *Service) getUserTeams(ctx context.Context, ns claims.NamespaceInfo, use return teamIDs, nil } -func (s *Service) getUserBasicRole(ctx context.Context, ns claims.NamespaceInfo, userIdentifiers *store.UserIdentifiers) (store.BasicRole, error) { +func (s *Service) getUserBasicRole(ctx context.Context, ns types.NamespaceInfo, userIdentifiers *store.UserIdentifiers) (store.BasicRole, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserBasicRole") defer span.End() @@ -535,7 +539,7 @@ func (s *Service) checkInheritedPermissions(ctx context.Context, scopeMap map[st return false, nil } -func (s *Service) buildFolderTree(ctx context.Context, ns claims.NamespaceInfo) (folderTree, error) { +func (s *Service) buildFolderTree(ctx context.Context, ns types.NamespaceInfo) (folderTree, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.buildFolderTree") defer span.End() @@ -545,7 +549,7 @@ func (s *Service) buildFolderTree(ctx context.Context, ns claims.NamespaceInfo) } res, err, _ := s.sf.Do(ns.Value+"_buildFolderTree", func() (interface{}, error) { - folders, err := s.store.GetFolders(ctx, ns) + folders, err := s.folderStore.ListFolders(ctx, ns) if err != nil { return nil, fmt.Errorf("could not get folders: %w", err) } diff --git a/pkg/services/authz/rbac/service_test.go b/pkg/services/authz/rbac/service_test.go index 310e7cc8cfc..930f5920e36 100644 --- a/pkg/services/authz/rbac/service_test.go +++ b/pkg/services/authz/rbac/service_test.go @@ -620,6 +620,7 @@ func setupService() *Service { folderCache: newCacheWrap[folderTree](cache, logger, shortCacheTTL), store: fStore, permissionStore: fStore, + folderStore: fStore, identityStore: &fakeIdentityStore{}, sf: new(singleflight.Group), } @@ -663,7 +664,7 @@ func (f *fakeStore) GetUserPermissions(ctx context.Context, namespace claims.Nam return f.userPermissions, nil } -func (f *fakeStore) GetFolders(ctx context.Context, namespace claims.NamespaceInfo) ([]store.Folder, error) { +func (f *fakeStore) ListFolders(ctx context.Context, namespace claims.NamespaceInfo) ([]store.Folder, error) { f.calls++ if f.err { return nil, fmt.Errorf("store error") diff --git a/pkg/services/authz/rbac/store/folder_store.go b/pkg/services/authz/rbac/store/folder_store.go new file mode 100644 index 00000000000..078cb2f4c61 --- /dev/null +++ b/pkg/services/authz/rbac/store/folder_store.go @@ -0,0 +1,158 @@ +package store + +import ( + "context" + "fmt" + + "github.com/grafana/authlib/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/pager" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" +) + +type FolderStore interface { + ListFolders(ctx context.Context, ns types.NamespaceInfo) ([]Folder, error) +} + +type Folder struct { + UID string + ParentUID *string +} + +var _ FolderStore = (*SQLFolderStore)(nil) + +func NewSQLFolderStore(sql legacysql.LegacyDatabaseProvider, tracer tracing.Tracer) *SQLFolderStore { + return &SQLFolderStore{sql, tracer} +} + +type SQLFolderStore struct { + sql legacysql.LegacyDatabaseProvider + tracer tracing.Tracer +} + +var sqlFolders = mustTemplate("folder_query.sql") + +type listFoldersQuery struct { + sqltemplate.SQLTemplate + + Query *FolderQuery + FolderTable string +} + +type FolderQuery struct { + OrgID int64 +} + +func (r listFoldersQuery) Validate() error { + return nil +} + +func newListFolders(sql *legacysql.LegacyDatabaseHelper, query *FolderQuery) listFoldersQuery { + return listFoldersQuery{ + SQLTemplate: sqltemplate.New(sql.DialectForDriver()), + Query: query, + FolderTable: sql.Table("folder"), + } +} + +func (s *SQLFolderStore) ListFolders(ctx context.Context, ns types.NamespaceInfo) ([]Folder, error) { + ctx, span := s.tracer.Start(ctx, "authz_direct_db.database.ListFolders") + defer span.End() + + sql, err := s.sql(ctx) + if err != nil { + return nil, err + } + + query := newListFolders(sql, &FolderQuery{OrgID: ns.OrgID}) + q, err := sqltemplate.Execute(sqlFolders, query) + if err != nil { + return nil, err + } + + rows, err := sql.DB.GetSqlxSession().Query(ctx, q, query.GetArgs()...) + defer func() { + if rows != nil { + _ = rows.Close() + } + }() + if err != nil { + return nil, err + } + + var folders []Folder + for rows.Next() { + var folder Folder + if err := rows.Scan(&folder.UID, &folder.ParentUID); err != nil { + return nil, err + } + folders = append(folders, folder) + } + + return folders, nil +} + +var _ FolderStore = (*APIFolderStore)(nil) + +func NewAPIFolderStore(tracer tracing.Tracer, configProvider func(ctx context.Context) *rest.Config) *APIFolderStore { + return &APIFolderStore{tracer, configProvider} +} + +type APIFolderStore struct { + tracer tracing.Tracer + configProvider func(ctx context.Context) *rest.Config +} + +func (s *APIFolderStore) ListFolders(ctx context.Context, ns types.NamespaceInfo) ([]Folder, error) { + ctx, span := s.tracer.Start(ctx, "authz.apistore.ListFolders") + defer span.End() + + client, err := s.client(ctx, ns.Value) + if err != nil { + return nil, fmt.Errorf("create resource client: %w", err) + } + + p := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { + return client.List(ctx, opts) + }) + + const defaultPageSize = 500 + folders := make([]Folder, 0, defaultPageSize) + err = p.EachListItem(ctx, metav1.ListOptions{Limit: defaultPageSize}, func(obj runtime.Object) error { + object, err := utils.MetaAccessor(obj) + if err != nil { + return err + } + + folder := Folder{UID: object.GetName()} + parent := object.GetFolder() + if parent != "" { + folder.ParentUID = &parent + } + + folders = append(folders, folder) + return nil + }) + + if err != nil { + return nil, fmt.Errorf("fetching folders: %w", err) + } + + return folders, nil +} + +func (s *APIFolderStore) client(ctx context.Context, namespace string) (dynamic.ResourceInterface, error) { + client, err := dynamic.NewForConfig(s.configProvider(ctx)) + if err != nil { + return nil, err + } + return client.Resource(folderv0alpha1.FolderResourceInfo.GroupVersionResource()).Namespace(namespace), nil +} diff --git a/pkg/services/authz/rbac/store/models.go b/pkg/services/authz/rbac/store/models.go index 9abcbb8a257..bc3e6593245 100644 --- a/pkg/services/authz/rbac/store/models.go +++ b/pkg/services/authz/rbac/store/models.go @@ -19,21 +19,3 @@ type UserIdentifierQuery struct { UserID int64 UserUID string } - -type FolderQuery struct { - OrgID int64 -} - -type DashboardQuery struct { - OrgID int64 -} - -type Folder struct { - UID string - ParentUID *string -} - -type Dashboard struct { - UID string - ParentUID *string -} diff --git a/pkg/services/authz/rbac/store/queries.go b/pkg/services/authz/rbac/store/queries.go index 4ad0a3d5e11..10c9a17ea8a 100644 --- a/pkg/services/authz/rbac/store/queries.go +++ b/pkg/services/authz/rbac/store/queries.go @@ -16,7 +16,6 @@ var ( sqlQueryBasicRoles = mustTemplate("basic_role_query.sql") sqlUserIdentifiers = mustTemplate("user_identifier_query.sql") - sqlFolders = mustTemplate("folder_query.sql") ) func mustTemplate(filename string) *template.Template { diff --git a/pkg/services/authz/rbac/store/store.go b/pkg/services/authz/rbac/store/store.go index 9b7c2832b00..124ba38331b 100644 --- a/pkg/services/authz/rbac/store/store.go +++ b/pkg/services/authz/rbac/store/store.go @@ -15,7 +15,6 @@ import ( type Store interface { GetUserIdentifiers(ctx context.Context, query UserIdentifierQuery) (*UserIdentifiers, error) GetBasicRoles(ctx context.Context, ns claims.NamespaceInfo, query BasicRoleQuery) (*BasicRole, error) - GetFolders(ctx context.Context, ns claims.NamespaceInfo) ([]Folder, error) } type StoreImpl struct { @@ -104,41 +103,3 @@ func (s *StoreImpl) GetBasicRoles(ctx context.Context, ns claims.NamespaceInfo, return &role, nil } - -func (s *StoreImpl) GetFolders(ctx context.Context, ns claims.NamespaceInfo) ([]Folder, error) { - ctx, span := s.tracer.Start(ctx, "authz_direct_db.database.GetFolders") - defer span.End() - - sql, err := s.sql(ctx) - if err != nil { - return nil, err - } - - query := FolderQuery{OrgID: ns.OrgID} - req := newGetFolders(sql, &query) - q, err := sqltemplate.Execute(sqlFolders, req) - if err != nil { - return nil, err - } - - rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) - defer func() { - if rows != nil { - _ = rows.Close() - } - }() - if err != nil { - return nil, err - } - - var folders []Folder - for rows.Next() { - var folder Folder - if err := rows.Scan(&folder.UID, &folder.ParentUID); err != nil { - return nil, err - } - folders = append(folders, folder) - } - - return folders, nil -} diff --git a/pkg/services/authz/server.go b/pkg/services/authz/server.go deleted file mode 100644 index 868e7c2816b..00000000000 --- a/pkg/services/authz/server.go +++ /dev/null @@ -1,37 +0,0 @@ -package authz - -import ( - authzv1 "github.com/grafana/authlib/authz/proto/v1" - cache "github.com/grafana/authlib/cache" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" - authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" - "github.com/grafana/grafana/pkg/services/authz/rbac" - "github.com/grafana/grafana/pkg/services/authz/rbac/store" - "github.com/grafana/grafana/pkg/services/grpcserver" - "github.com/grafana/grafana/pkg/storage/legacysql" - "github.com/prometheus/client_golang/prometheus" -) - -func RegisterRBACAuthZService( - handler grpcserver.Provider, - db legacysql.LegacyDatabaseProvider, - tracer tracing.Tracer, - reg prometheus.Registerer, - cache cache.Cache) { - server := rbac.NewService( - db, - legacy.NewLegacySQLStores(db), - store.NewSQLPermissionStore(db, tracer), - log.New("authz-grpc-server"), - tracer, - reg, - cache, - ) - - srv := handler.GetServer() - authzv1.RegisterAuthzServiceServer(srv, server) - authzextv1.RegisterAuthzExtentionServiceServer(srv, server) -} diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index a409151a0d4..b18163780f8 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -45,14 +45,17 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect + 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/BurntSushi/toml v1.4.0 // indirect github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/RoaringBitmap/roaring v1.9.3 // indirect github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect @@ -85,6 +88,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect github.com/aws/smithy-go v1.20.3 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.12.0 // indirect @@ -107,7 +111,9 @@ require ( github.com/blevesearch/zapx/v14 v14.3.10 // indirect github.com/blevesearch/zapx/v15 v15.3.16 // indirect github.com/blevesearch/zapx/v16 v16.1.8 // indirect + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect github.com/bufbuild/protocompile v0.4.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect @@ -117,6 +123,8 @@ 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/dennwc/varint v1.0.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/dlmiddlecote/sqlstats v1.0.2 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -137,8 +145,10 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gchaincl/sqlhooks v1.3.0 // indirect github.com/getkin/kin-openapi v0.129.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-kit/log v0.2.1 // indirect + github.com/go-ldap/ldap/v3 v3.4.4 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -152,6 +162,7 @@ require ( github.com/go-openapi/strfmt v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/gobwas/glob v0.2.3 // indirect @@ -160,10 +171,12 @@ require ( github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/status v1.1.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang-migrate/migrate/v4 v4.7.0 // indirect github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/mock v1.7.0-rc.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.3 // indirect @@ -178,6 +191,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038 // indirect github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect @@ -186,8 +200,12 @@ require ( 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.265.0 // indirect + github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d // indirect + github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect + github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grafana/sqlds/v4 v4.1.3 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect @@ -208,6 +226,7 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.2 // indirect @@ -232,6 +251,7 @@ require ( github.com/magefile/mage v1.15.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -253,12 +273,14 @@ require ( github.com/mithrandie/go-file/v2 v2.1.0 // indirect github.com/mithrandie/go-text v1.6.0 // indirect github.com/mithrandie/ternary v1.1.1 // indirect + github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/natefinch/wrap v0.2.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect @@ -287,6 +309,7 @@ require ( github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.13.2 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/prometheus v0.301.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/cors v1.11.1 // indirect @@ -299,7 +322,6 @@ require ( github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/smartystreets/goconvey v1.6.4 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect @@ -317,6 +339,7 @@ require ( github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a // indirect github.com/urfave/cli v1.22.16 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.etcd.io/bbolt v1.3.11 // indirect @@ -366,11 +389,14 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/mail.v2 v2.3.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.1 // indirect k8s.io/component-base v0.32.1 // indirect + k8s.io/kms v0.32.1 // indirect + k8s.io/kube-aggregator v0.32.0 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 73e0a03e693..6d345c4151e 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -136,6 +136,8 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/at-wat/mqtt-go v0.19.4 h1:R2cbCU7O5PHQ38unbe1Y51ncG3KsFEJV6QeipDoqdLQ= @@ -184,6 +186,8 @@ github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -343,6 +347,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 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 v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= @@ -387,6 +393,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= @@ -549,7 +557,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -805,6 +812,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/mocktools/go-smtp-mock/v2 v2.3.1 h1:wq75NDSsOy5oHo/gEQQT0fRRaYKRqr1IdkjhIPXxagM= +github.com/mocktools/go-smtp-mock/v2 v2.3.1/go.mod h1:h9AOf/IXLSU2m/1u4zsjtOM/WddPwdOUBz56dV9f81M= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -829,6 +838,8 @@ github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk= github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= @@ -842,8 +853,9 @@ github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNs github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -936,6 +948,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/prometheus v0.301.0 h1:0z8dgegmILivNomCd79RKvVkIols8vBGPKmcIBc7OyY= github.com/prometheus/prometheus v0.301.0/go.mod h1:BJLjWCKNfRfjp7Q48DrAjARnCi7GhfUVvUFEAWTssZM= +github.com/prometheus/sigv4 v0.1.0 h1:FgxH+m1qf9dGQ4w8Dd6VkthmpFQfGTzUeavMoQeG1LA= +github.com/prometheus/sigv4 v0.1.0/go.mod h1:doosPW9dOitMzYe2I2BN0jZqUuBrGPbXrNsTScN18iU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= @@ -975,7 +989,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= @@ -1044,7 +1057,6 @@ github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a/go.mod h1:1xEUf2abjfP9 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= -github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -1060,6 +1072,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= @@ -1155,6 +1168,7 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= @@ -1194,6 +1208,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -1239,6 +1254,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -1268,6 +1285,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= @@ -1323,6 +1341,7 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1381,7 +1400,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425222832-ad9eeb80039a/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -1422,6 +1440,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= @@ -1559,6 +1578,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc= gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/storage/unified/apistore/prepare_test.go b/pkg/storage/unified/apistore/prepare_test.go index 330c2791cfa..d24b31a5a91 100644 --- a/pkg/storage/unified/apistore/prepare_test.go +++ b/pkg/storage/unified/apistore/prepare_test.go @@ -13,9 +13,14 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/exp/rand" "k8s.io/apimachinery/pkg/api/apitesting" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apiserver/pkg/storage" ) +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + func TestPrepareObjectForStorage(t *testing.T) { _ = v0alpha1.AddToScheme(scheme) node, err := snowflake.NewNode(rand.Int63n(1024)) diff --git a/pkg/storage/unified/apistore/store_test.go b/pkg/storage/unified/apistore/store_test.go index 81a3b3044d4..ec1013de44a 100644 --- a/pkg/storage/unified/apistore/store_test.go +++ b/pkg/storage/unified/apistore/store_test.go @@ -3,7 +3,7 @@ // Provenance-includes-license: Apache-2.0 // Provenance-includes-copyright: The Kubernetes Authors. -package apistore +package apistore_test import ( "context" diff --git a/pkg/storage/unified/apistore/util.go b/pkg/storage/unified/apistore/util.go index fb95ed08020..e41a50b5efa 100644 --- a/pkg/storage/unified/apistore/util.go +++ b/pkg/storage/unified/apistore/util.go @@ -9,7 +9,6 @@ import ( "bytes" "fmt" "strconv" - "strings" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" - grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -128,41 +126,3 @@ func isUnchanged(codec runtime.Codec, obj runtime.Object, newObj runtime.Object) return bytes.Equal(buf.Bytes(), newBuf.Bytes()), nil } - -func testKeyParser(val string) (*resource.ResourceKey, error) { - k, err := grafanaregistry.ParseKey(val) - if err != nil { - if strings.HasPrefix(val, "pods/") { - parts := strings.Split(val, "/") - if len(parts) == 2 { - err = nil - k = &grafanaregistry.Key{ - Resource: parts[0], // pods - Name: parts[1], - } - } else if len(parts) == 3 { - err = nil - k = &grafanaregistry.Key{ - Resource: parts[0], // pods - Namespace: parts[1], - Name: parts[2], - } - } - } - } - if err != nil { - return nil, err - } - if k.Group == "" { - k.Group = "example.apiserver.k8s.io" - } - if k.Resource == "" { - return nil, apierrors.NewInternalError(fmt.Errorf("missing resource in request")) - } - return &resource.ResourceKey{ - Namespace: k.Namespace, - Group: k.Group, - Resource: k.Resource, - Name: k.Name, - }, err -} diff --git a/pkg/storage/unified/apistore/watcher_test.go b/pkg/storage/unified/apistore/watcher_test.go index 8a8b278577f..5b0842fa985 100644 --- a/pkg/storage/unified/apistore/watcher_test.go +++ b/pkg/storage/unified/apistore/watcher_test.go @@ -3,11 +3,13 @@ // Provenance-includes-license: Apache-2.0 // Provenance-includes-copyright: The Kubernetes Authors. -package apistore +package apistore_test import ( "context" + "fmt" "os" + "strings" "testing" "time" @@ -16,6 +18,7 @@ import ( "gocloud.dev/blob/fileblob" "gocloud.dev/blob/memblob" "k8s.io/apimachinery/pkg/api/apitesting" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -28,9 +31,11 @@ import ( "k8s.io/apiserver/pkg/storage/storagebackend" "k8s.io/apiserver/pkg/storage/storagebackend/factory" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" storagetesting "github.com/grafana/grafana/pkg/apiserver/storage/testing" infraDB "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/apistore" "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" @@ -160,7 +165,7 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte client := resource.NewLocalResourceClient(server) config := storagebackend.NewDefaultConfig(setupOpts.prefix, setupOpts.codec) - store, destroyFunc, err := NewStorage( + store, destroyFunc, err := apistore.NewStorage( config.ForResource(setupOpts.groupResource), client, func(obj runtime.Object) (string, error) { @@ -176,7 +181,7 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte storage.DefaultNamespaceScopedAttr, make(map[string]storage.IndexerFunc, 0), nil, - StorageOptions{}, + apistore.StorageOptions{}, ) if err != nil { return nil, nil, nil, err @@ -371,3 +376,41 @@ func newPod() runtime.Object { func newPodList() runtime.Object { return &example.PodList{} } + +func testKeyParser(val string) (*resource.ResourceKey, error) { + k, err := grafanaregistry.ParseKey(val) + if err != nil { + if strings.HasPrefix(val, "pods/") { + parts := strings.Split(val, "/") + if len(parts) == 2 { + err = nil + k = &grafanaregistry.Key{ + Resource: parts[0], // pods + Name: parts[1], + } + } else if len(parts) == 3 { + err = nil + k = &grafanaregistry.Key{ + Resource: parts[0], // pods + Namespace: parts[1], + Name: parts[2], + } + } + } + } + if err != nil { + return nil, err + } + if k.Group == "" { + k.Group = "example.apiserver.k8s.io" + } + if k.Resource == "" { + return nil, apierrors.NewInternalError(fmt.Errorf("missing resource in request")) + } + return &resource.ResourceKey{ + Namespace: k.Namespace, + Group: k.Group, + Resource: k.Resource, + Name: k.Name, + }, err +} From 3c56e32b0c87ef303b115cc632df88d89b6bec43 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 13 Feb 2025 14:04:03 +0300 Subject: [PATCH 557/894] K8s/Utils: Find title in unstructured content (#100576) --- pkg/apimachinery/utils/meta.go | 12 ++++++++++++ pkg/apimachinery/utils/meta_test.go | 2 ++ 2 files changed, 14 insertions(+) diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index 4d39d4fcd57..5b2978533bb 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -699,6 +699,18 @@ func (m *grafanaMetaAccessor) FindTitle(defaultTitle string) string { } } + obj, ok := m.obj.(*unstructured.Unstructured) + if ok { + title, ok, _ := unstructured.NestedString(obj.Object, "spec", "title") + if ok && title != "" { + return title + } + title, ok, _ = unstructured.NestedString(obj.Object, "spec", "name") + if ok && title != "" { + return title + } + } + title := m.r.FieldByName("Title") if title.IsValid() && title.Kind() == reflect.String { return title.String() diff --git a/pkg/apimachinery/utils/meta_test.go b/pkg/apimachinery/utils/meta_test.go index 451deaa47d5..12d12cc06b5 100644 --- a/pkg/apimachinery/utils/meta_test.go +++ b/pkg/apimachinery/utils/meta_test.go @@ -194,6 +194,7 @@ func TestMetaAccessor(t *testing.T) { res.Object = map[string]any{ "spec": map[string]any{ "hello": "world", + "title": "Title", }, "status": map[string]any{ "sloth": "🦥", @@ -218,6 +219,7 @@ func TestMetaAccessor(t *testing.T) { rv, err := meta.GetResourceVersionInt64() require.NoError(t, err) require.Equal(t, int64(12345), rv) + require.Equal(t, "Title", meta.FindTitle("")) // Make sure access to spec works for Unstructured spec, err = meta.GetSpec() From 1c7a758127585029710f89e55b8d06766da06c76 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Thu, 13 Feb 2025 12:11:41 +0100 Subject: [PATCH 558/894] Frontend: Lazy load Echo Backends (#100345) feat(app): lazy load echo backends depending on config. Move lodash to sharedDependencies --- public/app/app.ts | 31 +++++++++---------- .../plugins/loader/sharedDependencies.ts | 4 ++- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/public/app/app.ts b/public/app/app.ts index 86b2386dc6c..697e909fc37 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -5,7 +5,6 @@ import 'whatwg-fetch'; // fetch polyfill needed for PhantomJs rendering import 'file-saver'; import 'jquery'; -import _ from 'lodash'; // eslint-disable-line lodash/import-scope import { createElement } from 'react'; import { createRoot } from 'react-dom/client'; @@ -46,7 +45,6 @@ import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelData import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; import { setPluginPage } from '@grafana/runtime/src/components/PluginPage'; import config, { updateConfig } from 'app/core/config'; -import { arrayMove } from 'app/core/utils/arrayMove'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; import getDefaultMonacoLanguages from '../lib/monaco-languages'; @@ -67,13 +65,6 @@ import { backendSrv } from './core/services/backend_srv'; import { contextSrv, RedirectToUrlKey } from './core/services/context_srv'; import { Echo } from './core/services/echo/Echo'; import { reportPerformance } from './core/services/echo/EchoSrv'; -import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend'; -import { ApplicationInsightsBackend } from './core/services/echo/backends/analytics/ApplicationInsightsBackend'; -import { BrowserConsoleBackend } from './core/services/echo/backends/analytics/BrowseConsoleBackend'; -import { GA4EchoBackend } from './core/services/echo/backends/analytics/GA4Backend'; -import { GAEchoBackend } from './core/services/echo/backends/analytics/GABackend'; -import { RudderstackBackend } from './core/services/echo/backends/analytics/RudderstackBackend'; -import { GrafanaJavascriptAgentBackend } from './core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend'; import { KeybindingSrv } from './core/services/keybindingSrv'; import { startMeasure, stopMeasure } from './core/utils/metrics'; import { initDevFeatures } from './dev'; @@ -113,10 +104,6 @@ import { createSystemVariableAdapter } from './features/variables/system/adapter import { createTextBoxVariableAdapter } from './features/variables/textbox/adapter'; import { configureStore } from './store/configureStore'; -// add move to lodash for backward compatabilty with plugins -// @ts-ignore -_.move = arrayMove; - // import symlinked extensions const extensionsIndex = require.context('.', true, /extensions\/index.ts/); const extensionsExports = extensionsIndex.keys().map((key) => { @@ -139,7 +126,7 @@ export class GrafanaApp { initI18nPromise.then(({ language }) => updateConfig({ language })); setBackendSrv(backendSrv); - initEchoSrv(); + await initEchoSrv(); // This needs to be done after the `initEchoSrv` since it is being used under the hood. startMeasure('frontend_app_init'); @@ -295,7 +282,7 @@ function initExtensions() { } } -function initEchoSrv() { +async function initEchoSrv() { setEchoSrv(new Echo({ debug: process.env.NODE_ENV === 'development' })); window.addEventListener('load', (e) => { @@ -315,6 +302,7 @@ function initEchoSrv() { }); if (contextSrv.user.orgRole !== '') { + const { PerformanceBackend } = await import('./core/services/echo/backends/PerformanceBackend'); registerEchoBackend(new PerformanceBackend({})); } @@ -328,6 +316,10 @@ function initEchoSrv() { .filter(Boolean) .map((url) => new RegExp(`${url}.*.`)); + const { GrafanaJavascriptAgentBackend } = await import( + './core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend' + ); + registerEchoBackend( new GrafanaJavascriptAgentBackend({ ...config.grafanaJavascriptAgent, @@ -346,6 +338,7 @@ function initEchoSrv() { } if (config.googleAnalyticsId) { + const { GAEchoBackend } = await import('./core/services/echo/backends/analytics/GABackend'); registerEchoBackend( new GAEchoBackend({ googleAnalyticsId: config.googleAnalyticsId, @@ -354,6 +347,7 @@ function initEchoSrv() { } if (config.googleAnalytics4Id) { + const { GA4EchoBackend } = await import('./core/services/echo/backends/analytics/GA4Backend'); registerEchoBackend( new GA4EchoBackend({ googleAnalyticsId: config.googleAnalytics4Id, @@ -363,6 +357,7 @@ function initEchoSrv() { } if (config.rudderstackWriteKey && config.rudderstackDataPlaneUrl) { + const { RudderstackBackend } = await import('./core/services/echo/backends/analytics/RudderstackBackend'); registerEchoBackend( new RudderstackBackend({ writeKey: config.rudderstackWriteKey, @@ -377,6 +372,9 @@ function initEchoSrv() { } if (config.applicationInsightsConnectionString) { + const { ApplicationInsightsBackend } = await import( + './core/services/echo/backends/analytics/ApplicationInsightsBackend' + ); registerEchoBackend( new ApplicationInsightsBackend({ connectionString: config.applicationInsightsConnectionString, @@ -386,6 +384,7 @@ function initEchoSrv() { } if (config.analyticsConsoleReporting) { + const { BrowserConsoleBackend } = await import('./core/services/echo/backends/analytics/BrowseConsoleBackend'); registerEchoBackend(new BrowserConsoleBackend()); } } @@ -395,7 +394,7 @@ function initEchoSrv() { * like PerformanceMark or PerformancePaintTiming (e.g. created with performance.mark, or first-contentful-paint) */ function reportMetricPerformanceMark(metricName: string, prefix = '', suffix = ''): void { - const metric = _.first(performance.getEntriesByName(metricName)); + const metric = performance.getEntriesByName(metricName).at(0); if (metric) { const metricName = metric.name.replace(/-/g, '_'); reportPerformance(`${prefix}${metricName}${suffix}`, Math.round(metric.startTime) / 1000); diff --git a/public/app/features/plugins/loader/sharedDependencies.ts b/public/app/features/plugins/loader/sharedDependencies.ts index dbb8ed8692f..7059274c717 100644 --- a/public/app/features/plugins/loader/sharedDependencies.ts +++ b/public/app/features/plugins/loader/sharedDependencies.ts @@ -18,6 +18,7 @@ import { appEvents, contextSrv } from 'app/core/core'; import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; import impressionSrv from 'app/core/services/impression_srv'; import TimeSeries from 'app/core/time_series2'; +import { arrayMove } from 'app/core/utils/arrayMove'; import * as flatten from 'app/core/utils/flatten'; import kbn from 'app/core/utils/kbn'; import * as ticks from 'app/core/utils/ticks'; @@ -90,7 +91,8 @@ export const sharedDependenciesMap = { __useDefault: true, }, ...jQueryFlotDeps, - lodash: () => import('lodash').then((module) => ({ ...module, __useDefault: true })), + // add move to lodash for backward compatabilty with plugins + lodash: () => import('lodash').then((module) => ({ ...module, move: arrayMove, __useDefault: true })), moment: () => import('moment').then((module) => ({ ...module, __useDefault: true })), prismjs: () => import('prismjs'), react: () => import('react'), From a69fac6e16906cbf29d9708d611dc09298cdbdaa Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:40:53 +0100 Subject: [PATCH 559/894] Mark grpc data source timeouts as cancelled queries (#100573) * Set up to reproduce issue locally * add check for deadline exceeded * Revert "Set up to reproduce issue locally" This reverts commit d8d9b354cab93e0e88edc739c4227cc75541b867. * Trigger build --------- Co-authored-by: Will Browne --- pkg/plugins/instrumentationutils/request_status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/plugins/instrumentationutils/request_status.go b/pkg/plugins/instrumentationutils/request_status.go index 9d6cb7fba5e..5e77480ee77 100644 --- a/pkg/plugins/instrumentationutils/request_status.go +++ b/pkg/plugins/instrumentationutils/request_status.go @@ -35,7 +35,7 @@ func RequestStatusFromError(err error) RequestStatus { status = RequestStatusError if errors.Is(err, context.Canceled) { status = RequestStatusCancelled - } else if s, ok := grpcstatus.FromError(err); ok && s.Code() == grpccodes.Canceled { + } else if s, ok := grpcstatus.FromError(err); ok && s.Code() == grpccodes.Canceled || s.Code() == grpccodes.DeadlineExceeded { status = RequestStatusCancelled } } From be60ef0500a603e667598ef47bbab8204885aeea Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:10:58 +0100 Subject: [PATCH 560/894] IDToken: cache invalidation (#100592) * Make org role part of id token cache key. This way we will always sign a new token when it changes * Remove calls to remove id token --- pkg/api/http_server.go | 4 +- pkg/api/org_users.go | 6 -- pkg/api/org_users_test.go | 74 ++++++------------- pkg/services/auth/idimpl/service.go | 8 +- pkg/services/auth/idimpl/service_test.go | 31 ++++++++ .../serviceaccounts/manager/service.go | 9 --- 6 files changed, 60 insertions(+), 72 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 850a4ea5fd5..21f6f2dcd56 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -207,7 +207,6 @@ type HTTPServer struct { tempUserService tempUser.Service loginAttemptService loginAttempt.Service orgService org.Service - idService auth.IDService orgDeletionService org.DeletionService TeamService team.Service accesscontrolService accesscontrol.Service @@ -273,7 +272,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService, statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, promGatherer prometheus.Gatherer, starApi *starApi.API, promRegister prometheus.Registerer, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, anonService anonymous.Service, - userVerifier user.Verifier, pluginPreinstall plugininstaller.Preinstall, idService auth.IDService, + userVerifier user.Verifier, pluginPreinstall plugininstaller.Preinstall, ) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -361,7 +360,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi tempUserService: tempUserService, loginAttemptService: loginAttemptService, orgService: orgService, - idService: idService, orgDeletionService: orgDeletionService, TeamService: teamService, navTreeService: navTreeService, diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index f19d4138bb8..1b83b4e3b77 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -7,11 +7,9 @@ import ( "net/http" "strconv" - claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/authn" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -434,10 +432,6 @@ func (hs *HTTPServer) updateOrgUserHelper(c *contextmodel.ReqContext, cmd org.Up } } - if err := hs.idService.RemoveIDToken(c.Req.Context(), &authn.Identity{ID: strconv.FormatInt(cmd.UserID, 10), Type: claims.TypeUser, OrgID: cmd.OrgID}); err != nil { - return response.Error(http.StatusInternalServerError, "Failed to invalidate the ID token cache", err) - } - if err := hs.orgService.UpdateOrgUser(c.Req.Context(), &cmd); err != nil { if errors.Is(err, org.ErrLastOrgAdmin) { return response.Error(http.StatusBadRequest, "Cannot change role so that there is no organization admin left", nil) diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index 324e9da2b39..59bb47330d6 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -9,12 +9,9 @@ import ( "strings" "testing" - "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/services/auth/idtest" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/dtos" @@ -205,12 +202,11 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) { func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) { type testCase struct { - desc string - SkipOrgRoleSync bool - AuthEnabled bool - AuthModule string - shouldInvalidateIDToken bool - expectedCode int + desc string + SkipOrgRoleSync bool + AuthEnabled bool + AuthModule string + expectedCode int } permissions := []accesscontrol.Permission{ {Action: accesscontrol.ActionOrgUsersRead, Scope: "users:*"}, @@ -220,12 +216,11 @@ func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) { } tests := []testCase{ { - desc: "should be able to change basicRole when skip_org_role_sync true", - SkipOrgRoleSync: true, - AuthEnabled: true, - AuthModule: login.LDAPAuthModule, - shouldInvalidateIDToken: true, - expectedCode: http.StatusOK, + desc: "should be able to change basicRole when skip_org_role_sync true", + SkipOrgRoleSync: true, + AuthEnabled: true, + AuthModule: login.LDAPAuthModule, + expectedCode: http.StatusOK, }, { desc: "should not be able to change basicRole when skip_org_role_sync false", @@ -242,20 +237,18 @@ func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) { expectedCode: http.StatusForbidden, }, { - desc: "should be able to change basicRole with a basic Auth", - SkipOrgRoleSync: false, - AuthEnabled: false, - AuthModule: "", - shouldInvalidateIDToken: true, - expectedCode: http.StatusOK, + desc: "should be able to change basicRole with a basic Auth", + SkipOrgRoleSync: false, + AuthEnabled: false, + AuthModule: "", + expectedCode: http.StatusOK, }, { - desc: "should be able to change basicRole with a basic Auth", - SkipOrgRoleSync: true, - AuthEnabled: true, - AuthModule: "", - shouldInvalidateIDToken: true, - expectedCode: http.StatusOK, + desc: "should be able to change basicRole with a basic Auth", + SkipOrgRoleSync: true, + AuthEnabled: true, + AuthModule: "", + expectedCode: http.StatusOK, }, } @@ -286,11 +279,6 @@ func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) { } hs.userService = &usertest.FakeUserService{ExpectedSignedInUser: userWithPermissions} hs.orgService = &orgtest.FakeOrgService{} - idService := &idtest.MockService{} - if tt.shouldInvalidateIDToken { - idService.On("RemoveIDToken", mock.Anything, mock.Anything).Return(nil) - } - hs.idService = idService hs.SocialService = &socialtest.FakeSocialService{ ExpectedAuthInfoProvider: &social.OAuthInfo{Enabled: tt.AuthEnabled, SkipOrgRoleSync: tt.SkipOrgRoleSync}, } @@ -627,7 +615,6 @@ func TestOrgUsersAPIEndpointWithSetPerms_AccessControl(t *testing.T) { ExpectedUser: &user.User{}, ExpectedSignedInUser: userWithPermissions(1, tt.permissions), } - hs.idService = &idtest.FakeService{} hs.accesscontrolService = &actest.FakeService{} }) @@ -650,24 +637,16 @@ func TestPatchOrgUsersAPIEndpoint_AccessControl(t *testing.T) { name string role org.RoleType permissions []accesscontrol.Permission - setup func(*testing.T, *idtest.MockService) input string expectedCode int } tests := []testCase{ { - name: "user with permissions can update org role", - permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: "users:*"}}, - role: org.RoleAdmin, - input: `{"role": "Viewer"}`, - setup: func(t *testing.T, idService *idtest.MockService) { - idService.On("RemoveIDToken", mock.Anything, mock.MatchedBy(func(id *authn.Identity) bool { - return id.GetIdentityType() == types.TypeUser && - id.GetID() == "user:1" && - id.GetOrgID() == int64(1) - })).Return(nil) - }, + name: "user with permissions can update org role", + permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: "users:*"}}, + role: org.RoleAdmin, + input: `{"role": "Viewer"}`, expectedCode: http.StatusOK, }, { @@ -694,11 +673,6 @@ func TestPatchOrgUsersAPIEndpoint_AccessControl(t *testing.T) { AuthModule: "", }, } - idService := &idtest.MockService{} - if tt.setup != nil { - tt.setup(t, idService) - } - hs.idService = idService hs.accesscontrolService = &actest.FakeService{} hs.userService = &usertest.FakeUserService{ ExpectedUser: &user.User{}, diff --git a/pkg/services/auth/idimpl/service.go b/pkg/services/auth/idimpl/service.go index a4b89204d7b..5dec411fdee 100644 --- a/pkg/services/auth/idimpl/service.go +++ b/pkg/services/auth/idimpl/service.go @@ -63,7 +63,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri s.metrics.tokenSigningDurationHistogram.Observe(time.Since(t).Seconds()) }(time.Now()) - cacheKey := prefixCacheKey(id.GetCacheKey()) + cacheKey := getCacheKey(id) type resultType struct { token string @@ -140,7 +140,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri } func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) error { - return s.cache.Delete(ctx, prefixCacheKey(id.GetCacheKey())) + return s.cache.Delete(ctx, getCacheKey(id)) } func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { @@ -181,8 +181,8 @@ func getAudience(orgID int64) jwt.Audience { return jwt.Audience{fmt.Sprintf("org:%d", orgID)} } -func prefixCacheKey(key string) string { - return fmt.Sprintf("%s-%s", cachePrefix, key) +func getCacheKey(ident identity.Requester) string { + return cachePrefix + ident.GetCacheKey() + string(ident.GetOrgRole()) } func shouldLogErr(err error) bool { diff --git a/pkg/services/auth/idimpl/service_test.go b/pkg/services/auth/idimpl/service_test.go index 0f3814968e3..bb7ee510e55 100644 --- a/pkg/services/auth/idimpl/service_test.go +++ b/pkg/services/auth/idimpl/service_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" ) @@ -101,4 +102,34 @@ func TestService_SignIdentity(t *testing.T) { assert.Equal(t, claims.TypeUser, gotClaims.Rest.Type) assert.Equal(t, "edpu3nnt61se8e", gotClaims.Rest.Identifier) }) + + t.Run("should sign new token if org role has changed", func(t *testing.T) { + s := ProvideService( + setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(), + &authntest.FakeService{}, nil, + ) + + ident := &authn.Identity{ + ID: "1", + Type: claims.TypeUser, + AuthenticatedBy: login.AzureADAuthModule, + Login: "U1", + UID: "edpu3nnt61se8e", + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, + } + + first, _, err := s.SignIdentity(context.Background(), ident) + require.NoError(t, err) + + second, _, err := s.SignIdentity(context.Background(), ident) + require.NoError(t, err) + + assert.Equal(t, first, second) + + ident.OrgRoles[1] = org.RoleEditor + third, _, err := s.SignIdentity(context.Background(), ident) + require.NoError(t, err) + assert.NotEqual(t, first, third) + }) } diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index f53e7b72d9b..cde680f616d 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -17,8 +17,6 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" - "github.com/grafana/grafana/pkg/services/auth" - "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" @@ -44,7 +42,6 @@ type ServiceAccountsService struct { secretScanService secretscan.Checker orgService org.Service serverLock *serverlock.ServerLockService - idService auth.IDService secretScanEnabled bool secretScanInterval time.Duration @@ -61,7 +58,6 @@ func ProvideServiceAccountsService( acService accesscontrol.Service, permissions accesscontrol.ServiceAccountPermissionsService, serverLockService *serverlock.ServerLockService, - idService auth.IDService, ) (*ServiceAccountsService, error) { serviceAccountsStore := database.ProvideServiceAccountsStore( cfg, @@ -81,7 +77,6 @@ func ProvideServiceAccountsService( backgroundLog: log.New("serviceaccounts.background"), orgService: orgService, serverLock: serverLockService, - idService: idService, } if err := RegisterRoles(acService); err != nil { @@ -271,10 +266,6 @@ func (sa *ServiceAccountsService) UpdateServiceAccount(ctx context.Context, orgI return nil, err } - if err := sa.idService.RemoveIDToken(ctx, &authn.Identity{ID: strconv.FormatInt(serviceAccountID, 10), Type: claims.TypeServiceAccount, OrgID: orgID}); err != nil { - return nil, err - } - return sa.store.UpdateServiceAccount(ctx, orgID, serviceAccountID, saForm) } From a58564a35efe8c05a21d8190b283af5bc0979d2a Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:24:59 +0100 Subject: [PATCH 561/894] Unified Storage: Register metrics (#100600) use seperate once struct --- pkg/storage/unified/resource/bleve_index_metrics.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/storage/unified/resource/bleve_index_metrics.go b/pkg/storage/unified/resource/bleve_index_metrics.go index 0cb52455cff..a15f36be543 100644 --- a/pkg/storage/unified/resource/bleve_index_metrics.go +++ b/pkg/storage/unified/resource/bleve_index_metrics.go @@ -12,6 +12,7 @@ import ( var ( onceIndex sync.Once + onceSprinkles sync.Once IndexMetrics *BleveIndexMetrics SprinklesIndexMetrics *SprinklesMetrics ) @@ -36,7 +37,7 @@ type SprinklesMetrics struct { var IndexCreationBuckets = []float64{1, 5, 10, 25, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} func NewSprinklesMetrics() *SprinklesMetrics { - onceIndex.Do(func() { + onceSprinkles.Do(func() { SprinklesIndexMetrics = &SprinklesMetrics{ SprinklesLatency: prometheus.NewHistogram(prometheus.HistogramOpts{ Namespace: "index_server", From afe8b08a48acc695e78cc3c5d05b5ba45ad9ffad Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Thu, 13 Feb 2025 15:03:54 +0100 Subject: [PATCH 562/894] MultiCombobox: Fix labels disappearing on selected items when filtering (#100602) * Fix label disappearing on filtering * Remove only from test * Fix custom value test --- .../components/Combobox/MultiCombobox.test.tsx | 17 +++++++++++++++-- .../src/components/Combobox/MultiCombobox.tsx | 4 ++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx index bee10de6783..5be7bc09098 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx @@ -156,8 +156,8 @@ describe('MultiCombobox', () => { await user.type(input, 'D'); await user.keyboard('{arrowdown}{enter}'); expect(onChange).toHaveBeenCalledWith([ - { value: 'a' }, - { value: 'c' }, + { label: 'A', value: 'a' }, + { label: 'C', value: 'c' }, { label: 'D', value: 'D', description: 'Use custom value' }, ]); }); @@ -235,6 +235,19 @@ describe('MultiCombobox', () => { await user.click(await screen.findByRole('option', { name: 'All' })); expect(onChange).toHaveBeenCalledWith([]); }); + + it('should keep label names on selected items when searching', async () => { + const options = [ + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + ]; + render(); + const input = screen.getByRole('combobox'); + await user.click(input); + await user.type(input, 'b'); + expect(screen.getByText('A')).toBeInTheDocument(); + }); }); describe('async', () => { diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 9a90dfd4588..7d18074910a 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -79,8 +79,8 @@ export const MultiCombobox = (props: MultiComboboxPro return []; } - return getSelectedItemsFromValue(value, baseOptions); - }, [value, baseOptions]); + return getSelectedItemsFromValue(value, typeof props.options !== 'function' ? props.options : baseOptions); + }, [value, props.options, baseOptions]); const { measureRef, counterMeasureRef, suffixMeasureRef, shownItems } = useMeasureMulti( selectedItems, From 9ad66538711163b7cc48c58e13b44fe6b328be15 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 13 Feb 2025 07:20:17 -0700 Subject: [PATCH 563/894] Dashboard Schema V2: Improve diffing (#100022) * improve diffing * define dash spec props a-z * Fix * sort deep initialSaveModel * update tests * Fix test, description, and query ds issues * Fix seralizer test * response transformers * skip panelMerge tests --- .betterer.results | 6 ++- .../dashboard/v2alpha0/dashboard.schema.cue | 40 +++++++++--------- .../schema/dashboard/v2alpha0/types.gen.ts | 41 ++++++++++--------- public/app/core/utils/object.test.ts | 1 + public/app/core/utils/object.ts | 7 ++-- .../dashboard-scene/scene/DashboardScene.tsx | 3 +- .../DashboardSceneSerializer.test.ts | 20 +++++++++ .../transformSaveModelSchemaV2ToScene.test.ts | 9 ++-- .../transformSaveModelSchemaV2ToScene.ts | 2 +- .../transformSceneToSaveModelSchemaV2.ts | 11 ++--- .../api/ResponseTransformers.test.ts | 12 +++--- .../dashboard/api/ResponseTransformers.ts | 4 +- .../dashboard/state/DashboardModel.ts | 2 + .../dashboard/utils/panelMerge.test.ts | 3 +- 14 files changed, 97 insertions(+), 64 deletions(-) diff --git a/.betterer.results b/.betterer.results index 8db179df663..334e3e5dd2a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1324,8 +1324,10 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"] + [0, 0, 0, "Do not use any type assertions.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"] ], "public/app/core/utils/richHistory.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 21012f3461a..f9763bb219b 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -6,10 +6,7 @@ import ( DashboardV2Spec: { // Title of dashboard. - title: string - - // Description of dashboard. - description?: string + annotations: [...AnnotationQueryKind] // Configuration of dashboard cursor sync behavior. // "Off" for no shared crosshair or tooltip (default). @@ -17,6 +14,19 @@ DashboardV2Spec: { // "Tooltip" for shared crosshair AND shared tooltip. cursorSync: DashboardCursorSync + // Description of dashboard. + description?: string + + // Whether a dashboard is editable or not. + editable?: bool | *true + + elements: [ElementReference.name]: Element + + layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind + + // Links with references to other dashboards or external websites. + links: [...DashboardLink] + // When set to true, the dashboard will redraw panels at an interval matching the pixel width. // This will keep data "moving left" regardless of the query refresh rate. This setting helps // avoid dashboards presenting stale live data. @@ -25,30 +35,20 @@ DashboardV2Spec: { // When set to true, the dashboard will load all panels in the dashboard when it's loaded. preload: bool - // Whether a dashboard is editable or not. - editable?: bool | *true - - // Links with references to other dashboards or external websites. - links: [...DashboardLink] + // Plugins only. The version of the dashboard installed together with the plugin. + // This is used to determine if the dashboard should be updated when the plugin is updated. + revision?: uint16 // Tags associated with dashboard. tags: [...string] timeSettings: TimeSettingsSpec + // Title of dashboard. + title: string + // Configured template variables. variables: [...VariableKind] - - elements: [ElementReference.name]: Element - - annotations: [...AnnotationQueryKind] - - layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind - - - // Plugins only. The version of the dashboard installed together with the plugin. - // This is used to determine if the dashboard should be updated when the plugin is updated. - revision?: uint16 } // Supported dashboard elements diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index e85e71996fd..1ad02256fe8 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -5,49 +5,50 @@ import * as common from '@grafana/schema'; export interface DashboardV2Spec { // Title of dashboard. - title: string; - // Description of dashboard. - description?: string; + annotations: AnnotationQueryKind[]; // Configuration of dashboard cursor sync behavior. // "Off" for no shared crosshair or tooltip (default). // "Crosshair" for shared crosshair. // "Tooltip" for shared crosshair AND shared tooltip. cursorSync: DashboardCursorSync; + // Description of dashboard. + description?: string; + // Whether a dashboard is editable or not. + editable?: boolean; + elements: Record; + layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind; + // Links with references to other dashboards or external websites. + links: DashboardLink[]; // When set to true, the dashboard will redraw panels at an interval matching the pixel width. // This will keep data "moving left" regardless of the query refresh rate. This setting helps // avoid dashboards presenting stale live data. liveNow?: boolean; // When set to true, the dashboard will load all panels in the dashboard when it's loaded. preload: boolean; - // Whether a dashboard is editable or not. - editable?: boolean; - // Links with references to other dashboards or external websites. - links: DashboardLink[]; - // Tags associated with dashboard. - tags: string[]; - timeSettings: TimeSettingsSpec; - // Configured template variables. - variables: VariableKind[]; - elements: Record; - annotations: AnnotationQueryKind[]; - layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind; // Plugins only. The version of the dashboard installed together with the plugin. // This is used to determine if the dashboard should be updated when the plugin is updated. revision?: number; + // Tags associated with dashboard. + tags: string[]; + timeSettings: TimeSettingsSpec; + // Title of dashboard. + title: string; + // Configured template variables. + variables: VariableKind[]; } export const defaultDashboardV2Spec = (): DashboardV2Spec => ({ - title: "", + annotations: [], cursorSync: "Off", - preload: false, editable: true, + elements: {}, + layout: defaultGridLayoutKind(), links: [], + preload: false, tags: [], timeSettings: defaultTimeSettingsSpec(), + title: "", variables: [], - elements: {}, - annotations: [], - layout: defaultGridLayoutKind(), }); // Supported dashboard elements diff --git a/public/app/core/utils/object.test.ts b/public/app/core/utils/object.test.ts index 8e27feccc6b..c84f7d71d9e 100644 --- a/public/app/core/utils/object.test.ts +++ b/public/app/core/utils/object.test.ts @@ -7,6 +7,7 @@ describe('objects', () => { deeper: 10, foo: null, arr: [null, 1, 'hello'], + value: -Infinity, }, bar: undefined, simple: 'A', diff --git a/public/app/core/utils/object.ts b/public/app/core/utils/object.ts index a5cccf419c2..a51d593b635 100644 --- a/public/app/core/utils/object.ts +++ b/public/app/core/utils/object.ts @@ -1,16 +1,17 @@ import { isArray, isPlainObject } from 'lodash'; /** @returns a deep clone of the object, but with any null value removed */ -export function sortedDeepCloneWithoutNulls(value: T): T { +export function sortedDeepCloneWithoutNulls(value: T): T { if (isArray(value)) { return value.map(sortedDeepCloneWithoutNulls) as unknown as T; } if (isPlainObject(value)) { - return Object.keys(value) + return Object.keys(value as { [key: string]: any }) .sort() .reduce((acc: any, key) => { const v = (value as any)[key]; - if (v != null) { + // Remove null values and also -Infinity which is not a valid JSON value + if (v != null && v !== -Infinity) { acc[key] = sortedDeepCloneWithoutNulls(v); } return acc; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 0a088325aec..1be803a66ea 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -30,6 +30,7 @@ import { ScrollRefElement } from 'app/core/components/NativeScrollbar'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { getNavModel } from 'app/core/selectors/navModel'; import store from 'app/core/store'; +import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; @@ -674,7 +675,7 @@ export class DashboardScene extends SceneObjectBase { saveModel?: Dashboard | DashboardV2Spec, meta?: DashboardMeta | DashboardWithAccessInfo['metadata'] ): void { - this._serializer.initialSaveModel = saveModel; + this._serializer.initialSaveModel = sortedDeepCloneWithoutNulls(saveModel); this._serializer.metadata = meta; } diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index 747c69eaf07..74d58c10719 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -36,6 +36,26 @@ jest.mock('@grafana/runtime', () => ({ getInstanceSettings: jest.fn(), }; }, + config: { + ...jest.requireActual('@grafana/runtime').config, + bootData: { + settings: { + defaultDatasource: '-- Grafana --', + datasources: { + '-- Grafana --': { + name: 'Grafana', + meta: { id: 'grafana' }, + type: 'datasource', + }, + prometheus: { + name: 'prometheus', + meta: { id: 'prometheus' }, + type: 'datasource', + }, + }, + }, + }, + }, })); describe('DashboardSceneSerializer', () => { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 90033cc9856..108a1e1d159 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -293,7 +293,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME); }); - it('should set panel ds as undefined if it is not mixed DS', () => { + it('should set ds if it is not mixed DS', () => { const dashboard = cloneDeep(defaultDashboard); getPanelElement(dashboard.spec, 'panel-1')?.spec.data.spec.queries.push({ kind: 'PanelQuery', @@ -317,10 +317,13 @@ describe('transformSaveModelSchemaV2ToScene', () => { const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); expect(vizPanels.length).toBe(3); - expect(getQueryRunnerFor(vizPanels[0])?.state.datasource).toBeUndefined(); + expect(getQueryRunnerFor(vizPanels[0])?.state.queries[0].datasource).toEqual({ + type: 'prometheus', + uid: 'datasource1', + }); }); - it('should set panel ds as mixed if one ds is undefined', () => { + it('should set panel ds as mixed if no panels have ds defined', () => { const dashboard = cloneDeep(defaultDashboard); getPanelElement(dashboard.spec, 'panel-1')?.spec.data.spec.queries.push({ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index a8f97ad4287..40b5b01fbf9 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -234,7 +234,7 @@ function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined { } }); - return isMixedDatasource ? { type: 'mixed', uid: MIXED_DATASOURCE_NAME } : undefined; + return isMixedDatasource ? { type: 'mixed', uid: MIXED_DATASOURCE_NAME } : datasource; } function panelQueryKindToSceneQuery(query: PanelQueryKind): SceneDataQuery { diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 979fba6483c..3672567fdb9 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -11,6 +11,7 @@ import { VizPanel, } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; +import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object'; import { DashboardV2Spec, @@ -73,7 +74,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps const dashboardSchemaV2: DeepPartial = { //dashboard settings title: sceneDash.title, - description: sceneDash.description ?? '', + description: sceneDash.description, cursorSync: getCursorSync(sceneDash), liveNow: getLiveNow(sceneDash), preload: sceneDash.preload, @@ -116,7 +117,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps try { // validateDashboardSchemaV2 will throw an error if the dashboard is not valid if (validateDashboardSchemaV2(dashboardSchemaV2)) { - return dashboardSchemaV2; + return sortedDeepCloneWithoutNulls(dashboardSchemaV2); } // should never reach this point, validation should throw an error throw new Error('Error we could transform the dashboard to schema v2: ' + dashboardSchemaV2); @@ -241,7 +242,7 @@ function getVizPanelQueries(vizPanel: VizPanel): PanelQueryKind[] { const queries: PanelQueryKind[] = []; const queryRunner = getQueryRunnerFor(vizPanel); const vizPanelQueries = queryRunner?.state.queries; - const datasource = queryRunner?.state.datasource; + const datasource = queryRunner?.state.datasource ?? getDefaultDataSourceRef(); if (vizPanelQueries) { vizPanelQueries.forEach((query) => { @@ -250,7 +251,7 @@ function getVizPanelQueries(vizPanel: VizPanel): PanelQueryKind[] { spec: omit(query, 'datasource', 'refId', 'hide'), }; const querySpec: PanelQuerySpec = { - datasource: datasource ?? getDefaultDataSourceRef(), + datasource: query.datasource ?? datasource, query: dataQuery, refId: query.refId, hidden: Boolean(query.hide), @@ -446,7 +447,7 @@ function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec { if ('title' in dash && typeof dash.title !== 'string') { throw new Error('Title is not a string'); } - if ('description' in dash && typeof dash.description !== 'string') { + if ('description' in dash && dash.description !== undefined && typeof dash.description !== 'string') { throw new Error('Description is not a string'); } if ('cursorSync' in dash && typeof dash.cursorSync !== 'string') { diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index c598db36084..4a7a29cf711 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -487,7 +487,7 @@ describe('ResponseTransformers', () => { expect(layout.spec.items[0].spec).toEqual({ element: { kind: 'ElementReference', - name: '1', + name: 'panel-1', }, x: 0, y: 0, @@ -495,7 +495,7 @@ describe('ResponseTransformers', () => { height: 8, repeat: { value: 'var1', direction: 'h', mode: 'variable', maxPerRow: undefined }, }); - expect(spec.elements['1']).toEqual({ + expect(spec.elements['panel-1']).toEqual({ kind: 'Panel', spec: { title: 'Panel Title', @@ -550,14 +550,14 @@ describe('ResponseTransformers', () => { expect(layout.spec.items[1].spec).toEqual({ element: { kind: 'ElementReference', - name: '2', + name: 'panel-2', }, x: 0, y: 8, width: 12, height: 8, }); - expect(spec.elements['2']).toEqual({ + expect(spec.elements['panel-2']).toEqual({ kind: 'LibraryPanel', spec: { libraryPanel: { @@ -580,7 +580,7 @@ describe('ResponseTransformers', () => { expect(panelInRow).toEqual({ element: { kind: 'ElementReference', - name: '4', + name: 'panel-4', }, x: 0, y: 0, @@ -598,7 +598,7 @@ describe('ResponseTransformers', () => { expect(panelInCollapsedRow).toEqual({ element: { kind: 'ElementReference', - name: '5', + name: 'panel-5', }, x: 0, y: 0, diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 69edee427e4..3d810f3eedf 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -383,7 +383,7 @@ function buildElement(p: Panel): [PanelKind | LibraryPanelKind, string] { }, }; - return [panelKind, p.id!.toString()]; + return [panelKind, `panel-${p.id}`]; } else { // PanelKind @@ -433,7 +433,7 @@ function buildElement(p: Panel): [PanelKind | LibraryPanelKind, string] { }, }; - return [panelKind, p.id!.toString()]; + return [panelKind, `panel-${p.id}`]; } } diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index f640cc8cbb6..c54e1743f96 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -286,6 +286,8 @@ export class DashboardModel implements TimeModel { * * @internal and experimental */ + // TODO: remove this as it's not being used anymore + // Also remove public/app/features/dashboard/utils/panelMerge.ts updatePanels(panels: IPanelModel[]): PanelMergeInfo { const info = mergePanels(this.panels, panels ?? []); if (info.changed) { diff --git a/public/app/features/dashboard/utils/panelMerge.test.ts b/public/app/features/dashboard/utils/panelMerge.test.ts index 4bc201aa8ad..51536b2d426 100644 --- a/public/app/features/dashboard/utils/panelMerge.test.ts +++ b/public/app/features/dashboard/utils/panelMerge.test.ts @@ -4,7 +4,8 @@ import { FieldColorModeId, ThresholdsMode } from '@grafana/schema/src'; import { DashboardModel } from '../state/DashboardModel'; import { createDashboardModelFixture, createPanelSaveModel } from '../state/__fixtures__/dashboardFixtures'; -describe('Merge dashboard panels', () => { +// skipping these tests because panelMerge is not used +describe.skip('Merge dashboard panels', () => { describe('simple changes', () => { let dashboard: DashboardModel; let rawPanels: PanelModel[]; From 71f97f380de49c21527babdf01e8de975adb4ae1 Mon Sep 17 00:00:00 2001 From: Victor Cinaglia Date: Thu, 13 Feb 2025 09:35:35 -0500 Subject: [PATCH 564/894] Docs: Fix URLs to auth providers from Team Sync page (#100563) * iam/docs: fix links to providers in team sync page * iam/docs: make auth proxy link look more like other links --- .../introduction/grafana-enterprise.md | 2 +- .../configure-security/configure-team-sync.md | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/sources/introduction/grafana-enterprise.md b/docs/sources/introduction/grafana-enterprise.md index 28a647254ef..e9317e632a4 100644 --- a/docs/sources/introduction/grafana-enterprise.md +++ b/docs/sources/introduction/grafana-enterprise.md @@ -33,7 +33,7 @@ Grafana Enterprise includes integrations with more ways to authenticate your use Supported auth providers: -- [Auth Proxy]({{< relref "../setup-grafana/configure-security/configure-authentication/auth-proxy#team-sync-enterprise-only" >}}) +- [Auth Proxy](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/auth-proxy#team-sync-enterprise-only) - [Azure AD](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/azuread#group-sync-enterprise-only) - [Generic OAuth integration](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/generic-oauth#configure-group-synchronization) - [GitHub OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/github#configure-group-synchronization) diff --git a/docs/sources/setup-grafana/configure-security/configure-team-sync.md b/docs/sources/setup-grafana/configure-security/configure-team-sync.md index f5d50cff311..89665525574 100644 --- a/docs/sources/setup-grafana/configure-security/configure-team-sync.md +++ b/docs/sources/setup-grafana/configure-security/configure-team-sync.md @@ -27,16 +27,15 @@ This mechanism allows Grafana to remove an existing synchronized user from a tea ## Supported providers -- [Auth Proxy]({{< relref "./configure-authentication/auth-proxy#team-sync-enterprise-only" >}}) -- [Azure AD](https://grafana.com/docs/grafana//configure-authentication/azuread#group-sync-enterprise-only) -- [Azure AD](https://grafana.com/docs/grafana//configure-security/configure-authentication/azuread#group-sync-enterprise-only) -- [Generic OAuth integration](https://grafana.com/docs/grafana//configure-security/configure-authentication/generic-oauth#configure-group-synchronization) -- [GitHub OAuth](https://grafana.com/docs/grafana//configure-security/configure-authentication/github#configure-group-synchronization) -- [GitLab OAuth](https://grafana.com/docs/grafana//configure-security/configure-authentication/gitlab#configure-group-synchronization) -- [Google OAuth](https://grafana.com/docs/grafana//configure-security/configure-authentication/google#configure-group-synchronization) -- [LDAP](https://grafana.com/docs/grafana//configure-security/configure-authentication/enhanced-ldap#ldap-group-synchronization) -- [Okta](https://grafana.com/docs/grafana//configure-security/configure-authentication/okta#configure-group-synchronization-enterprise-only) -- [SAML](https://grafana.com/docs/grafana//configure-security/configure-authentication/saml#configure-group-synchronization) +- [Auth Proxy](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/auth-proxy/#team-sync-enterprise-only) +- [Azure AD](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/azuread#group-sync-enterprise-only) +- [Generic OAuth integration](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/generic-oauth#configure-group-synchronization) +- [GitHub OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/github#configure-group-synchronization) +- [GitLab OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/gitlab#configure-group-synchronization) +- [Google OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/google#configure-group-synchronization) +- [LDAP](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/enhanced-ldap#ldap-group-synchronization) +- [Okta](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/okta#configure-group-synchronization-enterprise-only) +- [SAML](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/saml#configure-group-synchronization) ## Synchronize a Grafana team with an external group From 9dd75aee328b77f681a7b0715fca1a2f86a7744f Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 13 Feb 2025 09:45:16 -0500 Subject: [PATCH 565/894] Alerting: Refactor State Transition (part 2 of n) (#99985) * split create to create and patch and move to state patch will be refactored further * move setNextState to state transition * move tests * split tests for patch function --- pkg/services/ngalert/state/cache.go | 75 ---- pkg/services/ngalert/state/cache_test.go | 311 ---------------- pkg/services/ngalert/state/manager.go | 128 +------ pkg/services/ngalert/state/state.go | 174 +++++++++ ...ache_bench_test.go => state_bench_test.go} | 8 +- pkg/services/ngalert/state/state_test.go | 331 ++++++++++++++++++ 6 files changed, 526 insertions(+), 501 deletions(-) rename pkg/services/ngalert/state/{cache_bench_test.go => state_bench_test.go} (85%) diff --git a/pkg/services/ngalert/state/cache.go b/pkg/services/ngalert/state/cache.go index 1abeff1c2a9..a7302fc5518 100644 --- a/pkg/services/ngalert/state/cache.go +++ b/pkg/services/ngalert/state/cache.go @@ -143,81 +143,6 @@ func expandAnnotationsAndLabels(ctx context.Context, log log.Logger, alertRule * return lbs, annotations } -func (c *cache) create(ctx context.Context, log log.Logger, alertRule *ngModels.AlertRule, result eval.Result, extraLabels data.Labels, externalURL *url.URL) *State { - lbs, annotations := expandAnnotationsAndLabels(ctx, log, alertRule, result, extraLabels, externalURL) - - cacheID := lbs.Fingerprint() - // For new states, we set StartsAt & EndsAt to EvaluatedAt as this is the - // expected value for a Normal state during state transition. - newState := State{ - OrgID: alertRule.OrgID, - AlertRuleUID: alertRule.UID, - CacheID: cacheID, - State: eval.Normal, - StateReason: "", - ResultFingerprint: result.Instance.Fingerprint(), // remember original result fingerprint - LatestResult: nil, - Error: nil, - Image: nil, - Annotations: annotations, - Labels: lbs, - Values: nil, - StartsAt: result.EvaluatedAt, - EndsAt: result.EvaluatedAt, - ResolvedAt: nil, - LastSentAt: nil, - LastEvaluationString: "", - LastEvaluationTime: result.EvaluatedAt, - EvaluationDuration: result.EvaluationDuration, - } - - existingState := c.get(alertRule.OrgID, alertRule.UID, cacheID) - if existingState == nil { - return &newState - } - // if there is existing state, copy over the current values that may be needed to determine the final state. - // TODO remove some unnecessary assignments below because they are overridden in setNextState - newState.State = existingState.State - newState.StateReason = existingState.StateReason - newState.Image = existingState.Image - newState.LatestResult = existingState.LatestResult - newState.Error = existingState.Error - newState.Values = existingState.Values - newState.LastEvaluationString = existingState.LastEvaluationString - newState.StartsAt = existingState.StartsAt - newState.EndsAt = existingState.EndsAt - newState.ResolvedAt = existingState.ResolvedAt - newState.LastSentAt = existingState.LastSentAt - // Annotations can change over time, however we also want to maintain - // certain annotations across evaluations - for key := range ngModels.InternalAnnotationNameSet { // Changing in - value, ok := existingState.Annotations[key] - if !ok { - continue - } - // If the annotation is not present then it should be copied from - // the current state to the new state - if _, ok = newState.Annotations[key]; !ok { - newState.Annotations[key] = value - } - } - - // if the current state is "data source error" then it may have additional labels that may not exist in the new state. - // See https://github.com/grafana/grafana/blob/c7fdf8ce706c2c9d438f5e6eabd6e580bac4946b/pkg/services/ngalert/state/state.go#L161-L163 - // copy known labels over to the new instance, it can help reduce flapping - // TODO fix this? - if existingState.State == eval.Error && result.State == eval.Error { - setIfExist := func(lbl string) { - if v, ok := existingState.Labels[lbl]; ok { - newState.Labels[lbl] = v - } - } - setIfExist("datasource_uid") - setIfExist("ref_id") - } - return &newState -} - // expand returns the expanded templates of all annotations or labels for the template data. // If a template cannot be expanded due to an error in the template the original template is // maintained and an error is added to the multierror. All errors in the multierror are diff --git a/pkg/services/ngalert/state/cache_test.go b/pkg/services/ngalert/state/cache_test.go index 9fe50fdb120..6defc25086d 100644 --- a/pkg/services/ngalert/state/cache_test.go +++ b/pkg/services/ngalert/state/cache_test.go @@ -3,15 +3,11 @@ package state import ( "context" "errors" - "fmt" "math/rand" - "net/url" "testing" "time" - "github.com/google/uuid" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" @@ -118,313 +114,6 @@ func Test_expand(t *testing.T) { }) } -func Test_create(t *testing.T) { - url := &url.URL{ - Scheme: "http", - Host: "localhost:3000", - Path: "/test", - } - l := log.New("test") - c := newCache() - - gen := models.RuleGen - generateRule := gen.With(gen.WithNotEmptyLabels(5, "rule-")).GenerateRef - - t.Run("should combine all labels", func(t *testing.T) { - rule := generateRule() - - extraLabels := models.GenerateAlertLabels(5, "extra-") - result := eval.Result{ - Instance: models.GenerateAlertLabels(5, "result-"), - } - state := c.create(context.Background(), l, rule, result, extraLabels, url) - for key, expected := range extraLabels { - require.Equal(t, expected, state.Labels[key]) - } - assert.Len(t, state.Labels, len(extraLabels)+len(rule.Labels)+len(result.Instance)) - for key, expected := range extraLabels { - assert.Equal(t, expected, state.Labels[key]) - } - for key, expected := range rule.Labels { - assert.Equal(t, expected, state.Labels[key]) - } - for key, expected := range result.Instance { - assert.Equal(t, expected, state.Labels[key]) - } - }) - t.Run("extra labels should take precedence over rule and result labels", func(t *testing.T) { - rule := generateRule() - - extraLabels := models.GenerateAlertLabels(2, "extra-") - - result := eval.Result{ - Instance: models.GenerateAlertLabels(5, "result-"), - } - for key := range extraLabels { - rule.Labels[key] = "rule-" + util.GenerateShortUID() - result.Instance[key] = "result-" + util.GenerateShortUID() - } - - state := c.create(context.Background(), l, rule, result, extraLabels, url) - for key, expected := range extraLabels { - require.Equal(t, expected, state.Labels[key]) - } - }) - t.Run("rule labels should take precedence over result labels", func(t *testing.T) { - rule := generateRule() - - extraLabels := models.GenerateAlertLabels(2, "extra-") - - result := eval.Result{ - Instance: models.GenerateAlertLabels(5, "result-"), - } - for key := range rule.Labels { - result.Instance[key] = "result-" + util.GenerateShortUID() - } - state := c.create(context.Background(), l, rule, result, extraLabels, url) - for key, expected := range rule.Labels { - require.Equal(t, expected, state.Labels[key]) - } - }) - t.Run("rule labels should be able to be expanded with result and extra labels", func(t *testing.T) { - result := eval.Result{ - Instance: models.GenerateAlertLabels(5, "result-"), - } - rule := generateRule() - - extraLabels := models.GenerateAlertLabels(2, "extra-") - - labelTemplates := make(data.Labels) - for key := range extraLabels { - labelTemplates["rule-"+key] = fmt.Sprintf("{{ with (index .Labels \"%s\") }}{{.}}{{end}}", key) - } - for key := range result.Instance { - labelTemplates["rule-"+key] = fmt.Sprintf("{{ with (index .Labels \"%s\") }}{{.}}{{end}}", key) - } - rule.Labels = labelTemplates - - state := c.create(context.Background(), l, rule, result, extraLabels, url) - for key, expected := range extraLabels { - assert.Equal(t, expected, state.Labels["rule-"+key]) - } - for key, expected := range result.Instance { - assert.Equal(t, expected, state.Labels["rule-"+key]) - } - }) - t.Run("rule annotations should be able to be expanded with result and extra labels", func(t *testing.T) { - result := eval.Result{ - Instance: models.GenerateAlertLabels(5, "result-"), - } - - rule := generateRule() - - extraLabels := models.GenerateAlertLabels(2, "extra-") - - annotationTemplates := make(data.Labels) - for key := range extraLabels { - annotationTemplates["rule-"+key] = fmt.Sprintf("{{ with (index .Labels \"%s\") }}{{.}}{{end}}", key) - } - for key := range result.Instance { - annotationTemplates["rule-"+key] = fmt.Sprintf("{{ with (index .Labels \"%s\") }}{{.}}{{end}}", key) - } - rule.Annotations = annotationTemplates - - state := c.create(context.Background(), l, rule, result, extraLabels, url) - for key, expected := range extraLabels { - assert.Equal(t, expected, state.Annotations["rule-"+key]) - } - for key, expected := range result.Instance { - assert.Equal(t, expected, state.Annotations["rule-"+key]) - } - }) - t.Run("when result labels collide with system labels from LabelsUserCannotSpecify", func(t *testing.T) { - result := eval.Result{ - Instance: models.GenerateAlertLabels(5, "result-"), - } - m := models.LabelsUserCannotSpecify - t.Cleanup(func() { - models.LabelsUserCannotSpecify = m - }) - - models.LabelsUserCannotSpecify = map[string]struct{}{ - "__label1__": {}, - "label2__": {}, - "__label3": {}, - "label4": {}, - } - result.Instance["__label1__"] = uuid.NewString() - result.Instance["label2__"] = uuid.NewString() - result.Instance["__label3"] = uuid.NewString() - result.Instance["label4"] = uuid.NewString() - - rule := generateRule() - - state := c.create(context.Background(), l, rule, result, nil, url) - - for key := range models.LabelsUserCannotSpecify { - assert.NotContains(t, state.Labels, key) - } - assert.Contains(t, state.Labels, "label1") - assert.Equal(t, state.Labels["label1"], result.Instance["__label1__"]) - - assert.Contains(t, state.Labels, "label2") - assert.Equal(t, state.Labels["label2"], result.Instance["label2__"]) - - assert.Contains(t, state.Labels, "label3") - assert.Equal(t, state.Labels["label3"], result.Instance["__label3"]) - - assert.Contains(t, state.Labels, "label4_user") - assert.Equal(t, state.Labels["label4_user"], result.Instance["label4"]) - - t.Run("should drop label if renamed collides with existing", func(t *testing.T) { - result.Instance["label1"] = uuid.NewString() - result.Instance["label1_user"] = uuid.NewString() - result.Instance["label4_user"] = uuid.NewString() - - state = c.create(context.Background(), l, rule, result, nil, url) - assert.NotContains(t, state.Labels, "__label1__") - assert.Contains(t, state.Labels, "label1") - assert.Equal(t, state.Labels["label1"], result.Instance["label1"]) - assert.Equal(t, state.Labels["label1_user"], result.Instance["label1_user"]) - - assert.NotContains(t, state.Labels, "label4") - assert.Equal(t, state.Labels["label4_user"], result.Instance["label4_user"]) - }) - }) - - t.Run("creates a state with preset fields if there is no current state", func(t *testing.T) { - rule := generateRule() - - extraLabels := models.GenerateAlertLabels(2, "extra-") - - result := eval.Result{ - Instance: models.GenerateAlertLabels(5, "result-"), - } - - expectedLbl, expectedAnn := expandAnnotationsAndLabels(context.Background(), l, rule, result, extraLabels, url) - - state := c.create(context.Background(), l, rule, result, extraLabels, url) - - assert.Equal(t, rule.OrgID, state.OrgID) - assert.Equal(t, rule.UID, state.AlertRuleUID) - assert.Equal(t, state.Labels.Fingerprint(), state.CacheID) - assert.Equal(t, result.State, state.State) - assert.Equal(t, "", state.StateReason) - assert.Equal(t, result.Instance.Fingerprint(), state.ResultFingerprint) - assert.Nil(t, state.LatestResult) - assert.Nil(t, state.Error) - assert.Nil(t, state.Image) - assert.EqualValues(t, expectedAnn, state.Annotations) - assert.EqualValues(t, expectedLbl, state.Labels) - assert.Nil(t, state.Values) - assert.Equal(t, result.EvaluatedAt, state.StartsAt) - assert.Equal(t, result.EvaluatedAt, state.EndsAt) - assert.Nil(t, state.ResolvedAt) - assert.Nil(t, state.LastSentAt) - assert.Equal(t, "", state.LastEvaluationString) - assert.Equal(t, result.EvaluatedAt, state.LastEvaluationTime) - assert.Equal(t, result.EvaluationDuration, state.EvaluationDuration) - }) - - t.Run("it populates some fields from the current state if it exists", func(t *testing.T) { - rule := generateRule() - - extraLabels := models.GenerateAlertLabels(2, "extra-") - - result := eval.Result{ - Instance: models.GenerateAlertLabels(5, "result-"), - } - - expectedLbl, expectedAnn := expandAnnotationsAndLabels(context.Background(), l, rule, result, extraLabels, url) - - current := randomSate(rule.GetKey()) - current.CacheID = expectedLbl.Fingerprint() - - c.set(¤t) - - state := c.create(context.Background(), l, rule, result, extraLabels, url) - - assert.Equal(t, rule.OrgID, state.OrgID) - assert.Equal(t, rule.UID, state.AlertRuleUID) - assert.Equal(t, state.Labels.Fingerprint(), state.CacheID) - assert.Equal(t, result.Instance.Fingerprint(), state.ResultFingerprint) - assert.EqualValues(t, expectedAnn, state.Annotations) - assert.EqualValues(t, expectedLbl, state.Labels) - assert.Equal(t, result.EvaluatedAt, state.LastEvaluationTime) - assert.Equal(t, result.EvaluationDuration, state.EvaluationDuration) - - assert.Equal(t, current.State, state.State) - assert.Equal(t, current.StateReason, state.StateReason) - assert.Equal(t, current.Image, state.Image) - assert.Equal(t, current.LatestResult, state.LatestResult) - assert.Equal(t, current.Error, state.Error) - assert.Equal(t, current.Values, state.Values) - assert.Equal(t, current.StartsAt, state.StartsAt) - assert.Equal(t, current.EndsAt, state.EndsAt) - assert.Equal(t, current.ResolvedAt, state.ResolvedAt) - assert.Equal(t, current.LastSentAt, state.LastSentAt) - assert.Equal(t, current.LastEvaluationString, state.LastEvaluationString) - - t.Run("if result Error and current state is Error it should copy datasource_uid and ref_id labels", func(t *testing.T) { - current = randomSate(rule.GetKey()) - current.CacheID = expectedLbl.Fingerprint() - current.State = eval.Error - current.Labels["datasource_uid"] = util.GenerateShortUID() - current.Labels["ref_id"] = util.GenerateShortUID() - - c.set(¤t) - - result.State = eval.Error - state = c.create(context.Background(), l, rule, result, extraLabels, url) - - l := expectedLbl.Copy() - l["datasource_uid"] = current.Labels["datasource_uid"] - l["ref_id"] = current.Labels["ref_id"] - - assert.Equal(t, current.CacheID, state.CacheID) - assert.EqualValues(t, l, state.Labels) - - assert.Equal(t, rule.OrgID, state.OrgID) - assert.Equal(t, rule.UID, state.AlertRuleUID) - - assert.Equal(t, result.Instance.Fingerprint(), state.ResultFingerprint) - assert.EqualValues(t, expectedAnn, state.Annotations) - assert.Equal(t, result.EvaluatedAt, state.LastEvaluationTime) - assert.Equal(t, result.EvaluationDuration, state.EvaluationDuration) - - assert.Equal(t, current.State, state.State) - assert.Equal(t, current.StateReason, state.StateReason) - assert.Equal(t, current.Image, state.Image) - assert.Equal(t, current.LatestResult, state.LatestResult) - assert.Equal(t, current.Error, state.Error) - assert.Equal(t, current.Values, state.Values) - assert.Equal(t, current.StartsAt, state.StartsAt) - assert.Equal(t, current.EndsAt, state.EndsAt) - assert.Equal(t, current.ResolvedAt, state.ResolvedAt) - assert.Equal(t, current.LastSentAt, state.LastSentAt) - assert.Equal(t, current.LastEvaluationString, state.LastEvaluationString) - }) - t.Run("copies system-owned annotations from current state", func(t *testing.T) { - current = randomSate(rule.GetKey()) - current.CacheID = expectedLbl.Fingerprint() - current.State = eval.Error - for key := range models.InternalAnnotationNameSet { - current.Annotations[key] = util.GenerateShortUID() - } - c.set(¤t) - - result.State = eval.Error - state = c.create(context.Background(), l, rule, result, extraLabels, url) - ann := expectedAnn.Copy() - for key := range models.InternalAnnotationNameSet { - ann[key] = current.Annotations[key] - } - assert.EqualValues(t, expectedLbl, state.Labels) - assert.EqualValues(t, ann, state.Annotations) - }) - }) -} - func Test_mergeLabels(t *testing.T) { t.Run("merges two maps", func(t *testing.T) { a := models.GenerateAlertLabels(5, "set1-") diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index cc22a65f517..f66002bbb3b 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -444,9 +444,16 @@ func (st *Manager) setNextStateForRule(ctx context.Context, alertRule *ngModels. } transitions := make([]StateTransition, 0, len(results)) for _, result := range results { - currentState := st.cache.create(ctx, logger, alertRule, result, extraLabels, st.externalURL) - s := st.setNextState(alertRule, currentState, result, nil, logger, takeImageFn) - st.cache.set(currentState) // replace the existing state with the new one + newState := newState(ctx, logger, alertRule, result, extraLabels, st.externalURL) + if curState := st.cache.get(alertRule.OrgID, alertRule.UID, newState.CacheID); curState != nil { + patch(newState, curState, result) + } + start := st.clock.Now() + s := newState.transition(alertRule, result, nil, logger, takeImageFn) + if st.metrics != nil { + st.metrics.StateUpdateDuration.Observe(st.clock.Now().Sub(start).Seconds()) + } + st.cache.set(newState) // replace the existing state with the new one transitions = append(transitions, s) } return transitions @@ -459,8 +466,12 @@ func (st *Manager) setNextStateForAll(alertRule *ngModels.AlertRule, result eval states: make(map[data.Fingerprint]*State, len(currentStates)), } for _, currentState := range currentStates { + start := st.clock.Now() newState := currentState.Copy() - t := st.setNextState(alertRule, newState, result, extraAnnotations, logger, takeImageFn) + t := newState.transition(alertRule, result, extraAnnotations, logger, takeImageFn) + if st.metrics != nil { + st.metrics.StateUpdateDuration.Observe(st.clock.Now().Sub(start).Seconds()) + } updated.states[newState.CacheID] = newState transitions = append(transitions, t) } @@ -468,115 +479,6 @@ func (st *Manager) setNextStateForAll(alertRule *ngModels.AlertRule, result eval return transitions } -// Set the current state based on evaluation results -func (st *Manager) setNextState(alertRule *ngModels.AlertRule, currentState *State, result eval.Result, extraAnnotations data.Labels, logger log.Logger, takeImageFn takeImageFn) StateTransition { - start := st.clock.Now() - - currentState.LastEvaluationTime = result.EvaluatedAt - currentState.EvaluationDuration = result.EvaluationDuration - currentState.SetNextValues(result) - currentState.LatestResult = &Evaluation{ - EvaluationTime: result.EvaluatedAt, - EvaluationState: result.State, - Values: currentState.Values, - Condition: alertRule.Condition, - } - currentState.LastEvaluationString = result.EvaluationString - oldState := currentState.State - oldReason := currentState.StateReason - - // Add the instance to the log context to help correlate log lines for a state - logger = logger.New("instance", result.Instance) - - // if the current state is Error but the result is different, then we need o clean up the extra labels - // that were added after the state key was calculated - // https://github.com/grafana/grafana/blob/1df4d332c982dc5e394201bb2ef35b442727ce63/pkg/services/ngalert/state/state.go#L298-L311 - // Usually, it happens in the case of classic conditions when the evalResult does not have labels. - // - // This is temporary change to make sure that the labels are not persistent in the state after it was in Error state - // TODO yuri. Remove it when correct Error result with labels is provided - if currentState.State == eval.Error && result.State != eval.Error { - // This is possible because state was updated after the CacheID was calculated. - _, curOk := currentState.Labels["ref_id"] - _, resOk := result.Instance["ref_id"] - if curOk && !resOk { - delete(currentState.Labels, "ref_id") - } - _, curOk = currentState.Labels["datasource_uid"] - _, resOk = result.Instance["datasource_uid"] - if curOk && !resOk { - delete(currentState.Labels, "datasource_uid") - } - } - - switch result.State { - case eval.Normal: - logger.Debug("Setting next state", "handler", "resultNormal") - resultNormal(currentState, alertRule, result, logger, "") - case eval.Alerting: - logger.Debug("Setting next state", "handler", "resultAlerting") - resultAlerting(currentState, alertRule, result, logger, "") - case eval.Error: - logger.Debug("Setting next state", "handler", "resultError") - resultError(currentState, alertRule, result, logger) - case eval.NoData: - logger.Debug("Setting next state", "handler", "resultNoData") - resultNoData(currentState, alertRule, result, logger) - case eval.Pending: // we do not emit results with this state - logger.Debug("Ignoring set next state as result is pending") - } - - // Set reason iff: result and state are different, reason is not Alerting or Normal - currentState.StateReason = "" - - if currentState.State != result.State && - result.State != eval.Normal && - result.State != eval.Alerting { - currentState.StateReason = resultStateReason(result, alertRule) - } - - // Set Resolved property so the scheduler knows to send a postable alert - // to Alertmanager. - newlyResolved := false - if oldState == eval.Alerting && currentState.State == eval.Normal { - currentState.ResolvedAt = &result.EvaluatedAt - newlyResolved = true - } else if currentState.State != eval.Normal && currentState.State != eval.Pending { // Retain the last resolved time for Normal->Normal and Normal->Pending. - currentState.ResolvedAt = nil - } - - if reason := shouldTakeImage(currentState.State, oldState, currentState.Image, newlyResolved); reason != "" { - image := takeImageFn(reason) - if image != nil { - currentState.Image = image - } - } - - for key, val := range extraAnnotations { - currentState.Annotations[key] = val - } - - nextState := StateTransition{ - State: currentState, - PreviousState: oldState, - PreviousStateReason: oldReason, - } - - if st.metrics != nil { - st.metrics.StateUpdateDuration.Observe(st.clock.Now().Sub(start).Seconds()) - } - - return nextState -} - -func resultStateReason(result eval.Result, rule *ngModels.AlertRule) string { - if rule.ExecErrState == ngModels.KeepLastErrState || rule.NoDataState == ngModels.KeepLast { - return ngModels.ConcatReasons(result.State.String(), ngModels.StateReasonKeepLast) - } - - return result.State.String() -} - func (st *Manager) GetAll(orgID int64) []*State { allStates := st.cache.getAll(orgID) return allStates diff --git a/pkg/services/ngalert/state/state.go b/pkg/services/ngalert/state/state.go index aee77903096..54694664cca 100644 --- a/pkg/services/ngalert/state/state.go +++ b/pkg/services/ngalert/state/state.go @@ -7,6 +7,7 @@ import ( "fmt" "maps" "math" + "net/url" "strings" "time" @@ -76,6 +77,35 @@ type State struct { EvaluationDuration time.Duration } +func newState(ctx context.Context, log log.Logger, alertRule *models.AlertRule, result eval.Result, extraLabels data.Labels, externalURL *url.URL) *State { + lbs, annotations := expandAnnotationsAndLabels(ctx, log, alertRule, result, extraLabels, externalURL) + + cacheID := lbs.Fingerprint() + // For new states, we set StartsAt & EndsAt to EvaluatedAt as this is the + // expected value for a Normal state during state transition. + return &State{ + OrgID: alertRule.OrgID, + AlertRuleUID: alertRule.UID, + CacheID: cacheID, + State: eval.Normal, + StateReason: "", + ResultFingerprint: result.Instance.Fingerprint(), // remember original result fingerprint + LatestResult: nil, + Error: nil, + Image: nil, + Annotations: annotations, + Labels: lbs, + Values: nil, + StartsAt: result.EvaluatedAt, + EndsAt: result.EvaluatedAt, + ResolvedAt: nil, + LastSentAt: nil, + LastEvaluationString: "", + LastEvaluationTime: result.EvaluatedAt, + EvaluationDuration: result.EvaluationDuration, + } +} + // Copy creates a shallow copy of the State except for labels and annotations. func (a *State) Copy() *State { // Deep copy annotations and labels @@ -664,3 +694,147 @@ func GetRuleExtraLabels(l log.Logger, rule *models.AlertRule, folderTitle string } return extraLabels } + +func patch(newState, existingState *State, result eval.Result) { + // if there is existing state, copy over the current values that may be needed to determine the final state. + // TODO remove some unnecessary assignments below because they are overridden in setNextState + newState.State = existingState.State + newState.StateReason = existingState.StateReason + newState.Image = existingState.Image + newState.LatestResult = existingState.LatestResult + newState.Error = existingState.Error + newState.Values = existingState.Values + newState.LastEvaluationString = existingState.LastEvaluationString + newState.StartsAt = existingState.StartsAt + newState.EndsAt = existingState.EndsAt + newState.ResolvedAt = existingState.ResolvedAt + newState.LastSentAt = existingState.LastSentAt + // Annotations can change over time, however we also want to maintain + // certain annotations across evaluations + for key := range models.InternalAnnotationNameSet { // Changing in + value, ok := existingState.Annotations[key] + if !ok { + continue + } + // If the annotation is not present then it should be copied from + // the current state to the new state + if _, ok = newState.Annotations[key]; !ok { + newState.Annotations[key] = value + } + } + + // if the current state is "data source error" then it may have additional labels that may not exist in the new state. + // See https://github.com/grafana/grafana/blob/c7fdf8ce706c2c9d438f5e6eabd6e580bac4946b/pkg/services/ngalert/state/state.go#L161-L163 + // copy known labels over to the new instance, it can help reduce flapping + // TODO fix this? + if existingState.State == eval.Error && result.State == eval.Error { + setIfExist := func(lbl string) { + if v, ok := existingState.Labels[lbl]; ok { + newState.Labels[lbl] = v + } + } + setIfExist("datasource_uid") + setIfExist("ref_id") + } +} + +func (a *State) transition(alertRule *models.AlertRule, result eval.Result, extraAnnotations data.Labels, logger log.Logger, takeImageFn takeImageFn) StateTransition { + a.LastEvaluationTime = result.EvaluatedAt + a.EvaluationDuration = result.EvaluationDuration + a.SetNextValues(result) + a.LatestResult = &Evaluation{ + EvaluationTime: result.EvaluatedAt, + EvaluationState: result.State, + Values: a.Values, + Condition: alertRule.Condition, + } + a.LastEvaluationString = result.EvaluationString + oldState := a.State + oldReason := a.StateReason + + // Add the instance to the log context to help correlate log lines for a state + logger = logger.New("instance", result.Instance) + + // if the current state is Error but the result is different, then we need o clean up the extra labels + // that were added after the state key was calculated + // https://github.com/grafana/grafana/blob/1df4d332c982dc5e394201bb2ef35b442727ce63/pkg/services/ngalert/state/state.go#L298-L311 + // Usually, it happens in the case of classic conditions when the evalResult does not have labels. + // + // This is temporary change to make sure that the labels are not persistent in the state after it was in Error state + // TODO yuri. Remove it when correct Error result with labels is provided + if a.State == eval.Error && result.State != eval.Error { + // This is possible because state was updated after the CacheID was calculated. + _, curOk := a.Labels["ref_id"] + _, resOk := result.Instance["ref_id"] + if curOk && !resOk { + delete(a.Labels, "ref_id") + } + _, curOk = a.Labels["datasource_uid"] + _, resOk = result.Instance["datasource_uid"] + if curOk && !resOk { + delete(a.Labels, "datasource_uid") + } + } + + switch result.State { + case eval.Normal: + logger.Debug("Setting next state", "handler", "resultNormal") + resultNormal(a, alertRule, result, logger, "") + case eval.Alerting: + logger.Debug("Setting next state", "handler", "resultAlerting") + resultAlerting(a, alertRule, result, logger, "") + case eval.Error: + logger.Debug("Setting next state", "handler", "resultError") + resultError(a, alertRule, result, logger) + case eval.NoData: + logger.Debug("Setting next state", "handler", "resultNoData") + resultNoData(a, alertRule, result, logger) + case eval.Pending: // we do not emit results with this state + logger.Debug("Ignoring set next state as result is pending") + } + + // Set reason iff: result and state are different, reason is not Alerting or Normal + a.StateReason = "" + + if a.State != result.State && + result.State != eval.Normal && + result.State != eval.Alerting { + a.StateReason = resultStateReason(result, alertRule) + } + + // Set Resolved property so the scheduler knows to send a postable alert + // to Alertmanager. + newlyResolved := false + if oldState == eval.Alerting && a.State == eval.Normal { + a.ResolvedAt = &result.EvaluatedAt + newlyResolved = true + } else if a.State != eval.Normal && a.State != eval.Pending { // Retain the last resolved time for Normal->Normal and Normal->Pending. + a.ResolvedAt = nil + } + + if reason := shouldTakeImage(a.State, oldState, a.Image, newlyResolved); reason != "" { + image := takeImageFn(reason) + if image != nil { + a.Image = image + } + } + + for key, val := range extraAnnotations { + a.Annotations[key] = val + } + + nextState := StateTransition{ + State: a, + PreviousState: oldState, + PreviousStateReason: oldReason, + } + return nextState +} + +func resultStateReason(result eval.Result, rule *models.AlertRule) string { + if rule.ExecErrState == models.KeepLastErrState || rule.NoDataState == models.KeepLast { + return models.ConcatReasons(result.State.String(), models.StateReasonKeepLast) + } + + return result.State.String() +} diff --git a/pkg/services/ngalert/state/cache_bench_test.go b/pkg/services/ngalert/state/state_bench_test.go similarity index 85% rename from pkg/services/ngalert/state/cache_bench_test.go rename to pkg/services/ngalert/state/state_bench_test.go index 357eabdd9e3..8f1ec2550c8 100644 --- a/pkg/services/ngalert/state/cache_bench_test.go +++ b/pkg/services/ngalert/state/state_bench_test.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" ) -func BenchmarkGetOrCreateTest(b *testing.B) { +func BenchmarkCreateAndPatch(b *testing.B) { cache := newCache() rule := models.RuleGen.With(func(rule *models.AlertRule) { for i := 0; i < 2; i++ { @@ -43,7 +43,11 @@ func BenchmarkGetOrCreateTest(b *testing.B) { // values := make([]int64, count) b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _ = cache.create(ctx, log, rule, result, nil, u) + s := newState(ctx, log, rule, result, nil, u) + current := cache.get(rule.OrgID, rule.UID, s.CacheID) + if current == nil { + patch(s, current, result) + } } }) } diff --git a/pkg/services/ngalert/state/state_test.go b/pkg/services/ngalert/state/state_test.go index 2515fe33190..7216784a65c 100644 --- a/pkg/services/ngalert/state/state_test.go +++ b/pkg/services/ngalert/state/state_test.go @@ -3,14 +3,17 @@ package state import ( "context" "errors" + "fmt" "math" "math/rand" + "net/url" "testing" "time" "github.com/benbjohnson/clock" "github.com/golang/mock/gomock" "github.com/google/uuid" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -801,3 +804,331 @@ func TestGetRuleExtraLabels(t *testing.T) { }) } } + +func TestNewState(t *testing.T) { + url := &url.URL{ + Scheme: "http", + Host: "localhost:3000", + Path: "/test", + } + l := log.New("test") + + gen := ngmodels.RuleGen + generateRule := gen.With(gen.WithNotEmptyLabels(5, "rule-")).GenerateRef + + t.Run("should combine all labels", func(t *testing.T) { + rule := generateRule() + + extraLabels := ngmodels.GenerateAlertLabels(5, "extra-") + result := eval.Result{ + Instance: ngmodels.GenerateAlertLabels(5, "result-"), + } + state := newState(context.Background(), l, rule, result, extraLabels, url) + for key, expected := range extraLabels { + require.Equal(t, expected, state.Labels[key]) + } + assert.Len(t, state.Labels, len(extraLabels)+len(rule.Labels)+len(result.Instance)) + for key, expected := range extraLabels { + assert.Equal(t, expected, state.Labels[key]) + } + for key, expected := range rule.Labels { + assert.Equal(t, expected, state.Labels[key]) + } + for key, expected := range result.Instance { + assert.Equal(t, expected, state.Labels[key]) + } + }) + t.Run("extra labels should take precedence over rule and result labels", func(t *testing.T) { + rule := generateRule() + + extraLabels := ngmodels.GenerateAlertLabels(2, "extra-") + + result := eval.Result{ + Instance: ngmodels.GenerateAlertLabels(5, "result-"), + } + for key := range extraLabels { + rule.Labels[key] = "rule-" + util.GenerateShortUID() + result.Instance[key] = "result-" + util.GenerateShortUID() + } + + state := newState(context.Background(), l, rule, result, extraLabels, url) + for key, expected := range extraLabels { + require.Equal(t, expected, state.Labels[key]) + } + }) + t.Run("rule labels should take precedence over result labels", func(t *testing.T) { + rule := generateRule() + + extraLabels := ngmodels.GenerateAlertLabels(2, "extra-") + + result := eval.Result{ + Instance: ngmodels.GenerateAlertLabels(5, "result-"), + } + for key := range rule.Labels { + result.Instance[key] = "result-" + util.GenerateShortUID() + } + state := newState(context.Background(), l, rule, result, extraLabels, url) + for key, expected := range rule.Labels { + require.Equal(t, expected, state.Labels[key]) + } + }) + t.Run("rule labels should be able to be expanded with result and extra labels", func(t *testing.T) { + result := eval.Result{ + Instance: ngmodels.GenerateAlertLabels(5, "result-"), + } + rule := generateRule() + + extraLabels := ngmodels.GenerateAlertLabels(2, "extra-") + + labelTemplates := make(data.Labels) + for key := range extraLabels { + labelTemplates["rule-"+key] = fmt.Sprintf("{{ with (index .Labels \"%s\") }}{{.}}{{end}}", key) + } + for key := range result.Instance { + labelTemplates["rule-"+key] = fmt.Sprintf("{{ with (index .Labels \"%s\") }}{{.}}{{end}}", key) + } + rule.Labels = labelTemplates + + state := newState(context.Background(), l, rule, result, extraLabels, url) + for key, expected := range extraLabels { + assert.Equal(t, expected, state.Labels["rule-"+key]) + } + for key, expected := range result.Instance { + assert.Equal(t, expected, state.Labels["rule-"+key]) + } + }) + t.Run("rule annotations should be able to be expanded with result and extra labels", func(t *testing.T) { + result := eval.Result{ + Instance: ngmodels.GenerateAlertLabels(5, "result-"), + } + + rule := generateRule() + + extraLabels := ngmodels.GenerateAlertLabels(2, "extra-") + + annotationTemplates := make(data.Labels) + for key := range extraLabels { + annotationTemplates["rule-"+key] = fmt.Sprintf("{{ with (index .Labels \"%s\") }}{{.}}{{end}}", key) + } + for key := range result.Instance { + annotationTemplates["rule-"+key] = fmt.Sprintf("{{ with (index .Labels \"%s\") }}{{.}}{{end}}", key) + } + rule.Annotations = annotationTemplates + + state := newState(context.Background(), l, rule, result, extraLabels, url) + for key, expected := range extraLabels { + assert.Equal(t, expected, state.Annotations["rule-"+key]) + } + for key, expected := range result.Instance { + assert.Equal(t, expected, state.Annotations["rule-"+key]) + } + }) + t.Run("when result labels collide with system labels from LabelsUserCannotSpecify", func(t *testing.T) { + result := eval.Result{ + Instance: ngmodels.GenerateAlertLabels(5, "result-"), + } + m := ngmodels.LabelsUserCannotSpecify + t.Cleanup(func() { + ngmodels.LabelsUserCannotSpecify = m + }) + + ngmodels.LabelsUserCannotSpecify = map[string]struct{}{ + "__label1__": {}, + "label2__": {}, + "__label3": {}, + "label4": {}, + } + result.Instance["__label1__"] = uuid.NewString() + result.Instance["label2__"] = uuid.NewString() + result.Instance["__label3"] = uuid.NewString() + result.Instance["label4"] = uuid.NewString() + + rule := generateRule() + + state := newState(context.Background(), l, rule, result, nil, url) + + for key := range ngmodels.LabelsUserCannotSpecify { + assert.NotContains(t, state.Labels, key) + } + assert.Contains(t, state.Labels, "label1") + assert.Equal(t, state.Labels["label1"], result.Instance["__label1__"]) + + assert.Contains(t, state.Labels, "label2") + assert.Equal(t, state.Labels["label2"], result.Instance["label2__"]) + + assert.Contains(t, state.Labels, "label3") + assert.Equal(t, state.Labels["label3"], result.Instance["__label3"]) + + assert.Contains(t, state.Labels, "label4_user") + assert.Equal(t, state.Labels["label4_user"], result.Instance["label4"]) + + t.Run("should drop label if renamed collides with existing", func(t *testing.T) { + result.Instance["label1"] = uuid.NewString() + result.Instance["label1_user"] = uuid.NewString() + result.Instance["label4_user"] = uuid.NewString() + + state = newState(context.Background(), l, rule, result, nil, url) + assert.NotContains(t, state.Labels, "__label1__") + assert.Contains(t, state.Labels, "label1") + assert.Equal(t, state.Labels["label1"], result.Instance["label1"]) + assert.Equal(t, state.Labels["label1_user"], result.Instance["label1_user"]) + + assert.NotContains(t, state.Labels, "label4") + assert.Equal(t, state.Labels["label4_user"], result.Instance["label4_user"]) + }) + }) + + t.Run("creates a state with preset fields if there is no current state", func(t *testing.T) { + rule := generateRule() + + extraLabels := ngmodels.GenerateAlertLabels(2, "extra-") + + result := eval.Result{ + Instance: ngmodels.GenerateAlertLabels(5, "result-"), + } + + expectedLbl, expectedAnn := expandAnnotationsAndLabels(context.Background(), l, rule, result, extraLabels, url) + + state := newState(context.Background(), l, rule, result, extraLabels, url) + + assert.Equal(t, rule.OrgID, state.OrgID) + assert.Equal(t, rule.UID, state.AlertRuleUID) + assert.Equal(t, state.Labels.Fingerprint(), state.CacheID) + assert.Equal(t, result.State, state.State) + assert.Equal(t, "", state.StateReason) + assert.Equal(t, result.Instance.Fingerprint(), state.ResultFingerprint) + assert.Nil(t, state.LatestResult) + assert.Nil(t, state.Error) + assert.Nil(t, state.Image) + assert.EqualValues(t, expectedAnn, state.Annotations) + assert.EqualValues(t, expectedLbl, state.Labels) + assert.Nil(t, state.Values) + assert.Equal(t, result.EvaluatedAt, state.StartsAt) + assert.Equal(t, result.EvaluatedAt, state.EndsAt) + assert.Nil(t, state.ResolvedAt) + assert.Nil(t, state.LastSentAt) + assert.Equal(t, "", state.LastEvaluationString) + assert.Equal(t, result.EvaluatedAt, state.LastEvaluationTime) + assert.Equal(t, result.EvaluationDuration, state.EvaluationDuration) + }) +} + +func TestPatch(t *testing.T) { + key := ngmodels.GenerateRuleKey(1) + t.Run("it populates some fields from the current state if it exists", func(t *testing.T) { + result := eval.Result{ + Instance: ngmodels.GenerateAlertLabels(5, "result-"), + } + + state := randomSate(key) + orig := state.Copy() + current := randomSate(key) + + patch(&state, ¤t, result) + + // Fields that should not change + assert.Equal(t, orig.OrgID, state.OrgID) + assert.Equal(t, orig.AlertRuleUID, state.AlertRuleUID) + assert.Equal(t, orig.CacheID, state.CacheID) + assert.Equal(t, orig.ResultFingerprint, state.ResultFingerprint) + assert.EqualValues(t, orig.Annotations, state.Annotations) + assert.EqualValues(t, orig.Labels, state.Labels) + assert.Equal(t, orig.LastEvaluationTime, state.LastEvaluationTime) + assert.Equal(t, orig.EvaluationDuration, state.EvaluationDuration) + + assert.Equal(t, current.State, state.State) + assert.Equal(t, current.StateReason, state.StateReason) + assert.Equal(t, current.Image, state.Image) + assert.Equal(t, current.LatestResult, state.LatestResult) + assert.Equal(t, current.Error, state.Error) + assert.Equal(t, current.Values, state.Values) + assert.Equal(t, current.StartsAt, state.StartsAt) + assert.Equal(t, current.EndsAt, state.EndsAt) + assert.Equal(t, current.ResolvedAt, state.ResolvedAt) + assert.Equal(t, current.LastSentAt, state.LastSentAt) + assert.Equal(t, current.LastEvaluationString, state.LastEvaluationString) + }) + + t.Run("copies system-owned annotations from current state", func(t *testing.T) { + state := randomSate(key) + orig := state.Copy() + expectedAnnotations := data.Labels(state.Annotations).Copy() + current := randomSate(key) + + for key := range ngmodels.InternalAnnotationNameSet { + val := util.GenerateShortUID() + current.Annotations[key] = val + expectedAnnotations[key] = val + } + + result := eval.Result{ + Instance: ngmodels.GenerateAlertLabels(5, "result-"), + } + + patch(&state, ¤t, result) + + assert.EqualValues(t, expectedAnnotations, state.Annotations) + assert.Equal(t, current.State, state.State) + assert.Equal(t, current.StateReason, state.StateReason) + assert.Equal(t, current.Image, state.Image) + assert.Equal(t, current.LatestResult, state.LatestResult) + assert.Equal(t, current.Error, state.Error) + assert.Equal(t, current.Values, state.Values) + assert.Equal(t, current.StartsAt, state.StartsAt) + assert.Equal(t, current.EndsAt, state.EndsAt) + assert.Equal(t, current.ResolvedAt, state.ResolvedAt) + assert.Equal(t, current.LastSentAt, state.LastSentAt) + assert.Equal(t, current.LastEvaluationString, state.LastEvaluationString) + + // Fields that should not change + assert.Equal(t, orig.OrgID, state.OrgID) + assert.Equal(t, orig.AlertRuleUID, state.AlertRuleUID) + assert.Equal(t, orig.CacheID, state.CacheID) + assert.Equal(t, orig.ResultFingerprint, state.ResultFingerprint) + assert.EqualValues(t, orig.Labels, state.Labels) + assert.Equal(t, orig.LastEvaluationTime, state.LastEvaluationTime) + assert.Equal(t, orig.EvaluationDuration, state.EvaluationDuration) + }) + + t.Run("if result Error and current state is Error it should copy datasource_uid and ref_id labels", func(t *testing.T) { + state := randomSate(key) + orig := state.Copy() + current := randomSate(key) + current.State = eval.Error + current.Labels["datasource_uid"] = util.GenerateShortUID() + current.Labels["ref_id"] = util.GenerateShortUID() + + result := eval.Result{ + Instance: ngmodels.GenerateAlertLabels(5, "result-"), + State: eval.Error, + } + + expectedLabels := orig.Labels.Copy() + expectedLabels["datasource_uid"] = current.Labels["datasource_uid"] + expectedLabels["ref_id"] = current.Labels["ref_id"] + + patch(&state, ¤t, result) + + assert.Equal(t, expectedLabels, state.Labels) + assert.Equal(t, current.State, state.State) + assert.Equal(t, current.StateReason, state.StateReason) + assert.Equal(t, current.Image, state.Image) + assert.Equal(t, current.LatestResult, state.LatestResult) + assert.Equal(t, current.Error, state.Error) + assert.Equal(t, current.Values, state.Values) + assert.Equal(t, current.StartsAt, state.StartsAt) + assert.Equal(t, current.EndsAt, state.EndsAt) + assert.Equal(t, current.ResolvedAt, state.ResolvedAt) + assert.Equal(t, current.LastSentAt, state.LastSentAt) + assert.Equal(t, current.LastEvaluationString, state.LastEvaluationString) + + // Fields that should not change + assert.Equal(t, orig.OrgID, state.OrgID) + assert.Equal(t, orig.AlertRuleUID, state.AlertRuleUID) + assert.Equal(t, orig.CacheID, state.CacheID) + assert.Equal(t, orig.ResultFingerprint, state.ResultFingerprint) + assert.Equal(t, orig.LastEvaluationTime, state.LastEvaluationTime) + assert.Equal(t, orig.EvaluationDuration, state.EvaluationDuration) + assert.EqualValues(t, orig.Annotations, state.Annotations) + }) +} From 7edcde63650b3070b2b050c738cb1fb509731780 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 13 Feb 2025 15:46:22 +0100 Subject: [PATCH 566/894] Dashboards: Bring back scripted dashboards (#100575) * Dashboards: Bring back scripted dashboards * Fix scripted dashboard examples * Fix dashboard-solo page not respecnig scripted dashboards --- .../pages/DashboardScenePage.tsx | 2 + .../pages/DashboardScenePageStateManager.ts | 9 ++++- .../dashboard-scene/solo/SoloPanelPage.tsx | 7 ++-- .../containers/DashboardPageProxy.tsx | 1 + public/app/routes/routes.tsx | 6 ++- public/dashboards/scripted.js | 28 +++++--------- public/dashboards/scripted_async.js | 3 +- public/dashboards/scripted_templated.js | 38 ++++++++++++------- 8 files changed, 52 insertions(+), 42 deletions(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index 6ac66d3d312..19a4c412b42 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -37,6 +37,8 @@ export function DashboardScenePage({ route, queryParams, location }: Props) { stateManager.loadSnapshot(slug!); } else { stateManager.loadDashboard({ + type, + slug, uid: uid ?? '', route: route.routeName as DashboardRoutes, urlFolderUid: queryParams.folderUid, diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 161ee48f10c..b9f37706bad 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -63,6 +63,7 @@ export interface LoadDashboardOptions { uid: string; route: DashboardRoutes; type?: string; + slug?: string; urlFolderUid?: string; params?: { version: number; @@ -266,6 +267,8 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag } public async fetchDashboard({ + type, + slug, uid, route, urlFolderUid, @@ -323,7 +326,7 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag } : undefined; - rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid, queryParams); + rsp = await dashboardLoaderSrv.loadDashboard(type || 'db', slug || '', uid, queryParams); if (route === DashboardRoutes.Embedded) { rsp.meta.isEmbedded = true; @@ -477,6 +480,8 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan } public async fetchDashboard({ + type, + slug, uid, route, urlFolderUid, @@ -529,7 +534,7 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan ...params.variables, } : undefined; - rsp = await this.dashboardLoader.loadDashboard('db', '', uid, queryParams); + rsp = await this.dashboardLoader.loadDashboard(type || 'db', slug || '', uid, queryParams); if (route === DashboardRoutes.Embedded) { throw new Error('Method not implemented.'); // rsp.meta.isEmbedded = true; diff --git a/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx b/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx index 265d32c9e72..01894fe9d9e 100644 --- a/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx +++ b/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx @@ -26,12 +26,12 @@ export interface Props extends GrafanaRouteComponentProps { - stateManager.loadDashboard({ uid, route: DashboardRoutes.Embedded }); + stateManager.loadDashboard({ uid, type, slug, route: DashboardRoutes.Embedded }); return () => stateManager.clearState(); - }, [stateManager, queryParams, uid]); + }, [stateManager, queryParams, uid, type, slug]); if (!queryParams.panelId) { return ; @@ -64,7 +64,6 @@ export function SoloPanelRenderer({ dashboard, panelId }: { dashboard: Dashboard const [panel, error] = useSoloPanel(dashboard, panelId); const { controls } = dashboard.useState(); const refreshPicker = controls?.useState()?.refreshPicker; - const styles = useStyles2(getStyles); useEffect(() => { diff --git a/public/app/features/dashboard/containers/DashboardPageProxy.tsx b/public/app/features/dashboard/containers/DashboardPageProxy.tsx index 52d91d5d956..1570fbc6ee7 100644 --- a/public/app/features/dashboard/containers/DashboardPageProxy.tsx +++ b/public/app/features/dashboard/containers/DashboardPageProxy.tsx @@ -51,6 +51,7 @@ function DashboardPageProxy(props: DashboardPageProxyProps) { route: props.route.routeName as DashboardRoutes, uid: params.uid ?? '', type: params.type, + slug: params.slug, }); }, [params.uid, props.route.routeName]); diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 839eb62ad85..e518b69eb60 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -96,8 +96,10 @@ export function getAppRoutes(): RouteDescriptor[] { path: '/dashboard-solo/:type/:slug', routeName: DashboardRoutes.Normal, chromeless: true, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') + component: SafeDynamicImport(() => + config.featureToggles.dashboardSceneSolo + ? import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard-scene/solo/SoloPanelPage') + : import(/* webpackChunkName: "SoloPanelPageOld" */ '../features/dashboard/containers/SoloPanelPage') ), }, { diff --git a/public/dashboards/scripted.js b/public/dashboards/scripted.js index daa4c4efb70..03de55bff44 100644 --- a/public/dashboards/scripted.js +++ b/public/dashboards/scripted.js @@ -13,16 +13,11 @@ 'use strict'; -// accessible variables in this scope -let window, document, $, jQuery, moment, kbn; +// accessible variables in this scope: window, document, $, jQuery, moment, kbn; // Setup some variables let dashboard; -// All url parameters are available via the ARGS object -// eslint-disable-next-line no-redeclare -let ARGS; - // Initialize a skeleton with nothing but a rows array and service object dashboard = { rows: [], @@ -56,6 +51,7 @@ for (let i = 0; i < rows; i++) { height: '300px', panels: [ { + id: 1, title: 'Events', type: 'graph', span: 12, @@ -63,23 +59,17 @@ for (let i = 0; i < rows; i++) { linewidth: 2, targets: [ { - target: "randomWalk('" + seriesName + "')", + scenarioId: 'random_walk', + refId: 'A', + seriesCount: 1, + alias: seriesName, }, { - target: "randomWalk('random walk2')", + scenarioId: 'random_walk', + refId: 'B', + seriesCount: 1, }, ], - seriesOverrides: [ - { - alias: '/random/', - yaxis: 2, - fill: 0, - linewidth: 5, - }, - ], - tooltip: { - shared: true, - }, }, ], }); diff --git a/public/dashboards/scripted_async.js b/public/dashboards/scripted_async.js index 98fb144d243..d13a80eb034 100644 --- a/public/dashboards/scripted_async.js +++ b/public/dashboards/scripted_async.js @@ -17,7 +17,7 @@ 'use strict'; // accessible variables in this scope -let window, document, ARGS, $, jQuery, moment, kbn; +// let window, document, ARGS, $, jQuery, moment, kbn; return function (callback) { // Setup some variables @@ -60,6 +60,7 @@ return function (callback) { height: '300px', panels: [ { + id: 1, title: 'Async dashboard test', type: 'text', span: 12, diff --git a/public/dashboards/scripted_templated.js b/public/dashboards/scripted_templated.js index df8647ff86e..0b09fa50986 100644 --- a/public/dashboards/scripted_templated.js +++ b/public/dashboards/scripted_templated.js @@ -14,14 +14,14 @@ 'use strict'; // accessible variables in this scope -let window, document, $, jQuery, moment, kbn; +// let window, document, $, jQuery, moment, kbn; // Setup some variables let dashboard; // All url parameters are available via the ARGS object // eslint-disable-next-line no-redeclare -let ARGS; +// let ARGS; // Initialize a skeleton with nothing but a rows array and service object dashboard = { @@ -44,19 +44,22 @@ dashboard.templating = { list: [ { name: 'test', - query: 'apps.backend.*', - refresh: 1, - type: 'query', - datasource: null, hide: 2, + includeAll: false, + multi: false, + query: 'a,b,c\n', + skipUrlSync: false, + type: 'custom', }, { - name: 'test2', - query: '*', - refresh: 1, - type: 'query', - datasource: null, - hide: 2, + name: 'seriesName', + label: 'Series name', + hide: 0, + includeAll: false, + multi: false, + query: 'series1,series2,series3\n', + skipUrlSync: false, + type: 'custom', }, ], }; @@ -78,6 +81,7 @@ for (let i = 0; i < rows; i++) { height: '300px', panels: [ { + id: 1, title: 'Events', type: 'graph', span: 12, @@ -85,10 +89,16 @@ for (let i = 0; i < rows; i++) { linewidth: 2, targets: [ { - target: "randomWalk('" + seriesName + "')", + scenarioId: 'random_walk', + refId: 'A', + seriesCount: 1, + alias: seriesName, }, { - target: "randomWalk('[[test2]]')", + scenarioId: 'random_walk', + refId: 'B', + seriesCount: 1, + alias: '${seriesName}', }, ], }, From 527fc3bb21f969d829c932e79e5226f7e1a47d43 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Thu, 13 Feb 2025 15:56:16 +0100 Subject: [PATCH 567/894] Dashboards: Remove `schemaVersion < min_version` validation (#100555) --- pkg/apis/dashboard/migration/migrate.go | 4 ---- pkg/apis/dashboard/migration/migrate_test.go | 9 --------- .../dashboard/migration/schemaversion/errors.go | 15 --------------- .../migration/schemaversion/migrations.go | 5 +---- pkg/apis/dashboard/v1alpha1/conversion.go | 9 +-------- pkg/apis/dashboard/v2alpha1/conversion.go | 9 +-------- 6 files changed, 3 insertions(+), 48 deletions(-) diff --git a/pkg/apis/dashboard/migration/migrate.go b/pkg/apis/dashboard/migration/migrate.go index 36a4679f191..2a419596767 100644 --- a/pkg/apis/dashboard/migration/migrate.go +++ b/pkg/apis/dashboard/migration/migrate.go @@ -9,10 +9,6 @@ func Migrate(dash map[string]interface{}, targetVersion int) error { inputVersion := schemaversion.GetSchemaVersion(dash) dash["schemaVersion"] = inputVersion - if inputVersion < schemaversion.MINIUM_VERSION { - return schemaversion.NewMinimumVersionError(inputVersion) - } - for nextVersion := inputVersion + 1; nextVersion <= targetVersion; nextVersion++ { if migration, ok := schemaversion.Migrations[nextVersion]; ok { if err := migration(dash); err != nil { diff --git a/pkg/apis/dashboard/migration/migrate_test.go b/pkg/apis/dashboard/migration/migrate_test.go index a24baa993ee..e037c37f00f 100644 --- a/pkg/apis/dashboard/migration/migrate_test.go +++ b/pkg/apis/dashboard/migration/migrate_test.go @@ -22,15 +22,6 @@ func TestMigrate(t *testing.T) { files, err := os.ReadDir(INPUT_DIR) require.NoError(t, err) - t.Run("minimum version check", func(t *testing.T) { - err := migration.Migrate(map[string]interface{}{ - "schemaVersion": schemaversion.MINIUM_VERSION - 1, - }, schemaversion.MINIUM_VERSION) - - var minVersionErr = schemaversion.NewMinimumVersionError(schemaversion.MINIUM_VERSION - 1) - require.ErrorAs(t, err, &minVersionErr) - }) - for _, f := range files { if f.IsDir() { continue diff --git a/pkg/apis/dashboard/migration/schemaversion/errors.go b/pkg/apis/dashboard/migration/schemaversion/errors.go index 110a596a1ad..f5bbbe7d1fa 100644 --- a/pkg/apis/dashboard/migration/schemaversion/errors.go +++ b/pkg/apis/dashboard/migration/schemaversion/errors.go @@ -2,23 +2,8 @@ package schemaversion import "fmt" -var _ error = &MinimumVersionError{} var _ error = &MigrationError{} -// MinimumVersionError is an error that is returned when the schema version is below the minimum version. -func NewMinimumVersionError(inputVersion int) *MinimumVersionError { - return &MinimumVersionError{inputVersion: inputVersion} -} - -// MinimumVersionError is an error type for minimum version errors. -type MinimumVersionError struct { - inputVersion int -} - -func (e *MinimumVersionError) Error() string { - return fmt.Errorf("input schema version is below minimum version. input: %d minimum: %d", e.inputVersion, MINIUM_VERSION).Error() -} - // ErrMigrationFailed is an error that is returned when a migration fails. func NewMigrationError(msg string, currentVersion, targetVersion int) *MigrationError { return &MigrationError{ diff --git a/pkg/apis/dashboard/migration/schemaversion/migrations.go b/pkg/apis/dashboard/migration/schemaversion/migrations.go index ef46439a591..3d83da73a3f 100644 --- a/pkg/apis/dashboard/migration/schemaversion/migrations.go +++ b/pkg/apis/dashboard/migration/schemaversion/migrations.go @@ -4,10 +4,7 @@ import "strconv" type SchemaVersionMigrationFunc func(map[string]interface{}) error -const ( - MINIUM_VERSION = 36 - LATEST_VERSION = 41 -) +const LATEST_VERSION = 41 var Migrations = map[int]SchemaVersionMigrationFunc{ 37: V37, diff --git a/pkg/apis/dashboard/v1alpha1/conversion.go b/pkg/apis/dashboard/v1alpha1/conversion.go index 358c482c4e3..c01b13db29c 100644 --- a/pkg/apis/dashboard/v1alpha1/conversion.go +++ b/pkg/apis/dashboard/v1alpha1/conversion.go @@ -1,8 +1,6 @@ package v1alpha1 import ( - "errors" - conversion "k8s.io/apimachinery/pkg/conversion" klog "k8s.io/klog/v2" @@ -15,12 +13,7 @@ func Convert_v0alpha1_Unstructured_To_v1alpha1_DashboardSpec(in *common.Unstruct out.Unstructured = *in err := migration.Migrate(out.Unstructured.Object, schemaversion.LATEST_VERSION) if err != nil { - minErr := &schemaversion.MinimumVersionError{} - if errors.As(err, &minErr) { - out.Unstructured.Object["__migrationError"] = err.Error() - } else { - return err - } + return err } t, ok := out.Unstructured.Object["title"].(string) diff --git a/pkg/apis/dashboard/v2alpha1/conversion.go b/pkg/apis/dashboard/v2alpha1/conversion.go index 280cac67b14..9a1818fc0c3 100644 --- a/pkg/apis/dashboard/v2alpha1/conversion.go +++ b/pkg/apis/dashboard/v2alpha1/conversion.go @@ -1,8 +1,6 @@ package v2alpha1 import ( - "errors" - conversion "k8s.io/apimachinery/pkg/conversion" klog "k8s.io/klog/v2" @@ -15,12 +13,7 @@ func Convert_v0alpha1_Unstructured_To_v2alpha1_DashboardSpec(in *common.Unstruct out.Unstructured = *in err := migration.Migrate(out.Unstructured.Object, schemaversion.LATEST_VERSION) if err != nil { - minErr := &schemaversion.MinimumVersionError{} - if errors.As(err, &minErr) { - out.Unstructured.Object["__migrationError"] = err.Error() - } else { - return err - } + return err } t, ok := out.Unstructured.Object["title"].(string) From 5a74a1a0f6b795e943865f9ac2a473611578b855 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:19:22 -0500 Subject: [PATCH 568/894] Metrics: Use correct gatherer in graphite bridge (#100624) --- pkg/infra/metrics/service.go | 6 ++++-- pkg/infra/metrics/settings.go | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/infra/metrics/service.go b/pkg/infra/metrics/service.go index f704ab9248b..99c1e048d4f 100644 --- a/pkg/infra/metrics/service.go +++ b/pkg/infra/metrics/service.go @@ -26,12 +26,13 @@ func (lw *logWrapper) Println(v ...any) { lw.logger.Info("graphite metric bridge", v...) } -func ProvideService(cfg *setting.Cfg, reg prometheus.Registerer) (*InternalMetricsService, error) { +func ProvideService(cfg *setting.Cfg, reg prometheus.Registerer, gatherer prometheus.Gatherer) (*InternalMetricsService, error) { initMetricVars(reg) initFrontendMetrics(reg) s := &InternalMetricsService{ - Cfg: cfg, + Cfg: cfg, + gatherer: gatherer, } return s, s.readSettings() } @@ -41,6 +42,7 @@ type InternalMetricsService struct { intervalSeconds int64 graphiteCfg *graphitebridge.Config + gatherer prometheus.Gatherer } func (im *InternalMetricsService) Run(ctx context.Context) error { diff --git a/pkg/infra/metrics/settings.go b/pkg/infra/metrics/settings.go index 54715db249e..587956158f2 100644 --- a/pkg/infra/metrics/settings.go +++ b/pkg/infra/metrics/settings.go @@ -5,8 +5,6 @@ import ( "strings" "time" - "github.com/prometheus/client_golang/prometheus" - "github.com/grafana/grafana/pkg/infra/metrics/graphitebridge" ) @@ -40,7 +38,7 @@ func (im *InternalMetricsService) parseGraphiteSettings() error { URL: address, Prefix: graphiteSection.Key("prefix").MustString("prod.grafana.%(instance_name)s"), CountersAsDelta: true, - Gatherer: prometheus.DefaultGatherer, + Gatherer: im.gatherer, Interval: time.Duration(im.intervalSeconds) * time.Second, Timeout: 10 * time.Second, Logger: &logWrapper{logger: metricsLogger}, From 1018aec6bcd0be36f9de4efda325fe189f28ccd3 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Thu, 13 Feb 2025 16:31:11 +0100 Subject: [PATCH 569/894] Dashboards: Fix repeats not being added on refresh when using searchLayout (#100621) Fix repeats not being added --- .../dashboard-scene/scene/PanelSearchLayout.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx index 780fab43b99..2026f777a6a 100644 --- a/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx +++ b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import classNames from 'classnames'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { SceneGridRow, VizPanel, sceneGraph } from '@grafana/scenes'; @@ -12,6 +12,7 @@ import { forceActivateFullSceneObjectTree } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; +import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent'; export interface Props { dashboard: DashboardScene; @@ -25,6 +26,7 @@ export function PanelSearchLayout({ dashboard, panelSearch = '', panelsPerRow }: const { body } = dashboard.state; const filteredPanels: VizPanel[] = []; const styles = useStyles2(getStyles); + const [_, setRepeatsUpdated] = useState(''); const bodyGrid = body instanceof DefaultGridLayoutManager ? body.state.grid : null; @@ -34,11 +36,11 @@ export function PanelSearchLayout({ dashboard, panelSearch = '', panelsPerRow }: for (const gridItem of bodyGrid.state.children) { if (gridItem instanceof DashboardGridItem) { - filterPanels(gridItem, dashboard, panelSearch, filteredPanels); + filterPanels(gridItem, dashboard, panelSearch, filteredPanels, setRepeatsUpdated); } else if (gridItem instanceof SceneGridRow) { for (const rowItem of gridItem.state.children) { if (rowItem instanceof DashboardGridItem) { - filterPanels(rowItem, dashboard, panelSearch, filteredPanels); + filterPanels(rowItem, dashboard, panelSearch, filteredPanels, setRepeatsUpdated); } } } @@ -98,7 +100,8 @@ function filterPanels( gridItem: DashboardGridItem, dashboard: DashboardScene, searchString: string, - filteredPanels: VizPanel[] + filteredPanels: VizPanel[], + setRepeatsUpdated: (updated: string) => void ) { const interpolatedSearchString = sceneGraph.interpolate(dashboard, searchString).toLowerCase(); @@ -107,6 +110,12 @@ function filterPanels( const panel = gridItem.state.body; const interpolatedTitle = panel.interpolate(panel.state.title, undefined, 'text').toLowerCase(); if (interpolatedTitle.includes(interpolatedSearchString)) { + gridItem.subscribeToEvent(DashboardRepeatsProcessedEvent, (event) => { + const source = event.payload.source; + if (source instanceof DashboardGridItem) { + setRepeatsUpdated(event.payload.source.state.key ?? ''); + } + }); gridItem.activate(); } } From 30939fd0e937f67b07f527af3d7e19da936dd43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Irene=20Rodr=C3=ADguez?= Date: Thu, 13 Feb 2025 16:44:20 +0100 Subject: [PATCH 570/894] Update relrefs (#100626) --- .../explore/correlations-editor-in-explore.md | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/sources/explore/correlations-editor-in-explore.md b/docs/sources/explore/correlations-editor-in-explore.md index 50053226158..dd0a0ce1936 100644 --- a/docs/sources/explore/correlations-editor-in-explore.md +++ b/docs/sources/explore/correlations-editor-in-explore.md @@ -3,6 +3,7 @@ labels: products: - enterprise - oss + - cloud title: Correlations Editor in Explore weight: 20 --- @@ -13,22 +14,22 @@ weight: 20 The Explore editor is available in 10.1 and later versions. In the editor, transformations is available in Grafana 10.3 and later versions. {{% /admonition %}} -Correlations allow users to build a link between any two data sources. For more information about correlations in general, please see the [correlations]({{< relref "../administration/correlations" >}}) topic in the administration page. +Correlations allow users to build a link between any two data sources. For more information about correlations in general, please see the [correlations](/docs/grafana//administration/correlations/) topic in the administration page. ## Create a correlation 1. In Grafana, navigate to the Explore page. -1. Select a data source that you would like to be [the source data source]({{< relref "../administration/correlations/correlation-configuration#source-data-source-and-result-field" >}}) for a new correlation. -1. Run a query producing data in [a supported visualization]({{< relref "../administration/correlations#correlations" >}}). -1. Click **+ Add** in the top toolbar and select **Add correlation** (you can also select **Correlations Editor** from the [Command Palette]({{< relref "../search#command-palette" >}})). +1. Select a data source that you would like to be [the source data source](/docs/grafana//administration/correlations/correlation-configuration/#source-data-source-and-result-field) for a new correlation. +1. Run a query producing data in [a supported visualization](/docs/grafana//administration/correlations/#correlations). +1. Click **+ Add** in the top toolbar and select **Add correlation** (you can also select **Correlations Editor** from the [Command Palette](/docs/grafana//search/#command-palette)). 1. Explore is now in Correlations Editor mode indicated by a blue border and top bar. You can exit Correlations Editor by clicking **Exit** in the top bar. 1. You can now create the following new correlations for the visualization with links that are attached to the data that you can use to build a new query: - Logs: links are displayed next to field values inside log details for each log row - Table: every table cell is a link 1. Click on a link to add a new correlation. - Links are associated with a field that is used as a [result field of a correlation]({{< relref "../administration/correlations/correlation-configuration" >}}). -1. In the split view that opens, use the right pane to set up [the target query source of the correlation]({{< relref "../administration/correlations/correlation-configuration#target-query" >}}). -1. Build a target query using [variables syntax]({{< relref "../dashboards/variables/variable-syntax" >}}) with variables from the list provided at the top of the pane. The list contains sample values from the selected data row. + Links are associated with a field that is used as a [result field of a correlation](/docs/grafana//administration/correlations/correlation-configuration/). +1. In the split view that opens, use the right pane to set up [the target query source of the correlation](/docs/grafana//administration/correlations/correlation-configuration/#target-query). +1. Build a target query using [variables syntax](/docs/grafana//dashboards/variables/variable-syntax/) with variables from the list provided at the top of the pane. The list contains sample values from the selected data row. 1. Provide a label and description (optional). A label will be used as the name of the link inside the visualization and can contain variables. 1. Provide transformations (optional; see below for details). @@ -37,7 +38,7 @@ Correlations allow users to build a link between any two data sources. For more ## Transformations -Transformations allow you to extract values that exist in a field with other data. For example, using a transformation, you can extract one portion of a log line to use in a correlation. For more details on transformations in correlations, see [Correlations]({{< relref "../administration/correlations/correlation-configuration/#correlation-transformations" >}}). +Transformations allow you to extract values that exist in a field with other data. For example, using a transformation, you can extract one portion of a log line to use in a correlation. For more details on transformations in correlations, see [Correlations](/docs/grafana//explore/correlations-editor-in-explore/#transformations). After clicking one of the generated links in the editor mode, you can add transformations by clicking **Add transformation** in the Transformations dropdown menu. @@ -47,7 +48,7 @@ You can use a transformation in your correlation with the following steps: Select the portion of the field that you want to use for the transformation. For example, a log line. Once selected, the value of this field will be used to assist you in building the transformation. 1. Select the type of the transformation. - See [correlations]({{< relref "../administration/correlations/correlation-configuration/#correlation-transformations" >}}) for the options and relevant settings. + See [correlations](/docs/grafana//explore/correlations-editor-in-explore/#transformations) for the options and relevant settings. 1. Based on your selection, you might see one or more variables populate, or you might need to provide more specifications in options that are displayed. 1. Select **Add transformation to correlation** to add the specified variables to the list of available variables. @@ -57,7 +58,7 @@ For regular expressions in this dialog box, the `mapValue` referred to in other ## Correlations examples -The following examples show how to create correlations using the Correlations Editor in Explore. If you'd like to follow these examples, make sure to set up a [test data source]({{< relref "../datasources/testdata#testdata-data-source" >}}). +The following examples show how to create correlations using the Correlations Editor in Explore. If you'd like to follow these examples, make sure to set up a [test data source](/docs/grafana//datasources/testdata/#testdata-data-source). ### Create a text to graph correlation @@ -65,7 +66,7 @@ This example shows how to create a correlation using Correlations Editor in Expl Correlations allow you to use results of one query to run a new query in any data source. In this example, you will run a query that renders tabular data. The data will be used to run a different query that yields a graph result. -To follow this example, make sure you have set up [a test data source]({{< relref "../datasources/testdata#testdata-data-source" >}}). +To follow this example, make sure you have set up [a test data source](/docs/grafana//datasources/testdata/#testdata-data-source). 1. In Grafana, navigate to **Explore**. 1. Select the **test data source** from the dropdown menu at the top left of the page. @@ -100,7 +101,7 @@ You can apply the same steps to any data source. Correlations allow you to creat In this example, you will create a correlation to demonstrate how to use transformations to extract values from the log line and another field. -To follow this example, make sure you have set up [a test data source]({{< relref "../datasources/testdata#testdata-data-source" >}}). +To follow this example, make sure you have set up [a test data source](/docs/grafana//datasources/testdata/#testdata-data-source). 1. In Grafana, navigate to **Explore**. 1. Select the **test data source** from the dropdown menu at the top left of the page. From aeb57f671bfacf3cf30eddc24e1b9f489c17a6b8 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Thu, 13 Feb 2025 16:53:03 +0100 Subject: [PATCH 571/894] Docs: Improve instructions to change basic roles (#100586) --- .../plan-rbac-rollout-strategy/index.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/sources/administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/index.md b/docs/sources/administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/index.md index 09c2fc4c3d4..e04c2fcecf2 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/index.md @@ -369,9 +369,11 @@ Here are two ways to achieve this: # Update the role curl -H 'Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt' -H 'Content-Type: application/json' \ - -X PUT-d @/tmp/basic_viewer.json '/api/access-control/roles/basic_viewer' + -X PUT -d @/tmp/basic_viewer.json '/api/access-control/roles/basic_viewer' ``` + The token that is used in this request is the [service account token](ref:service-accounts). + - Or use the `role > from` list and `permission > state` option of your provisioning file: ```yaml @@ -394,6 +396,20 @@ Here are two ways to achieve this: state: 'present' ``` + If your goal is to remove an access to an app you should remove it from the role and update it. For example: + + ```bash + # Fetch the role, modify it to remove permissions to kentik-connect-app and increment role version + curl -H 'Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt' \ + -X GET '/api/access-control/roles/basic_viewer' | \ + jq 'del(.created)| del(.updated) | del(.permissions[].created) | del(.permissions[].updated) | .version += 1' | \ + jq 'del(.permissions[] | select (.action == "plugins.app:access" and .scope == "plugins:id:kentik-connect-app"))' + + # Update the role + curl -H 'Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt' -H 'Content-Type: application/json' \ + -X PUT -d @/tmp/basic_viewer.json '/api/access-control/roles/basic_viewer' + ``` + ### Manage user permissions through teams In the scenario where you want users to grant access by the team they belong to, we recommend to set users role to `No Basic Role` and let the team assignment assign the role instead. From 0dab3848267f0aa29c2314f3388df0af3af306c0 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 13 Feb 2025 18:55:36 +0300 Subject: [PATCH 572/894] K8s/Frontend: Update watch support (#100631) use watch from gitsync --- public/app/features/apiserver/client.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/public/app/features/apiserver/client.ts b/public/app/features/apiserver/client.ts index 90a5004b8b0..f631aca175a 100644 --- a/public/app/features/apiserver/client.ts +++ b/public/app/features/apiserver/client.ts @@ -1,6 +1,6 @@ import { Observable, from, retry, catchError, filter, map, mergeMap } from 'rxjs'; -import { config, getBackendSrv } from '@grafana/runtime'; +import { BackendSrvRequest, config, getBackendSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { getAPINamespace } from '../../api/utils'; @@ -40,18 +40,26 @@ export class ScopedResourceClient implements return getBackendSrv().get>(`${this.url}/${name}`); } - public watch(opts?: WatchOptions): Observable> { + public watch( + params?: WatchOptions, + config?: Pick + ): Observable> { const decoder = new TextDecoder(); - const params = { - ...opts, + const { name, ...rest } = params ?? {}; // name needs to be added to fieldSelector + const requestParams = { + ...rest, watch: true, - labelSelector: this.parseListOptionsSelector(opts?.labelSelector), - fieldSelector: this.parseListOptionsSelector(opts?.fieldSelector), + labelSelector: this.parseListOptionsSelector(params?.labelSelector), + fieldSelector: this.parseListOptionsSelector(params?.fieldSelector), }; + if (name) { + requestParams.fieldSelector = `metadata.name=${name}`; + } return getBackendSrv() .chunked({ - url: params.name ? `${this.url}/${params.name}` : this.url, - params, + url: this.url, + params: requestParams, + ...config, }) .pipe( filter((response) => response.ok && response.data instanceof Uint8Array), From d719e6c621211116868735ab3df87a94dd0eecf8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 13 Feb 2025 17:02:51 +0100 Subject: [PATCH 573/894] ServiceAccounts: Fix search in SA picker (#100634) --- public/app/core/components/Select/ServiceAccountPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/components/Select/ServiceAccountPicker.tsx b/public/app/core/components/Select/ServiceAccountPicker.tsx index 28c8e3441d2..b1c0e476891 100644 --- a/public/app/core/components/Select/ServiceAccountPicker.tsx +++ b/public/app/core/components/Select/ServiceAccountPicker.tsx @@ -38,7 +38,7 @@ export class ServiceAccountPicker extends Component { } return getBackendSrv() - .get(`/api/serviceaccounts/search`) + .get(`/api/serviceaccounts/search?query=${query}&perpage=100`) .then((result: ServiceAccountsState) => { return result.serviceAccounts.map((sa) => ({ id: sa.id, From 90eb499b781ca94ed39390f93515aa543c25b08d Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 13 Feb 2025 17:17:14 +0100 Subject: [PATCH 574/894] PublicDashboards: Fetch dashboard as Grafana (#100344) --- pkg/apimachinery/identity/context.go | 2 + .../publicdashboards/service/query.go | 104 +--- .../publicdashboards/service/query_test.go | 457 +----------------- .../publicdashboards/service/service.go | 7 +- 4 files changed, 25 insertions(+), 545 deletions(-) diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 627cace5d61..81a7f81de6c 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -75,12 +75,14 @@ func getWildcardPermissions(actions ...string) map[string][]string { // serviceIdentityPermissions is a list of wildcard permissions for provided actions. // We should add every action required "internally" here. var serviceIdentityPermissions = getWildcardPermissions( + "annotations:read", "folders:read", "folders:write", "folders:create", "dashboards:read", "dashboards:write", "dashboards:create", + "datasources:query", "datasources:read", "alert.provisioning:write", "alert.provisioning.secrets:read", diff --git a/pkg/services/publicdashboards/service/query.go b/pkg/services/publicdashboards/service/query.go index 3d8731e895d..446f74ab3b6 100644 --- a/pkg/services/publicdashboards/service/query.go +++ b/pkg/services/publicdashboards/service/query.go @@ -8,16 +8,13 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/expr" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" - "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tsdb/grafanads" ) @@ -37,8 +34,8 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT return nil, models.ErrInternalServerError.Errorf("FindAnnotations: failed to unmarshal dashboard annotations: %w", err) } - anonymousUser := buildAnonymousUser(ctx, dash, pd.features) - + // We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the annotations. + svcCtx, svcIdent := identity.WithServiceIdentity(ctx, dash.OrgID) uniqueEvents := make(map[int64]models.AnnotationEvent, 0) for _, anno := range annoDto.Annotations.List { // skip annotations that are not enabled or are not a grafana datasource @@ -51,7 +48,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT OrgID: dash.OrgID, DashboardID: dash.ID, DashboardUID: dash.UID, - SignedInUser: anonymousUser, + SignedInUser: svcIdent, } if anno.Target != nil { @@ -63,7 +60,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT } } - annotationItems, err := pd.AnnotationsRepo.Find(ctx, annoQuery) + annotationItems, err := pd.AnnotationsRepo.Find(svcCtx, annoQuery) if err != nil { return nil, models.ErrInternalServerError.Errorf("FindAnnotations: failed to find annotations: %w", err) } @@ -139,8 +136,9 @@ func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context, return nil, models.ErrPanelQueriesNotFound.Errorf("GetQueryDataResponse: failed to extract queries from panel") } - anonymousUser := buildAnonymousUser(ctx, dashboard, pd.features) - res, err := pd.QueryDataService.QueryData(ctx, anonymousUser, skipDSCache, metricReq) + // We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the datasource. + svcCtx, svcIdent := identity.WithServiceIdentity(ctx, dashboard.OrgID) + res, err := pd.QueryDataService.QueryData(svcCtx, svcIdent, skipDSCache, metricReq) reqDatasources := metricReq.GetUniqueDatasourceTypes() if err != nil { @@ -180,92 +178,6 @@ func (pd *PublicDashboardServiceImpl) buildMetricRequest(dashboard *dashboards.D }, nil } -// buildAnonymousUser creates a user with permissions to read from all datasources used in the dashboard -func buildAnonymousUser(ctx context.Context, dashboard *dashboards.Dashboard, features featuremgmt.FeatureToggles) *user.SignedInUser { - datasourceUids := getUniqueDashboardDatasourceUids(dashboard.Data) - - // Create a user with blank permissions - anonymousUser := &user.SignedInUser{OrgID: dashboard.OrgID, Permissions: make(map[int64]map[string][]string)} - - // Scopes needed for Annotation queries - annotationScopes := []string{accesscontrol.ScopeAnnotationsTypeDashboard} - // Need to access all dashboards since tags annotations span across all dashboards - dashboardScopes := []string{dashboards.ScopeDashboardsProvider.GetResourceAllScope()} - - // Scopes needed for datasource queries - queryScopes := make([]string, 0) - readScopes := make([]string, 0) - for _, uid := range datasourceUids { - scope := datasources.ScopeProvider.GetResourceScopeUID(uid) - queryScopes = append(queryScopes, scope) - readScopes = append(readScopes, scope) - } - - // Apply all scopes to the actions we need the user to be able to perform - permissions := make(map[string][]string) - permissions[datasources.ActionQuery] = queryScopes - permissions[datasources.ActionRead] = readScopes - permissions[dashboards.ActionDashboardsRead] = dashboardScopes - permissions[accesscontrol.ActionAnnotationsRead] = annotationScopes - - if features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) { - permissions[accesscontrol.ActionAnnotationsRead] = dashboardScopes - } - - anonymousUser.Permissions[dashboard.OrgID] = permissions - - return anonymousUser -} - -func getUniqueDashboardDatasourceUids(dashboard *simplejson.Json) []string { - var datasourceUids []string - exists := map[string]bool{} - - // collapsed rows contain panels in a nested structure, so we need to flatten them before calculate unique uids - flattenedPanels := getFlattenedPanels(dashboard) - - for _, panelObj := range flattenedPanels { - panel := simplejson.NewFromAny(panelObj) - uid := getDataSourceUidFromJson(panel) - - // if uid is for a mixed datasource, get the datasource uids from the targets - if uid == "-- Mixed --" { - for _, targetObj := range panel.Get("targets").MustArray() { - target := simplejson.NewFromAny(targetObj) - datasourceUid := getDataSourceUidFromJson(target) - if _, ok := exists[datasourceUid]; !ok { - datasourceUids = append(datasourceUids, datasourceUid) - exists[datasourceUid] = true - } - } - } else { - if _, ok := exists[uid]; !ok { - datasourceUids = append(datasourceUids, uid) - exists[uid] = true - } - } - } - - return datasourceUids -} - -func getFlattenedPanels(dashboard *simplejson.Json) []any { - var flatPanels []any - for _, panelObj := range dashboard.Get("panels").MustArray() { - panel := simplejson.NewFromAny(panelObj) - // if the panel is a row and it is collapsed, get the queries from the panels inside the row - // if it is not collapsed, the row does not have any panels - if panel.Get("type").MustString() == "row" { - if panel.Get("collapsed").MustBool() { - flatPanels = append(flatPanels, panel.Get("panels").MustArray()...) - } - } else { - flatPanels = append(flatPanels, panelObj) - } - } - return flatPanels -} - func groupQueriesByPanelId(dashboard *simplejson.Json) map[int64][]*simplejson.Json { result := make(map[int64][]*simplejson.Json) diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go index f6aec672eac..0db1dfd08b0 100644 --- a/pkg/services/publicdashboards/service/query_test.go +++ b/pkg/services/publicdashboards/service/query_test.go @@ -11,8 +11,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/db" dashboard2 "github.com/grafana/grafana/pkg/kinds/dashboard" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -110,161 +110,6 @@ const ( "schemaVersion": 35 }` - dashboardWithMixedDatasource = ` -{ - "panels": [ - { - "datasource": { - "type": "datasource", - "uid": "-- Mixed --" - }, - "id": 1, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "abc123" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - }, - { - "datasource": "6SOeCRrVk", - "exemplar": true, - "expr": "test{id=\"f0dd9b69-ad04-4342-8e79-ced8c245683b\", name=\"test\"}", - "hide": false, - "interval": "", - "legendFormat": "", - "refId": "B" - } - ], - "title": "Panel Title", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "id": 2, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "id": 3, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "schemaVersion": 35 -}` - - dashboardWithDuplicateDatasources = ` -{ - "panels": [ - { - "datasource": { - "type": "prometheus", - "uid": "abc123" - }, - "id": 1, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "abc123" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "id": 2, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "id": 3, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "schemaVersion": 35 -}` - oldStyleDashboard = ` { "panels": [ @@ -460,218 +305,6 @@ const ( ], "schemaVersion": 35 }` - - dashboardWithCollapsedRows = ` -{ -"panels": [ - { - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 12, - "title": "Row title", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "qCbTUC37k" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 1 - }, - "id": 11, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "qCbTUC37k" - }, - "editorMode": "builder", - "expr": "access_evaluation_duration_bucket", - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 9 - }, - "id": 10, - "panels": [ - { - "datasource": { - "type": "influxdb", - "uid": "P49A45DF074423DFB" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 10 - }, - "id": 8, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "9.4.0-pre", - "targets": [ - { - "datasource": { - "type": "influxdb", - "uid": "P49A45DF074423DFB" - }, - "query": "// v.bucket, v.timeRangeStart, and v.timeRange stop are all variables supported by the flux plugin and influxdb\nfrom(bucket: v.bucket)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_value\"] >= 10 and r[\"_value\"] <= 20)", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "title": "Row title 1", - "type": "row" - } - ] -}` ) func TestGetQueryDataResponse(t *testing.T) { @@ -731,8 +364,7 @@ func TestGetQueryDataResponse(t *testing.T) { func TestFindAnnotations(t *testing.T) { color := "red" name := "annoName" - features := featuremgmt.WithFeatures(featuremgmt.FlagAnnotationPermissionUpdate) - t.Run("will build anonymous user with correct permissions to get annotations", func(t *testing.T) { + t.Run("service identity has correct permissions to get annotations dashboards and query datasources", func(t *testing.T) { fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")). Return(&PublicDashboard{Uid: "uid1", IsEnabled: true}, nil) @@ -746,11 +378,14 @@ func TestFindAnnotations(t *testing.T) { } dash := dashboards.NewDashboard("testDashboard") - items, _ := service.FindAnnotations(context.Background(), reqDTO, "abc123") - anonUser := buildAnonymousUser(context.Background(), dash, features) - - assert.Equal(t, "dashboards:*", anonUser.Permissions[0]["dashboards:read"][0]) + items, err := service.FindAnnotations(context.Background(), reqDTO, "abc123") + require.NoError(t, err) assert.Len(t, items, 0) + + _, svcIdent := identity.WithServiceIdentity(context.Background(), dash.OrgID) + require.Equal(t, "*", svcIdent.GetPermissions()["datasources:query"][0]) + require.Equal(t, "*", svcIdent.GetPermissions()["dashboards:read"][0]) + require.Equal(t, "*", svcIdent.GetPermissions()["annotations:read"][0]) }) t.Run("Test events from tag queries overwrite built-in annotation queries and duplicate events are not returned", func(t *testing.T) { @@ -1121,47 +756,6 @@ func TestGetMetricRequest(t *testing.T) { }) } -func TestGetUniqueDashboardDatasourceUids(t *testing.T) { - t.Run("can get unique datasource ids from dashboard", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithDuplicateDatasources)) - require.NoError(t, err) - - uids := getUniqueDashboardDatasourceUids(json) - require.Len(t, uids, 2) - require.Equal(t, "abc123", uids[0]) - require.Equal(t, "_yxMP8Ynk", uids[1]) - }) - - t.Run("can get unique datasource ids from dashboard with a mixed datasource", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithMixedDatasource)) - require.NoError(t, err) - - uids := getUniqueDashboardDatasourceUids(json) - require.Len(t, uids, 3) - require.Equal(t, "abc123", uids[0]) - require.Equal(t, "6SOeCRrVk", uids[1]) - require.Equal(t, "_yxMP8Ynk", uids[2]) - }) - - t.Run("can get no datasource uids from empty dashboard", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(`{"panels": {}}`)) - require.NoError(t, err) - - uids := getUniqueDashboardDatasourceUids(json) - require.Len(t, uids, 0) - }) - - t.Run("can get unique datasource ids from dashboard with rows", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithCollapsedRows)) - require.NoError(t, err) - - uids := getUniqueDashboardDatasourceUids(json) - require.Len(t, uids, 2) - require.Equal(t, "qCbTUC37k", uids[0]) - require.Equal(t, "P49A45DF074423DFB", uids[1]) - }) -} - func TestBuildMetricRequest(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) @@ -1318,39 +912,6 @@ func TestBuildMetricRequest(t *testing.T) { }) } -func TestBuildAnonymousUser(t *testing.T) { - sqlStore, cfg := db.InitTestDBWithCfg(t) - dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) - features := featuremgmt.WithFeatures() - - t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) { - user := buildAnonymousUser(context.Background(), dashboard, features) - - require.Equal(t, dashboard.OrgID, user.OrgID) - require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:query"][0]) - require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:query"][1]) - require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:read"][0]) - require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:read"][1]) - }) - t.Run("will add dashboard and annotation permissions needed for getting annotations", func(t *testing.T) { - user := buildAnonymousUser(context.Background(), dashboard, features) - - require.Equal(t, dashboard.OrgID, user.OrgID) - require.Equal(t, "annotations:type:dashboard", user.Permissions[user.OrgID]["annotations:read"][0]) - require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["dashboards:read"][0]) - }) - t.Run("will add dashboard and annotation permissions needed for getting annotations when FlagAnnotationPermissionUpdate is enabled", func(t *testing.T) { - features = featuremgmt.WithFeatures(featuremgmt.FlagAnnotationPermissionUpdate) - user := buildAnonymousUser(context.Background(), dashboard, features) - - require.Equal(t, dashboard.OrgID, user.OrgID) - require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["annotations:read"][0]) - require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["dashboards:read"][0]) - }) -} - func TestGroupQueriesByPanelId(t *testing.T) { t.Run("can extract queries from dashboard with panel datasource string that has no datasource on panel targets", func(t *testing.T) { json, err := simplejson.NewJson([]byte(oldStyleDashboard)) diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index d3239e2857d..9223981518c 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -13,6 +13,7 @@ import ( "go.opentelemetry.io/otel" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -136,7 +137,11 @@ func (pd *PublicDashboardServiceImpl) Find(ctx context.Context, uid string) (*Pu func (pd *PublicDashboardServiceImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { ctx, span := tracer.Start(ctx, "publicdashboards.FindDashboard") defer span.End() - dash, err := pd.dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{UID: dashboardUid, OrgID: orgId}) + + // We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the dashboard. + dash, err := identity.WithServiceIdentityFn(ctx, orgId, func(ctx context.Context) (*dashboards.Dashboard, error) { + return pd.dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{UID: dashboardUid, OrgID: orgId}) + }) if err != nil { var dashboardErr dashboards.DashboardErr if ok := errors.As(err, &dashboardErr); ok { From 6e4c1a57c19c12d633fa1d72f19af30aa43db66d Mon Sep 17 00:00:00 2001 From: Johnny Kartheiser <140559259+JohnnyK-Grafana@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:29:04 -0600 Subject: [PATCH 575/894] docs: capitalization issues (#100562) fixing two capitalization issues for product names. --- .../configure-notifications/manage-contact-points/_index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md index 160d76360dc..215ed1926dc 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md @@ -140,13 +140,13 @@ Each contact point integration has its own configuration options and setup proce - [Discord](ref:discord) - [Email](ref:email) - [Google Chat](ref:gchat) -- [Grafana Oncall](ref:oncall) +- [Grafana OnCall](ref:oncall) - Kafka REST Proxy - Line - [Microsoft Teams](ref:teams) - [MQTT](ref:mqtt) - [Opsgenie](ref:opsgenie) -- [Pagerduty](ref:pagerduty) +- [PagerDuty](ref:pagerduty) - Pushover - Sensu Go - [Slack](ref:slack) From 155492c8a5858330aba5f8a6a5168ebd5ec4cc94 Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:35:53 -0300 Subject: [PATCH 576/894] search: handle "permission" query param in search (#100607) handle "permission" query param in search --- .../dashboard/legacysearcher/search_client.go | 19 +++++-------------- .../dashboards/service/dashboard_service.go | 4 ++++ pkg/storage/unified/resource/resource.pb.go | 16 +++++++++++++--- pkg/storage/unified/resource/resource.proto | 2 ++ pkg/storage/unified/search/bleve.go | 8 +++++++- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index bd2e4a896a8..c0e18288ea5 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/apis/dashboard" folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -40,9 +41,6 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour req.Query = strings.ReplaceAll(req.Query, "*", "") } - // TODO add missing support for the following query params: - // - folderIds (won't support, must use folderUIDs) - // - permission query := &dashboards.FindPersistedDashboardsQuery{ Title: req.Query, Limit: req.Limit, @@ -51,6 +49,10 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour IsDeleted: req.IsDeleted, } + if req.Permission == int64(dashboardaccess.PERMISSION_EDIT) { + query.Permission = dashboardaccess.PERMISSION_EDIT + } + var queryType string if req.Options.Key.Resource == dashboard.DASHBOARD_RESOURCE { queryType = searchstore.TypeDashboard @@ -123,22 +125,11 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour } } - // TODO need to test this - // emptyResponse, err := a.dashService.GetSharedDashboardUIDsQuery(ctx, query) - - // if err != nil { - // return nil, err - // } else if emptyResponse { - // return nil, nil - // } - res, err := c.dashboardStore.FindDashboards(ctx, query) if err != nil { return nil, err } - // TODO sort if query.Sort == "" see sortedHits in services/search/service.go - searchFields := resource.StandardSearchFields() list := &resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 3964922fcda..ed8a8fa1d19 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1755,6 +1755,10 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex request.IsDeleted = query.IsDeleted } + if query.Permission > 0 { + request.Permission = int64(query.Permission) + } + if query.Limit < 1 { query.Limit = 1000 } diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index ee8801f361b..dc6593d385f 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -2016,6 +2016,7 @@ type ResourceSearchRequest struct { Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"` IsDeleted bool `protobuf:"varint,10,opt,name=is_deleted,json=isDeleted,proto3" json:"is_deleted,omitempty"` Page int64 `protobuf:"varint,11,opt,name=page,proto3" json:"page,omitempty"` + Permission int64 `protobuf:"varint,12,opt,name=permission,proto3" json:"permission,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2127,6 +2128,13 @@ func (x *ResourceSearchRequest) GetPage() int64 { return 0 } +func (x *ResourceSearchRequest) GetPermission() int64 { + if x != nil { + return x.Permission + } + return 0 +} + type ResourceSearchResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Error details @@ -4238,8 +4246,8 @@ var file_resource_proto_rawDesc = string([]byte{ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xee, - 0x04, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x8e, + 0x05, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, @@ -4265,7 +4273,9 @@ var file_resource_proto_rawDesc = string([]byte{ 0x6c, 0x61, 0x69, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, + 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x70, 0x65, 0x72, + 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46, 0x61, 0x63, diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index f16f194d360..c11ed29f259 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -457,6 +457,8 @@ message ResourceSearchRequest { bool is_deleted = 10; int64 page = 11; + + int64 permission = 12; } message ResourceSearchResponse { diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index bf9748cf5bc..2b76bb97529 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -18,6 +18,7 @@ import ( "github.com/blevesearch/bleve/v2/search/query" bleveSearch "github.com/blevesearch/bleve/v2/search/searcher" index "github.com/blevesearch/bleve_index_api" + "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/featuremgmt" "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/selection" @@ -611,11 +612,16 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resource.Res if !ok { return nil, resource.AsErrorResult(fmt.Errorf("missing auth info")) } + verb := utils.VerbList + if req.Permission == int64(dashboardaccess.PERMISSION_EDIT) { + verb = utils.VerbPatch + } + checker, err := access.Compile(ctx, auth, authlib.ListRequest{ Namespace: b.key.Namespace, Group: b.key.Group, Resource: b.key.Resource, - Verb: utils.VerbList, + Verb: verb, }) if err != nil { return nil, resource.AsErrorResult(err) From 2bdeb727cfa56859e94e0474d7b19b923956eaee Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 13 Feb 2025 16:36:16 +0000 Subject: [PATCH 577/894] Chore: Bump react-router to v5.3.4 (#100500) --- package.json | 4 ++-- packages/grafana-ui/package.json | 2 +- yarn.lock | 40 +++++++++++--------------------- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 4d638dc7b9b..150751ba6e4 100644 --- a/package.json +++ b/package.json @@ -380,8 +380,8 @@ "react-redux": "9.2.0", "react-resizable": "3.0.5", "react-responsive-carousel": "^3.2.23", - "react-router": "5.3.3", - "react-router-dom": "5.3.3", + "react-router": "5.3.4", + "react-router-dom": "5.3.4", "react-router-dom-v5-compat": "^6.26.1", "react-select": "5.10.0", "react-split-pane": "0.1.92", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index cee844a1b5d..eff1830e872 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -97,7 +97,7 @@ "react-i18next": "^15.0.0", "react-inlinesvg": "4.1.5", "react-loading-skeleton": "3.5.0", - "react-router-dom": "5.3.3", + "react-router-dom": "5.3.4", "react-router-dom-v5-compat": "^6.26.1", "react-select": "5.10.0", "react-table": "7.8.0", diff --git a/yarn.lock b/yarn.lock index f818433fbda..f1092de32f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4124,7 +4124,7 @@ __metadata: react-i18next: "npm:^15.0.0" react-inlinesvg: "npm:4.1.5" react-loading-skeleton: "npm:3.5.0" - react-router-dom: "npm:5.3.3" + react-router-dom: "npm:5.3.4" react-router-dom-v5-compat: "npm:^6.26.1" react-select: "npm:5.10.0" react-select-event: "npm:^5.1.0" @@ -18399,8 +18399,8 @@ __metadata: react-refresh: "npm:0.14.0" react-resizable: "npm:3.0.5" react-responsive-carousel: "npm:^3.2.23" - react-router: "npm:5.3.3" - react-router-dom: "npm:5.3.3" + react-router: "npm:5.3.4" + react-router-dom: "npm:5.3.4" react-router-dom-v5-compat: "npm:^6.26.1" react-select: "npm:5.10.0" react-select-event: "npm:5.5.1" @@ -22463,19 +22463,6 @@ __metadata: languageName: node linkType: hard -"mini-create-react-context@npm:^0.4.0": - version: 0.4.1 - resolution: "mini-create-react-context@npm:0.4.1" - dependencies: - "@babel/runtime": "npm:^7.12.1" - tiny-warning: "npm:^1.0.3" - peerDependencies: - prop-types: ^15.0.0 - react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/c816c785b7dccd67fdfa6a5edc673363b11845b6abca8a9d9f3ffa74520266d979b56f5db0dfc62ed912a90553c15be28c816311fc9c7856ab66a81d461d50e6 - languageName: node - linkType: hard - "mini-css-extract-plugin@npm:2.9.2": version: 2.9.2 resolution: "mini-css-extract-plugin@npm:2.9.2" @@ -26768,20 +26755,20 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:5.3.3": - version: 5.3.3 - resolution: "react-router-dom@npm:5.3.3" +"react-router-dom@npm:5.3.4": + version: 5.3.4 + resolution: "react-router-dom@npm:5.3.4" dependencies: "@babel/runtime": "npm:^7.12.13" history: "npm:^4.9.0" loose-envify: "npm:^1.3.1" prop-types: "npm:^15.6.2" - react-router: "npm:5.3.3" + react-router: "npm:5.3.4" tiny-invariant: "npm:^1.0.2" tiny-warning: "npm:^1.0.0" peerDependencies: react: ">=15" - checksum: 10/49552596f1a4c753b99324a5f4345b3ee91fbb780aa65851a7113f053044ef96c083d2ded12937e593b23a0fcdf58b9e49780df6bf6e27d9eeb348b3c85ae611 + checksum: 10/5e0696ae2d86f466ff700944758a227e1dcd79b48797d567776506e4e3b4a08b81336155feb86a33be9f38c17c4d3d94212b5c60c8ee9a086022e4fd3961db29 languageName: node linkType: hard @@ -26798,15 +26785,14 @@ __metadata: languageName: node linkType: hard -"react-router@npm:5.3.3": - version: 5.3.3 - resolution: "react-router@npm:5.3.3" +"react-router@npm:5.3.4": + version: 5.3.4 + resolution: "react-router@npm:5.3.4" dependencies: "@babel/runtime": "npm:^7.12.13" history: "npm:^4.9.0" hoist-non-react-statics: "npm:^3.1.0" loose-envify: "npm:^1.3.1" - mini-create-react-context: "npm:^0.4.0" path-to-regexp: "npm:^1.7.0" prop-types: "npm:^15.6.2" react-is: "npm:^16.6.0" @@ -26814,7 +26800,7 @@ __metadata: tiny-warning: "npm:^1.0.0" peerDependencies: react: ">=15" - checksum: 10/4631eed91020c73950804c7c7454e74b2eb495f803c5ca60c8b5572ca72cc06e336f3b08d9ee3fa730128a52c4d9e16d1aa7e8b7f85560629117e16d99a01cef + checksum: 10/99d54a99af6bc6d7cad2e5ea7eee9485b62a8b8e16a1182b18daa7fad7dafa5e526850eaeebff629848b297ae055a9cb5b4aba8760e81af8b903efc049d48f5c languageName: node linkType: hard @@ -30309,7 +30295,7 @@ __metadata: languageName: node linkType: hard -"tiny-warning@npm:^1.0.0, tiny-warning@npm:^1.0.3": +"tiny-warning@npm:^1.0.0": version: 1.0.3 resolution: "tiny-warning@npm:1.0.3" checksum: 10/da62c4acac565902f0624b123eed6dd3509bc9a8d30c06e017104bedcf5d35810da8ff72864400ad19c5c7806fc0a8323c68baf3e326af7cb7d969f846100d71 From b58b5b5768fc34a81d8e2bf4d23150ae3f287f8a Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Thu, 13 Feb 2025 17:39:33 +0100 Subject: [PATCH 578/894] grpc: improve grpc logger (#100606) use proper grpc logging --- .../grpcserver/interceptors/logging.go | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/pkg/services/grpcserver/interceptors/logging.go b/pkg/services/grpcserver/interceptors/logging.go index 2a3997a7024..db4f017d1e4 100644 --- a/pkg/services/grpcserver/interceptors/logging.go +++ b/pkg/services/grpcserver/interceptors/logging.go @@ -2,27 +2,34 @@ package interceptors import ( "context" + "fmt" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" "google.golang.org/grpc" ) -func LoggingUnaryInterceptor(logger log.Logger, enabled bool) grpc.UnaryServerInterceptor { - return func( - ctx context.Context, - req any, - info *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, - ) (resp any, err error) { - resp, err = handler(ctx, req) - if enabled { - ctxLogger := logger.FromContext(ctx) - if err != nil { - ctxLogger.Error("gRPC call", "method", info.FullMethod, "req", req, "err", err) - } else { - ctxLogger.Info("gRPC call", "method", info.FullMethod, "req", req, "resp", resp) - } +func InterceptorLogger(l log.Logger, enabled bool) logging.Logger { + return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) { + if !enabled { + return } - return resp, err - } + l := l.FromContext(ctx) + switch lvl { + case logging.LevelDebug: + l.Debug(msg, fields...) + case logging.LevelInfo: + l.Info(msg, fields...) + case logging.LevelWarn: + l.Warn(msg, fields...) + case logging.LevelError: + l.Error(msg, fields...) + default: + panic(fmt.Sprintf("unknown level %v", lvl)) + } + }) +} + +func LoggingUnaryInterceptor(logger log.Logger, enabled bool) grpc.UnaryServerInterceptor { + return logging.UnaryServerInterceptor(InterceptorLogger(logger, enabled)) } From 5315b4fd2df445584bec3748d49c1e7227ad0e47 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 13 Feb 2025 18:41:09 +0200 Subject: [PATCH 579/894] Dashboard: Fix repeats behavior for inspect, solo panel and repeated and empty panels (#100605) --- e2e/old-arch/various-suite/solo-route.spec.ts | 4 +-- e2e/various-suite/solo-route.spec.ts | 4 +-- package.json | 4 +-- .../scene/DashboardSceneUrlSync.ts | 32 ++++++++++++++++--- .../DefaultGridLayoutManager.tsx | 8 +++++ .../dashboard-scene/utils/clone.test.ts | 2 ++ .../features/dashboard-scene/utils/utils.ts | 13 ++++++-- yarn.lock | 22 ++++++------- 8 files changed, 66 insertions(+), 23 deletions(-) diff --git a/e2e/old-arch/various-suite/solo-route.spec.ts b/e2e/old-arch/various-suite/solo-route.spec.ts index 9717baf41bd..415257ead7c 100644 --- a/e2e/old-arch/various-suite/solo-route.spec.ts +++ b/e2e/old-arch/various-suite/solo-route.spec.ts @@ -25,7 +25,7 @@ describe('Solo Route', () => { it('Can view solo repeated panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( - 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-16-clone-0/grid-item-2/panel-2-clone-0&__feature.dashboardSceneSolo=true' + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' ); e2e.components.Panels.Panel.title('server=A').should('exist'); @@ -38,7 +38,7 @@ describe('Solo Route', () => { 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true' ); - e2e.components.Panels.Panel.title('server = A, pod = Rob').should('exist'); + e2e.components.Panels.Panel.title('server = B, pod = Rob').should('exist'); cy.contains('uplot-main-div').should('not.exist'); }); }); diff --git a/e2e/various-suite/solo-route.spec.ts b/e2e/various-suite/solo-route.spec.ts index 9717baf41bd..415257ead7c 100644 --- a/e2e/various-suite/solo-route.spec.ts +++ b/e2e/various-suite/solo-route.spec.ts @@ -25,7 +25,7 @@ describe('Solo Route', () => { it('Can view solo repeated panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( - 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-16-clone-0/grid-item-2/panel-2-clone-0&__feature.dashboardSceneSolo=true' + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' ); e2e.components.Panels.Panel.title('server=A').should('exist'); @@ -38,7 +38,7 @@ describe('Solo Route', () => { 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true' ); - e2e.components.Panels.Panel.title('server = A, pod = Rob').should('exist'); + e2e.components.Panels.Panel.title('server = B, pod = Rob').should('exist'); cy.contains('uplot-main-div').should('not.exist'); }); }); diff --git a/package.json b/package.json index 150751ba6e4..7e889ddedde 100644 --- a/package.json +++ b/package.json @@ -275,8 +275,8 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "6.0.1", - "@grafana/scenes-react": "6.0.1", + "@grafana/scenes": "6.0.2", + "@grafana/scenes-react": "6.0.2", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 3493be625f9..cde4c8df0e0 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -22,7 +22,8 @@ import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutMana import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent'; export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { - private _eventSub?: Unsubscribable; + private _viewEventSub?: Unsubscribable; + private _inspectEventSub?: Unsubscribable; constructor(private _scene: DashboardScene) {} @@ -78,6 +79,14 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { if (typeof values.inspect === 'string') { let panel = findVizPanelByKey(this._scene, values.inspect); if (!panel) { + // If we are trying to view a repeat clone that can't be found it might be that the repeats have not been processed yet + // Here we check if the key contains the clone key so we force the repeat processing + // It doesn't matter if the element or the ancestors are clones or not, just that the key contains the clone key + if (containsCloneKey(values.inspect)) { + this._handleInspectRepeatClone(values.inspect); + return; + } + appEvents.emit(AppEvents.alertError, ['Panel not found']); locationService.partial({ inspect: null }); return; @@ -177,12 +186,27 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { } } + private _handleInspectRepeatClone(inspect: string) { + if (!this._inspectEventSub) { + this._inspectEventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { + const panel = findVizPanelByKey(this._scene, inspect); + if (panel) { + this._inspectEventSub?.unsubscribe(); + this._scene.setState({ + inspectPanelKey: inspect, + overlay: new PanelInspectDrawer({ panelRef: panel.getRef() }), + }); + } + }); + } + } + private _handleViewRepeatClone(viewPanel: string) { - if (!this._eventSub) { - this._eventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { + if (!this._viewEventSub) { + this._viewEventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { const panel = findVizPanelByKey(this._scene, viewPanel); if (panel) { - this._eventSub?.unsubscribe(); + this._viewEventSub?.unsubscribe(); this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: panel.getRef() }) }); } }); diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index 97b268757d9..bad747f55de 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -252,6 +252,14 @@ export class DefaultGridLayoutManager } public activateRepeaters() { + if (!this.isActive) { + this.activate(); + } + + if (!this.state.grid.isActive) { + this.state.grid.activate(); + } + this.state.grid.forEachChild((child) => { if (child instanceof DashboardGridItem && !child.isActive) { child.activate(); diff --git a/public/app/features/dashboard-scene/utils/clone.test.ts b/public/app/features/dashboard-scene/utils/clone.test.ts index 58dcef6fb38..97b0e377e6a 100644 --- a/public/app/features/dashboard-scene/utils/clone.test.ts +++ b/public/app/features/dashboard-scene/utils/clone.test.ts @@ -30,6 +30,8 @@ describe('clone', () => { expect(getOriginalKey('panel-clone-1')).toBe('panel'); expect(getOriginalKey('row-clone-1/panel-clone-2')).toBe('panel'); expect(getOriginalKey('tab-clone-0/row-clone-1/panel-clone-2')).toBe('panel'); + expect(getOriginalKey('panel-2-clone-3')).toBe('panel-2'); + expect(getOriginalKey('panel-2')).toBe('panel-2'); }); }); diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 27ecaac2fe2..5e52a1d517f 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -21,7 +21,7 @@ import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; -import { getLastKeyFromClone, getOriginalKey } from './clone'; +import { getOriginalKey, isClonedKey } from './clone'; export const NEW_PANEL_HEIGHT = 8; export const NEW_PANEL_WIDTH = 12; @@ -64,7 +64,16 @@ function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizP const panel = sceneGraph.findObject(scene, (obj) => { const objKey = obj.state.key!; - if (objKey === key || getLastKeyFromClone(objKey) === getLastKeyFromClone(key) || getOriginalKey(objKey) === key) { + if (objKey === key) { + return true; + } + + // It might be possible to have the keys changed in the meantime from `panel-2` to `panel-2-clone-0` + // We need to check this as well + const originalObjectKey = !isClonedKey(objKey) ? getOriginalKey(objKey) : objKey; + const originalKey = !isClonedKey(key) ? getOriginalKey(key) : key; + + if (originalObjectKey === originalKey) { return true; } diff --git a/yarn.lock b/yarn.lock index f1092de32f7..49162fcfe61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3814,11 +3814,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:6.0.1": - version: 6.0.1 - resolution: "@grafana/scenes-react@npm:6.0.1" +"@grafana/scenes-react@npm:6.0.2": + version: 6.0.2 + resolution: "@grafana/scenes-react@npm:6.0.2" dependencies: - "@grafana/scenes": "npm:6.0.1" + "@grafana/scenes": "npm:6.0.2" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3830,13 +3830,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/e4ad83cc628f17232fe9c8d74f641c65e2e289c177ce88a6990d00f6bea4e1a091115e7b98200de7bcff14ace0fe20eb816141fe533fee7d2ad5f7f665404d2c + checksum: 10/9744e01f2ff912229e43cedfa41d626ccdfd034f5b9718b57c593bc90edadade960f76baf1d8ad19eed03709c17c62397df1871b89acc635172aa14f6a20e096 languageName: node linkType: hard -"@grafana/scenes@npm:6.0.1": - version: 6.0.1 - resolution: "@grafana/scenes@npm:6.0.1" +"@grafana/scenes@npm:6.0.2": + version: 6.0.2 + resolution: "@grafana/scenes@npm:6.0.2" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3854,7 +3854,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/6862e57358ba2e63f139e7f3bb977b19945f67eb070aa2c85c073a55dc460d3ccfeecfee22aea92c660a7632ac997e6cd945f9466b64103436a221979e6e8fcb + checksum: 10/2584f296db6299ef0a09d51f5c267ebcf7e44bd17b4d6516e38d3220f8f1d7aebc63c5fc6523979c4ac4d3f555416ca573e85e03bd36eb33a11941a5b3497149 languageName: node linkType: hard @@ -18151,8 +18151,8 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:6.0.1" - "@grafana/scenes-react": "npm:6.0.1" + "@grafana/scenes": "npm:6.0.2" + "@grafana/scenes-react": "npm:6.0.2" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0" From 19777ba3e99bb40e7db1b5c9f92021d85deefac8 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:49:21 +0100 Subject: [PATCH 580/894] Skip flaky test that's breaking the CI pipelines (#100640) --- pkg/tests/alertmanager/alertmanager_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/tests/alertmanager/alertmanager_test.go b/pkg/tests/alertmanager/alertmanager_test.go index 0f127ea2ab6..269e53075e2 100644 --- a/pkg/tests/alertmanager/alertmanager_test.go +++ b/pkg/tests/alertmanager/alertmanager_test.go @@ -13,6 +13,7 @@ func TestAlertmanagerIntegration_ExtraDedupStage(t *testing.T) { } t.Run("assert no flapping alerts when stopOnExtraDedup is enabled", func(t *testing.T) { + t.Skip("skipping flaky test") s, err := NewAlertmanagerScenario() require.NoError(t, err) defer s.Close() From eeadb7e771f18fa62dc6b5d0cb757666cde43aeb Mon Sep 17 00:00:00 2001 From: xavi <114113189+volcanonoodle@users.noreply.github.com> Date: Thu, 13 Feb 2025 18:02:54 +0100 Subject: [PATCH 581/894] IAM: Log error when malformed json arrays are found in SSO configs (#99896) --- pkg/login/social/connectors/azuread_oauth.go | 20 +++++++++--- pkg/login/social/connectors/common.go | 25 ++++++++++++++- pkg/login/social/connectors/generic_oauth.go | 31 ++++++++++++++++--- pkg/login/social/connectors/github_oauth.go | 30 ++++++++++++++---- pkg/login/social/connectors/gitlab_oauth.go | 2 +- pkg/login/social/connectors/google_oauth.go | 2 +- .../social/connectors/grafana_com_oauth.go | 20 +++++++++--- pkg/login/social/connectors/okta_oauth.go | 2 +- pkg/login/social/socialimpl/service.go | 4 +-- pkg/util/strings.go | 17 +++++++--- public/app/features/auth-config/utils/data.ts | 5 ++- 11 files changed, 128 insertions(+), 30 deletions(-) diff --git a/pkg/login/social/connectors/azuread_oauth.go b/pkg/login/social/connectors/azuread_oauth.go index 0f4a72ed5a7..8ae1f3380ab 100644 --- a/pkg/login/social/connectors/azuread_oauth.go +++ b/pkg/login/social/connectors/azuread_oauth.go @@ -88,10 +88,17 @@ type keySetJWKS struct { } func NewAzureADProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles, cache remotecache.CacheStorage) *SocialAzureAD { + s := newSocialBase(social.AzureADProviderName, orgRoleMapper, info, features, cfg) + + allowedOrganizations, err := util.SplitStringWithError(info.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.AzureADProviderName, "error", err) + } + provider := &SocialAzureAD{ - SocialBase: newSocialBase(social.AzureADProviderName, orgRoleMapper, info, features, cfg), + SocialBase: s, cache: cache, - allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]), + allowedOrganizations: allowedOrganizations, forceUseGraphAPI: MustBool(info.Extra[forceUseGraphAPIKey], ExtraAzureADSettingKeys[forceUseGraphAPIKey].DefaultValue.(bool)), } @@ -236,7 +243,7 @@ func (s *SocialAzureAD) managedIdentityCallback(ctx context.Context) (string, er } func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.AzureADProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } @@ -250,7 +257,12 @@ func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettin appendUniqueScope(s.Config, social.OfflineAccessScope) } - s.allowedOrganizations = util.SplitString(newInfo.Extra[allowedOrganizationsKey]) + allowedOrganizations, err := util.SplitStringWithError(newInfo.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.AzureADProviderName, "error", err) + } + + s.allowedOrganizations = allowedOrganizations s.forceUseGraphAPI = MustBool(newInfo.Extra[forceUseGraphAPIKey], false) return nil diff --git a/pkg/login/social/connectors/common.go b/pkg/login/social/connectors/common.go index 255b96742e8..15fc590f359 100644 --- a/pkg/login/social/connectors/common.go +++ b/pkg/login/social/connectors/common.go @@ -2,6 +2,7 @@ package connectors import ( "context" + "errors" "fmt" "io" "net/http" @@ -13,6 +14,7 @@ import ( "github.com/mitchellh/mapstructure" "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -165,9 +167,25 @@ func MustBool(value any, defaultValue bool) bool { return result } +// CreateOAuthInfoFromKeyValuesWithLogging creates an OAuthInfo struct from a map[string]any using mapstructure +// it puts all extra key values into OAuthInfo's Extra map. +// It logs as errors any parsing errors that are not critical +func CreateOAuthInfoFromKeyValuesWithLogging(l log.Logger, provider string, settingsKV map[string]any) (*social.OAuthInfo, error) { + parsingWarns := []error{} + info, err := createOAuthInfoFromKeyValues(settingsKV, &parsingWarns) + if len(parsingWarns) > 0 { + l.Error("Invalid auth configuration setting", "error", errors.Join(parsingWarns...), "provider", provider) + } + return info, err +} + // CreateOAuthInfoFromKeyValues creates an OAuthInfo struct from a map[string]any using mapstructure // it puts all extra key values into OAuthInfo's Extra map func CreateOAuthInfoFromKeyValues(settingsKV map[string]any) (*social.OAuthInfo, error) { + return createOAuthInfoFromKeyValues(settingsKV, nil) +} + +func createOAuthInfoFromKeyValues(settingsKV map[string]any, parsingWarns *[]error) (*social.OAuthInfo, error) { emptyStrToSliceDecodeHook := func(from reflect.Type, to reflect.Type, data any) (any, error) { if from.Kind() == reflect.String && to.Kind() == reflect.Slice { strData, ok := data.(string) @@ -178,7 +196,12 @@ func CreateOAuthInfoFromKeyValues(settingsKV map[string]any) (*social.OAuthInfo, if strData == "" { return []string{}, nil } - return util.SplitString(strData), nil + + splitStr, err := util.SplitStringWithError(strData) + if err != nil && parsingWarns != nil { + *parsingWarns = append(*parsingWarns, err) + } + return splitStr, nil } return data, nil } diff --git a/pkg/login/social/connectors/generic_oauth.go b/pkg/login/social/connectors/generic_oauth.go index eb4a32f8381..15989c0df93 100644 --- a/pkg/login/social/connectors/generic_oauth.go +++ b/pkg/login/social/connectors/generic_oauth.go @@ -53,6 +53,18 @@ type SocialGenericOAuth struct { } func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGenericOAuth { + s := newSocialBase(social.GenericOAuthProviderName, orgRoleMapper, info, features, cfg) + + teamIds, err := util.SplitStringWithError(info.Extra[teamIdsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", teamIdsKey, "provider", social.GenericOAuthProviderName, "error", err) + } + + allowedOrganizations, err := util.SplitStringWithError(info.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GenericOAuthProviderName, "error", err) + } + provider := &SocialGenericOAuth{ SocialBase: newSocialBase(social.GenericOAuthProviderName, orgRoleMapper, info, features, cfg), teamsUrl: info.TeamsUrl, @@ -63,8 +75,8 @@ func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMa loginAttributePath: info.Extra[loginAttributePathKey], idTokenAttributeName: info.Extra[idTokenAttributeNameKey], teamIdsAttributePath: info.TeamIdsAttributePath, - teamIds: util.SplitString(info.Extra[teamIdsKey]), - allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]), + teamIds: teamIds, + allowedOrganizations: allowedOrganizations, } if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) { @@ -118,7 +130,7 @@ func validateTeamsUrlWhenNotEmpty(info *social.OAuthInfo, requester identity.Req } func (s *SocialGenericOAuth) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.GenericOAuthProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } @@ -128,6 +140,15 @@ func (s *SocialGenericOAuth) Reload(ctx context.Context, settings ssoModels.SSOS s.updateInfo(ctx, social.GenericOAuthProviderName, newInfo) + teamIds, err := util.SplitStringWithError(newInfo.Extra[teamIdsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", teamIdsKey, "provider", social.GenericOAuthProviderName, "error", err) + } + allowedOrganizations, err := util.SplitStringWithError(newInfo.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GenericOAuthProviderName, "error", err) + } + s.teamsUrl = newInfo.TeamsUrl s.emailAttributeName = newInfo.EmailAttributeName s.emailAttributePath = newInfo.EmailAttributePath @@ -136,8 +157,8 @@ func (s *SocialGenericOAuth) Reload(ctx context.Context, settings ssoModels.SSOS s.loginAttributePath = newInfo.Extra[loginAttributePathKey] s.idTokenAttributeName = newInfo.Extra[idTokenAttributeNameKey] s.teamIdsAttributePath = newInfo.TeamIdsAttributePath - s.teamIds = util.SplitString(newInfo.Extra[teamIdsKey]) - s.allowedOrganizations = util.SplitString(newInfo.Extra[allowedOrganizationsKey]) + s.teamIds = teamIds + s.allowedOrganizations = allowedOrganizations return nil } diff --git a/pkg/login/social/connectors/github_oauth.go b/pkg/login/social/connectors/github_oauth.go index 124b642f822..f5f0b43b3f3 100644 --- a/pkg/login/social/connectors/github_oauth.go +++ b/pkg/login/social/connectors/github_oauth.go @@ -62,13 +62,23 @@ var ( ) func NewGitHubProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGithub { - teamIdsSplitted := util.SplitString(info.Extra[teamIdsKey]) + s := newSocialBase(social.GitHubProviderName, orgRoleMapper, info, features, cfg) + + teamIdsSplitted, err := util.SplitStringWithError(info.Extra[teamIdsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", teamIdsKey, "provider", social.GitHubProviderName, "error", err) + } teamIds := mustInts(teamIdsSplitted) + allowedOrganizations, err := util.SplitStringWithError(info.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GitHubProviderName, "error", err) + } + provider := &SocialGithub{ - SocialBase: newSocialBase(social.GitHubProviderName, orgRoleMapper, info, features, cfg), + SocialBase: s, teamIds: teamIds, - allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]), + allowedOrganizations: allowedOrganizations, } if len(teamIdsSplitted) != len(teamIds) { @@ -117,14 +127,22 @@ func teamIdsNumbersValidator(info *social.OAuthInfo, requester identity.Requeste } func (s *SocialGithub) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.GitHubProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } - teamIdsSplitted := util.SplitString(newInfo.Extra[teamIdsKey]) + teamIdsSplitted, err := util.SplitStringWithError(newInfo.Extra[teamIdsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", teamIdsKey, "provider", social.GitHubProviderName, "error", err) + } teamIds := mustInts(teamIdsSplitted) + allowedOrganizations, err := util.SplitStringWithError(newInfo.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GitHubProviderName, "error", err) + } + if len(teamIdsSplitted) != len(teamIds) { s.log.Warn("Failed to parse team ids. Team ids must be a list of numbers.", "teamIds", teamIdsSplitted) } @@ -135,7 +153,7 @@ func (s *SocialGithub) Reload(ctx context.Context, settings ssoModels.SSOSetting s.updateInfo(ctx, social.GitHubProviderName, newInfo) s.teamIds = teamIds - s.allowedOrganizations = util.SplitString(newInfo.Extra[allowedOrganizationsKey]) + s.allowedOrganizations = allowedOrganizations return nil } diff --git a/pkg/login/social/connectors/gitlab_oauth.go b/pkg/login/social/connectors/gitlab_oauth.go index d51544dd7c7..7497c24d619 100644 --- a/pkg/login/social/connectors/gitlab_oauth.go +++ b/pkg/login/social/connectors/gitlab_oauth.go @@ -87,7 +87,7 @@ func (s *SocialGitlab) Validate(ctx context.Context, newSettings ssoModels.SSOSe } func (s *SocialGitlab) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.GitlabProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } diff --git a/pkg/login/social/connectors/google_oauth.go b/pkg/login/social/connectors/google_oauth.go index 3044548948b..113b4c0ef5a 100644 --- a/pkg/login/social/connectors/google_oauth.go +++ b/pkg/login/social/connectors/google_oauth.go @@ -87,7 +87,7 @@ func (s *SocialGoogle) Validate(ctx context.Context, newSettings ssoModels.SSOSe } func (s *SocialGoogle) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.GoogleProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } diff --git a/pkg/login/social/connectors/grafana_com_oauth.go b/pkg/login/social/connectors/grafana_com_oauth.go index 3f016b1f01c..84ea01632d1 100644 --- a/pkg/login/social/connectors/grafana_com_oauth.go +++ b/pkg/login/social/connectors/grafana_com_oauth.go @@ -39,15 +39,22 @@ type OrgRecord struct { } func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGrafanaCom { + s := newSocialBase(social.GrafanaComProviderName, orgRoleMapper, info, features, cfg) + // Override necessary settings info.AuthUrl = cfg.GrafanaComURL + "/oauth2/authorize" info.TokenUrl = cfg.GrafanaComURL + "/api/oauth2/token" info.AuthStyle = "inheader" + allowedOrganizations, err := util.SplitStringWithError(info.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GrafanaComProviderName, "error", err) + } + provider := &SocialGrafanaCom{ - SocialBase: newSocialBase(social.GrafanaComProviderName, orgRoleMapper, info, features, cfg), + SocialBase: s, url: cfg.GrafanaComURL, - allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]), + allowedOrganizations: allowedOrganizations, } if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) { @@ -80,11 +87,16 @@ func (s *SocialGrafanaCom) Validate(ctx context.Context, newSettings ssoModels.S } func (s *SocialGrafanaCom) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.GrafanaComProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } + allowedOrganizations, err := util.SplitStringWithError(newInfo.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GrafanaComProviderName, "error", err) + } + // Override necessary settings newInfo.AuthUrl = s.cfg.GrafanaComURL + "/oauth2/authorize" newInfo.TokenUrl = s.cfg.GrafanaComURL + "/api/oauth2/token" @@ -96,7 +108,7 @@ func (s *SocialGrafanaCom) Reload(ctx context.Context, settings ssoModels.SSOSet s.updateInfo(ctx, social.GrafanaComProviderName, newInfo) s.url = s.cfg.GrafanaComURL - s.allowedOrganizations = util.SplitString(newInfo.Extra[allowedOrganizationsKey]) + s.allowedOrganizations = allowedOrganizations return nil } diff --git a/pkg/login/social/connectors/okta_oauth.go b/pkg/login/social/connectors/okta_oauth.go index b126c2acd1d..ffa3a32a350 100644 --- a/pkg/login/social/connectors/okta_oauth.go +++ b/pkg/login/social/connectors/okta_oauth.go @@ -84,7 +84,7 @@ func (s *SocialOkta) Validate(ctx context.Context, newSettings ssoModels.SSOSett } func (s *SocialOkta) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.OktaProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } diff --git a/pkg/login/social/socialimpl/service.go b/pkg/login/social/socialimpl/service.go index ddfb9bf7016..65a9a5573cc 100644 --- a/pkg/login/social/socialimpl/service.go +++ b/pkg/login/social/socialimpl/service.go @@ -65,7 +65,7 @@ func ProvideService(cfg *setting.Cfg, continue } - info, err := connectors.CreateOAuthInfoFromKeyValues(ssoSetting.Settings) + info, err := connectors.CreateOAuthInfoFromKeyValuesWithLogging(ss.log, ssoSetting.Provider, ssoSetting.Settings) if err != nil { ss.log.Error("Failed to create OAuthInfo for provider", "error", err, "provider", ssoSetting.Provider) continue @@ -85,7 +85,7 @@ func ProvideService(cfg *setting.Cfg, settingsKVs := convertIniSectionToMap(sec) - info, err := connectors.CreateOAuthInfoFromKeyValues(settingsKVs) + info, err := connectors.CreateOAuthInfoFromKeyValuesWithLogging(ss.log, name, settingsKVs) if err != nil { ss.log.Error("Failed to create OAuthInfo for provider", "error", err, "provider", name) continue diff --git a/pkg/util/strings.go b/pkg/util/strings.go index f3a2d35540f..b3bbed21cf2 100644 --- a/pkg/util/strings.go +++ b/pkg/util/strings.go @@ -33,9 +33,18 @@ func stringsFallback(vals ...string) string { // SplitString splits a string and returns a list of strings. It supports JSON list syntax and strings separated by commas or spaces. // It supports quoted strings with spaces, e.g. "foo bar", "baz". +// It will return an empty list if it fails to parse the string. func SplitString(str string) []string { + result, _ := SplitStringWithError(str) + return result +} + +// SplitStringWithError splits a string and returns a list of strings. It supports JSON list syntax and strings separated by commas or spaces. +// It supports quoted strings with spaces, e.g. "foo bar", "baz". +// It returns an error if it cannot parse the string. +func SplitStringWithError(str string) ([]string, error) { if len(str) == 0 { - return []string{} + return []string{}, nil } // JSON list syntax support @@ -43,9 +52,9 @@ func SplitString(str string) []string { var res []string err := json.Unmarshal([]byte(str), &res) if err != nil { - return []string{} + return []string{}, fmt.Errorf("incorrect format: %s", str) } - return res + return res, nil } matches := stringListItemMatcher.FindAllString(str, -1) @@ -55,7 +64,7 @@ func SplitString(str string) []string { result[i] = strings.Trim(match, "\"") } - return result + return result, nil } // GetAgeString returns a string representing certain time from years to minutes. diff --git a/public/app/features/auth-config/utils/data.ts b/public/app/features/auth-config/utils/data.ts index c0beae9ec27..00a3dd84453 100644 --- a/public/app/features/auth-config/utils/data.ts +++ b/public/app/features/auth-config/utils/data.ts @@ -56,7 +56,10 @@ const strToValue = (val: string | string[]): SelectableValue[] => { } // Stored as JSON Array if (val.startsWith('[') && val.endsWith(']')) { - return JSON.parse(val).map((v: string) => ({ label: v, value: v })); + // Fallback to parsing it like a non-json string if it is not valid json, instead of crashing. + try { + return JSON.parse(val).map((v: string) => ({ label: v, value: v })); + } catch {} } return val.split(/[\s,]/).map((s) => ({ label: s, value: s })); From 02118cc6aad41160743d5490bedcc3eacb366aed Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 13 Feb 2025 17:44:13 +0000 Subject: [PATCH 582/894] Chore: Automerge i18n PRs (#99555) * add enable automerge step and update CODEOWNERS * add approver steps * move automerge step to pr approver token * get vault secrets * update workflow permissions * remove local --- .github/CODEOWNERS | 5 ++ .github/workflows/i18n-crowdin-download.yml | 57 +++++++++++++++++---- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a349655800b..f10a2d3b391 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -381,6 +381,11 @@ /crowdin.yml @grafana/grafana-frontend-platform /public/locales/ @grafana/grafana-frontend-platform +/public/locales/de-DE @grafanabot +/public/locales/es-ES @grafanabot +/public/locales/fr-FR @grafanabot +/public/locales/pt-BR @grafanabot +/public/locales/zh-Hans @grafanabot /public/app/core/internationalization/ @grafana/grafana-frontend-platform /e2e/ @grafana/grafana-frontend-platform /e2e/cloud-plugins-suite/ @grafana/partner-datasources diff --git a/.github/workflows/i18n-crowdin-download.yml b/.github/workflows/i18n-crowdin-download.yml index cba14abe43b..e0c3c50b9bb 100644 --- a/.github/workflows/i18n-crowdin-download.yml +++ b/.github/workflows/i18n-crowdin-download.yml @@ -3,7 +3,7 @@ name: Crowdin Download Action on: workflow_dispatch: schedule: - - cron: "0 * * * *" + - cron: "0 0 * * *" jobs: download-sources-from-crowdin: @@ -12,6 +12,7 @@ jobs: permissions: contents: write # needed to commit changes into the PR pull-requests: write # needed to update PR description, labels, etc + id-token: write # needed to get vault secrets steps: - name: Generate token @@ -41,17 +42,11 @@ jobs: pull_request_body: | :robot: Automatic download of translations from Crowdin. - Steps for merging: - 1. A quick sanity check of the changes and approve. Things to look out for: - - No changes in the English file. The source of truth is in the main branch, NOT in Crowdin. - - Translations maybe be removed if the English phrase was removed, but there should not be many of these - - Anything else that looks 'funky'. Ask if you're not sure. - 2. Approve & (Auto-)merge. :tada: + This runs once per day and will merge automatically if all the required checks pass. - If there's a conflict, close the pull request and **delete the branch**. A GH action will recreate the pull request. - Remember, the longer this pull request is open, the more likely it is that it'll get conflicts. + If there's a conflict, close the pull request and **delete the branch**. + You can then either wait for the schedule to trigger a new PR, or rerun the action manually. pull_request_labels: 'area/frontend, area/internationalization, no-changelog, no-backport' - pull_request_reviewers: 'grafana-frontend-platform' pull_request_base_branch_name: 'main' base_url: 'https://grafana.api.crowdin.com' config: 'crowdin.yml' @@ -119,3 +114,45 @@ jobs: with: pr: ${{ steps.crowdin-download.outputs.pull_request_number }} token: ${{ steps.generate_token.outputs.token }} + + - name: Get vault secrets + id: vault-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@main + with: + # Secrets placed in ci/repo/grafana/grafana/grafana-pr-approver + repo_secrets: | + GRAFANA_PR_APPROVER_APP_ID=grafana-pr-approver:app-id + GRAFANA_PR_APPROVER_APP_PEM=grafana-pr-approver:private-key + + - name: Generate approver token + if: steps.crowdin-download.outputs.pull_request_url + id: generate_approver_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ env.GRAFANA_PR_APPROVER_APP_ID }} + private_key: ${{ env.GRAFANA_PR_APPROVER_APP_PEM }} + + - name: Approve and automerge PR + if: steps.crowdin-download.outputs.pull_request_url + shell: bash + # Only approve if: + # - the PR does not modify files other than json files under the public/locales/ directory + # - the PR does not modify the en-US locale + run: | + filesChanged=$(gh pr diff --name-only ${{ steps.crowdin-download.outputs.pull_request_url }}) + + if [[ $(echo $filesChanged | grep -v 'public/locales/[a-zA-Z\-]*/grafana.json' | wc -l) -ne 0 ]]; then + echo "Non-i18n changes detected, not approving" + exit 1 + fi + + if [[ $(echo $filesChanged | grep "public/locales/en-US" | wc -l) -ne 0 ]]; then + echo "public/locales/en-US changes detected, not approving" + exit 1 + fi + + echo "Approving and enabling automerge" + gh pr review ${{ steps.crowdin-download.outputs.pull_request_url }} --approve + gh pr merge --auto --squash ${{ steps.crowdin-download.outputs.pull_request_url }} + env: + GITHUB_TOKEN: ${{ steps.generate_approver_token.outputs.token }} From 5aeaa18ac2d4c8866b774b8b9bd175d216c0e065 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Thu, 13 Feb 2025 11:46:29 -0600 Subject: [PATCH 583/894] Canvas: One click links and actions (#99616) Co-authored-by: Leon Sorokin --- .../VizTooltip/VizTooltipFooter.tsx | 95 +++++++---- public/app/features/actions/utils.ts | 1 + public/app/features/canvas/element.ts | 3 +- public/app/features/canvas/elements/cloud.tsx | 3 +- .../features/canvas/elements/droneFront.tsx | 3 +- .../features/canvas/elements/droneSide.tsx | 3 +- .../app/features/canvas/elements/droneTop.tsx | 3 +- .../app/features/canvas/elements/ellipse.tsx | 3 +- public/app/features/canvas/elements/icon.tsx | 3 +- .../features/canvas/elements/metricValue.tsx | 9 +- .../canvas/elements/parallelogram.tsx | 3 +- .../features/canvas/elements/rectangle.tsx | 3 +- .../canvas/elements/server/server.tsx | 3 +- public/app/features/canvas/elements/text.tsx | 3 +- .../app/features/canvas/elements/triangle.tsx | 3 +- .../features/canvas/elements/windTurbine.tsx | 3 +- .../app/features/canvas/runtime/element.tsx | 148 ++++++++++++------ public/app/features/canvas/runtime/scene.tsx | 2 + .../app/plugins/panel/canvas/CanvasPanel.tsx | 5 + .../panel/canvas/components/CanvasTooltip.tsx | 1 - .../canvas/editor/element/elementEditor.tsx | 35 +---- .../plugins/panel/canvas/migrations.test.ts | 14 +- public/app/plugins/panel/canvas/migrations.ts | 23 ++- public/app/plugins/panel/canvas/module.tsx | 1 - public/locales/en-US/grafana.json | 1 + public/locales/pseudo-LOCALE/grafana.json | 1 + 26 files changed, 218 insertions(+), 157 deletions(-) diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index c3c4a34eb64..5b79c42711f 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -1,11 +1,13 @@ import { css } from '@emotion/css'; +import { useMemo } from 'react'; -import { ActionModel, Field, GrafanaTheme2, LinkModel } from '@grafana/data'; +import { ActionModel, Field, GrafanaTheme2, LinkModel, ThemeSpacingTokens } from '@grafana/data'; import { Button, DataLinkButton, Icon, Stack } from '..'; import { useStyles2 } from '../../themes'; import { Trans } from '../../utils/i18n'; import { ActionButton } from '../Actions/ActionButton'; +import { ResponsiveProp } from '../Layout/utils/responsiveness'; interface VizTooltipFooterProps { dataLinks: Array>; @@ -15,50 +17,75 @@ interface VizTooltipFooterProps { export const ADD_ANNOTATION_ID = 'add-annotation-button'; -const renderDataLinks = (dataLinks: LinkModel[], styles: ReturnType) => { - const oneClickLink = dataLinks.find((link) => link.oneClick === true); +type RenderOneClickTrans = (title: string) => React.ReactNode; +type RenderItem = ( + item: T, + idx: number, + styles: ReturnType +) => React.ReactNode; + +function makeRenderLinksOrActions( + renderOneClickTrans: RenderOneClickTrans, + renderItem: RenderItem, + itemGap?: ResponsiveProp +) { + const renderLinksOrActions = (items: T[], styles: ReturnType) => { + if (items.length === 0) { + return; + } + + const oneClickItem = items.find((item) => item.oneClick === true); + + if (oneClickItem != null) { + return ( +
          + + + + {renderOneClickTrans(oneClickItem.title)} + + +
          + ); + } - if (oneClickLink != null) { return ( - - - - - Click to open {{ linkTitle: oneClickLink.title }} - - - +
          + + {items.map((item, i) => renderItem(item, i, styles))} + +
          ); - } + }; - return ( - - {dataLinks.map((link, i) => ( - - ))} - - ); -}; + return renderLinksOrActions; +} -const renderActions = (actions: ActionModel[]) => { - return ( - - {actions.map((action, i) => ( - - ))} - - ); -}; +const renderDataLinks = makeRenderLinksOrActions( + (title) => ( + Click to open {{ linkTitle: title }} + ), + (item, i, styles) => ( + + ), + 0.5 +); + +const renderActions = makeRenderLinksOrActions( + (title) => Click to {{ actionTitle: title }}, + (item, i, styles) => +); export const VizTooltipFooter = ({ dataLinks, actions = [], annotate }: VizTooltipFooterProps) => { const styles = useStyles2(getStyles); - const hasOneClickLink = dataLinks.some((link) => link.oneClick === true); + const hasOneClickLink = useMemo(() => dataLinks.some((link) => link.oneClick === true), [dataLinks]); + const hasOneClickAction = useMemo(() => actions.some((action) => action.oneClick === true), [actions]); return (
          - {dataLinks.length > 0 &&
          {renderDataLinks(dataLinks, styles)}
          } - {!hasOneClickLink && actions.length > 0 &&
          {renderActions(actions)}
          } - {!hasOneClickLink && annotate != null && ( + {!hasOneClickAction && renderDataLinks(dataLinks, styles)} + {!hasOneClickLink && renderActions(actions, styles)} + {!hasOneClickLink && !hasOneClickAction && annotate != null && (
          )} diff --git a/public/app/features/search/service/unified.test.ts b/public/app/features/search/service/unified.test.ts index e62a126f38f..8841418cd5a 100644 --- a/public/app/features/search/service/unified.test.ts +++ b/public/app/features/search/service/unified.test.ts @@ -115,9 +115,6 @@ describe('Unified Storage Searcher', () => { .mockResolvedValueOnce(mockResults) .mockResolvedValueOnce(mockFolders); - const consoleWarn = jest.fn(); - jest.spyOn(console, 'warn').mockImplementationOnce(consoleWarn); - const query: SearchQuery = { query: 'test', limit: 50, @@ -127,14 +124,15 @@ describe('Unified Storage Searcher', () => { const response = await searcher.search(query); - expect(response.view.length).toBe(1); - expect(response.view.get(0).title).toBe('DB 2'); + expect(response.view.length).toBe(2); + expect(response.view.get(0).title).toBe('DB 1'); + expect(response.view.get(0).folder).toBe('sharedwithme'); + expect(response.view.get(1).title).toBe('DB 2'); const df = response.view.dataFrame; const locationInfo = df.meta?.custom?.locationInfo; expect(locationInfo).toBeDefined(); expect(locationInfo?.folder2.name).toBe('Folder 2'); - expect(consoleWarn).toHaveBeenCalled(); expect(mockSearcher.search).toHaveBeenCalledTimes(3); }); diff --git a/public/app/features/search/service/unified.ts b/public/app/features/search/service/unified.ts index 9d942f668d3..8e270aba37b 100644 --- a/public/app/features/search/service/unified.ts +++ b/public/app/features/search/service/unified.ts @@ -204,15 +204,19 @@ export class UnifiedSearcher implements GrafanaSearcher { if (!hasMissing) { return rsp; } - // we still have results here with folders we can't find - // filter the results since we probably don't have access to that folder + const locationInfo = await this.locationInfo; - const hits = rsp.hits.filter((hit) => { - if (hit.folder === undefined || locationInfo[hit.folder] !== undefined) { - return true; + const hits = rsp.hits.map((hit) => { + if (hit.folder === undefined) { + return { ...hit, location: 'general', folder: 'general' }; } - console.warn('Dropping search hit with missing folder', hit); - return false; + + // this means user has permission to see this dashboard, but not the folder contents + if (locationInfo[hit.folder] === undefined) { + return { ...hit, location: 'sharedwithme', folder: 'sharedwithme' }; + } + + return hit; }); const totalHits = rsp.totalHits - (rsp.hits.length - hits.length); return { ...rsp, hits, totalHits }; @@ -370,6 +374,11 @@ async function loadLocationInfo(): Promise> { name: 'Dashboards', url: '/dashboards', }, // share location info with everyone + sharedwithme: { + kind: 'sharedwithme', + name: 'Shared with me', + url: '', + }, }; for (const hit of rsp.hits) { locationInfo[hit.name] = { diff --git a/public/app/features/search/service/utils.ts b/public/app/features/search/service/utils.ts index 6aeeaefba92..85c0168e480 100644 --- a/public/app/features/search/service/utils.ts +++ b/public/app/features/search/service/utils.ts @@ -49,6 +49,10 @@ export function getIconForKind(kind: string, isOpen?: boolean): IconName { return isOpen ? 'folder-open' : 'folder'; } + if (kind === 'sharedwithme') { + return 'users-alt'; + } + return 'question-circle'; } From 1c2f4e35bf1a12a693df3c8c4052ae40b0e55fef Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Thu, 13 Feb 2025 15:44:15 -0700 Subject: [PATCH 592/894] Frontend tests: comment out flaky test (#100685) --- e2e/various-suite/solo-route.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/various-suite/solo-route.spec.ts b/e2e/various-suite/solo-route.spec.ts index 415257ead7c..c7f90f8cd36 100644 --- a/e2e/various-suite/solo-route.spec.ts +++ b/e2e/various-suite/solo-route.spec.ts @@ -22,7 +22,7 @@ describe('Solo Route', () => { cy.contains('uplot-main-div').should('not.exist'); }); - it('Can view solo repeated panel in scenes', () => { + /*it('Can view solo repeated panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' @@ -30,7 +30,7 @@ describe('Solo Route', () => { e2e.components.Panels.Panel.title('server=A').should('exist'); cy.contains('uplot-main-div').should('not.exist'); - }); + });*/ it('Can view solo in repeated row and panel in scenes', () => { // open Panel Tests - Graph NG From 7f20495289869d01cb5f5c274af7625e8fa267ab Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 02:30:34 +0200 Subject: [PATCH 593/894] I18n: Download translations from Crowdin (#100689) 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 | 422 +++++++++++++++++++++++----- public/locales/es-ES/grafana.json | 422 +++++++++++++++++++++++----- public/locales/fr-FR/grafana.json | 422 +++++++++++++++++++++++----- public/locales/pt-BR/grafana.json | 422 +++++++++++++++++++++++----- public/locales/zh-Hans/grafana.json | 422 +++++++++++++++++++++++----- 5 files changed, 1770 insertions(+), 340 deletions(-) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 46fe1314938..fed79005cc6 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -34,12 +34,6 @@ "save-button": "" } }, - "actions-editor": { - "inline": { - "add-button": "", - "one-click-link": "" - } - }, "admin": { "anon-users": { "not-found": "" @@ -174,6 +168,24 @@ } }, "alerting": { + "alert": { + "alert-state": "", + "annotations": "", + "evaluation": "", + "evaluation-paused": "", + "evaluation-paused-description": "", + "last-evaluated": "", + "last-evaluation-duration": "", + "last-updated-at": "", + "last-updated-by": "", + "no-annotations": "", + "pending-period": "", + "rule": "", + "rule-identifier": "", + "rule-type": "", + "state-error-timeout": "", + "state-no-data": "" + }, "alert-recording-rule-form": { "evaluation-behaviour": { "description": { @@ -283,6 +295,7 @@ "contactPointFilter": { "label": "" }, + "copy-to-clipboard": "", "export": { "subtitle": { "formats": "", @@ -299,6 +312,13 @@ } } }, + "group-actions": { + "actions-trigger": "", + "delete": "", + "edit": "", + "export": "", + "reorder": "" + }, "list-view": { "empty": { "new-alert-rule": "", @@ -484,11 +504,23 @@ }, "rule-list": { "configure-datasource": "", + "ds-error-boundary": { + "description": "", + "title": "" + }, "filter-view": { "no-more-results": "", "no-rules-found": "" }, - "new-alert-rule": "" + "new-alert-rule": "", + "pagination": { + "next-page": "", + "previous-page": "" + }, + "return-button": { + "title": "" + }, + "rulerrule-loading-error": "" }, "rule-state": { "creating": "", @@ -690,7 +722,10 @@ "title": "" }, "custom-value": { - "label": "" + "description": "" + }, + "group": { + "undefined": "" }, "options": { "no-found": "" @@ -863,6 +898,82 @@ "redirect-link": "Liste in Grafana Alerting", "subtitle": "Benachrichtigungsregeln im Zusammenhang mit diesem Dashboard" }, + "default-layout": { + "description": "", + "item-options": { + "repeat": { + "direction": { + "horizontal": "", + "title": "", + "vertical": "" + }, + "max": "", + "title": "", + "variable": { + "description": "", + "title": "" + } + } + }, + "name": "", + "row-actions": { + "delete": "", + "modal": { + "alt-action": "", + "text": "", + "title": "" + } + }, + "row-options": { + "button": { + "label": "" + }, + "form": { + "cancel": "", + "repeat-for": { + "label": "", + "learn-more": "", + "warning": { + "text": "" + } + }, + "title": "", + "update": "" + }, + "modal": { + "title": "" + } + } + }, + "edit-pane": { + "objects": { + "multi-select": { + "selection-number": "" + } + }, + "open": "", + "panels": { + "multi-select": { + "selection-number": "" + } + }, + "row": { + "header": { + "hide": "", + "title": "" + }, + "multi-select": { + "options-header": "", + "selection-number": "" + } + }, + "tab": { + "multi-select": { + "options-header": "", + "selection-number": "" + } + } + }, "empty": { "add-library-panel-body": "Visualisierungen hinzufügen, die mit anderen Dashboards geteilt werden.", "add-library-panel-button": "Bibliotheksfenster hinzufügen", @@ -874,6 +985,9 @@ "import-a-dashboard-header": "Dashboard importieren", "import-dashboard-button": "Dashboard importieren" }, + "errors": { + "failed-to-load": "" + }, "inspect": { "data-tab": "Daten", "error-tab": "Fehler", @@ -926,19 +1040,130 @@ "rows": "Gesamtanzahl an Zeilen", "table-title": "Statistiken" }, + "options": { + "description": "", + "title": "", + "title-option": "" + }, + "panel-edit": { + "alerting-tab": { + "dashboard-not-saved": "", + "no-rules": "" + } + }, + "responsive-layout": { + "description": "", + "item-options": { + "hide-no-data": "", + "title": "" + }, + "name": "", + "options": { + "columns": "", + "fixed": "", + "min": "", + "one-column": "", + "rows": "", + "three-columns": "", + "two-columns": "" + } + }, + "rows-layout": { + "description": "", + "name": "", + "row": { + "collapse": "", + "expand": "", + "new": "", + "repeat": { + "learn-more": "", + "warning": "" + } + }, + "row-options": { + "height": { + "expand": "", + "hide-row-header": "", + "min": "", + "title": "" + }, + "repeat": { + "title": "", + "variable": { + "title": "" + } + }, + "title": "", + "title-option": "" + } + }, + "tabs-layout": { + "description": "", + "name": "", + "tab": { + "new": "" + }, + "tab-options": { + "title": "", + "title-option": "" + } + }, "toolbar": { "add": "Hinzufügen", + "add-panel": "", + "add-panel-lib": "", + "add-row": "", + "add-tab": "", "alert-rules": "Warnregeln", + "back-to-dashboard": "", + "dashboard-settings": { + "label": "", + "tooltip": "" + }, + "discard-library-panel-changes": "", + "discard-panel": "", + "discard-panel-new": "", + "edit": { + "label": "", + "tooltip": "" + }, + "edit-dashboard-v2-schema": "", + "enter-edit-mode": { + "label": "", + "tooltip": "" + }, + "exit-edit-mode": { + "label": "", + "tooltip": "" + }, "mark-favorite": "Als Favorit markieren", + "more-save-options": "", "open-original": "Original-Dashboard öffnen", "playlist-next": "Zum nächsten Dashboard", "playlist-previous": "Zum vorherigen Dashboard", "playlist-stop": "Wiedergabeliste stoppen", + "public-dashboard": "", "refresh": "Dashboard aktualisieren", "save": "Dashboard speichern", + "save-dashboard": { + "label": "", + "tooltip": "" + }, + "save-dashboard-copy": { + "label": "", + "tooltip": "" + }, + "save-dashboard-short": "", + "save-library-panel": "", "settings": "Dashboard-Einstellungen", - "share": "Dashboard oder Panel teilen", + "share": { + "label": "", + "tooltip": "" + }, "share-button": "Teilen", + "show-hidden-elements": "", + "switch-old-dashboard": "", + "unlink-library-panel": "", "unmark-favorite": "Markierung als Favorit entfernen" }, "validation": { @@ -946,6 +1171,14 @@ "invalid-json": "JSON ungültig", "tags-expected-array": "Array der jeweiligen Tags", "tags-expected-strings": "String-Array der jeweiligen Tags" + }, + "viz-panel": { + "options": { + "description": "", + "title": "", + "title-option": "", + "transparent-background": "" + } } }, "dashboard-import": { @@ -1048,6 +1281,12 @@ "angular-deprecation-description": "", "angular-deprecation-heading": "" }, + "panel-queries": { + "add-query-from-library": "" + }, + "query-library": { + "add-query-button": "" + }, "settings": { "variables": { "dependencies": { @@ -1125,40 +1364,11 @@ "scan-for-older-logs": "", "stop-scan": "" }, - "query-library": { - "add-edit-description": "", - "cancel": "", - "default-description": "", - "delete-query": "", - "delete-query-text": "", - "delete-query-title": "", - "private": "", - "public": "", - "query-deleted": "", - "query-template-add-error": "", - "query-template-added": "", - "query-template-edit-error": "", - "query-template-edited": "", - "save": "" - }, - "query-template-modal": { - "add-info": "", - "add-title": "", - "auto-star": "", - "data-source-name": "", - "description": "", - "edit-info": "", - "edit-title": "", - "query": "", - "visibility": "" - }, - "query-template-modall": { - "data-source-type": "" - }, "rich-history": { "close-tooltip": "Abfrageverlauf schließen", "datasource-a-z": "Datenquelle A-Z", "datasource-z-a": "Datenquelle Z-A", + "library-history-dropdown": "", "newest-first": "Neueste zuerst", "oldest-first": "Älteste zuerst", "query-history": "Abfrageverlauf", @@ -1305,10 +1515,9 @@ "sortBy": "" }, "related-logs": { - "docsLink": "", + "LrrDocsLink": "", "openExploreLogs": "", - "relatedLogsUnavailableAfterDocsLink": "", - "relatedLogsUnavailableBeforeDocsLink": "", + "relatedLogsUnavailable": "", "warnExperimentalFeature": "" }, "viewBy": "" @@ -1357,6 +1566,24 @@ "send-custom-feedback": "" }, "grafana-ui": { + "action-editor": { + "button": { + "confirm": "", + "confirm-action": "" + }, + "inline": { + "add-action": "", + "edit-action": "" + }, + "modal": { + "action-body": "", + "action-method": "", + "action-query-params": "", + "action-title": "", + "action-title-placeholder": "", + "one-click-description": "" + } + }, "auto-save-field": { "saved": "", "saving": "" @@ -1373,11 +1600,21 @@ }, "data-link-editor-modal": { "cancel": "", + "one-click-description": "", "save": "" }, + "data-link-inline-editor": { + "one-click": "" + }, "data-links-inline-editor": { "add-link": "", - "one-click-link": "" + "edit-link": "", + "one-click": "", + "one-click-enabled": "", + "title-not-provided": "", + "tooltip-edit": "", + "tooltip-remove": "", + "url-not-provided": "" }, "data-source-http-settings": { "access-help": "", @@ -1458,7 +1695,12 @@ "right-axis-indicator": "" }, "viz-tooltip": { - "footer-add-annotation": "" + "actions-confirmation-input-placeholder": "", + "actions-confirmation-label": "", + "actions-confirmation-message": "", + "footer-add-annotation": "", + "footer-click-to-action": "", + "footer-click-to-navigate": "" } }, "graph": { @@ -1780,7 +2022,8 @@ }, "log-row-message": { "ellipsis": "", - "more": "" + "more": "", + "see-details": "" }, "log-rows": { "disable-popover": { @@ -1792,6 +2035,13 @@ "shortcut": "" } }, + "logs-navigation": { + "newer-logs": "", + "older-logs": "", + "scroll-bottom": "", + "scroll-top": "", + "start-of-range": "" + }, "popover-menu": { "copy": "", "disable-menu": "", @@ -1881,6 +2131,7 @@ "title": "" }, "migrated-counts": { + "alert_rule_groups": "", "alert_rules": "", "contact_points": "", "dashboards": "", @@ -1941,9 +2192,8 @@ "public-preview": { "button-text": "", "message": "", - "message-plugins": "", - "title": "", - "title-plugins": "" + "message-cloud": "", + "title": "" }, "resource-details": { "dismiss-button": "", @@ -1986,6 +2236,7 @@ }, "resource-type": { "alert_rule": "", + "alert_rule_group": "", "contact_point": "", "dashboard": "Dashboard", "datasource": "Datenquelle", @@ -2031,6 +2282,15 @@ "title": "Warum mit Grafana hosten?" } }, + "multicombobox": { + "all": { + "title": "", + "title-filtered": "" + }, + "clear": { + "title": "" + } + }, "nav": { "add-new-connections": { "title": "Neue Verbindung hinzufügen" @@ -2384,7 +2644,8 @@ "list-label": "Navigation", "open": "", "undock": "Menü abdocken" - } + }, + "rss-button": "" }, "news": { "drawer": { @@ -2516,19 +2777,35 @@ } }, "details": { + "connections-tab": { + "description": "" + }, "labels": { "contactGrafanaLabs": "", + "customLinks": "", + "customLinksTooltip": "", "dependencies": "", + "documentation": "", "downloads": "", "from": "", "installedVersion": "", "lastCommitDate": "", "latestVersion": "", - "links": "", + "license": "", + "raiseAnIssue": "", "reportAbuse": "", + "reportAbuseTooltip": "", + "repository": "", "signature": "", "status": "", "updatedAt": "" + }, + "modal": { + "cancel": "", + "copyEmail": "", + "description": "", + "node": "", + "title": "" } }, "empty-state": { @@ -2743,14 +3020,6 @@ "role-label": "Rolle" } }, - "query-library": { - "datasource-names": "", - "delete-query-button": "", - "query-template-get-error": "", - "search": "", - "user-info-get-error": "", - "user-names": "" - }, "query-operation": { "header": { "collapse-row": "Abfragezeile einklappen", @@ -2760,7 +3029,6 @@ "expand-row": "Suchzeile erweitern", "hide-response": "", "remove-query": "Abfrage entfernen", - "save-to-query-library": "", "show-response": "", "toggle-edit-mode": "Textbearbeitungsmodus umschalten" }, @@ -2862,6 +3130,10 @@ "title": "" }, "save-dashboards": { + "message-length": { + "info": "", + "title": "" + }, "name-exists": { "message-info": "", "message-suggestion": "", @@ -3145,6 +3417,7 @@ "home-dashboard-placeholder": "Standard-Dashboard", "locale-label": "Sprache", "locale-placeholder": "Sprache wählen", + "theme-description": "", "theme-label": "UI-Design", "week-start-label": "Wochenbeginn" }, @@ -3216,6 +3489,13 @@ "url-column-header": "Snapshot url", "view-button": "Anzeigen" }, + "table": { + "container": { + "content": "", + "show-all-series": "", + "show-only-series": "" + } + }, "tag-filter": { "clear-button": "", "loading": "Wird geladen ...", @@ -3309,20 +3589,26 @@ "start-your-metrics-exploration": "", "subtitle": "" }, - "metric-overview": { - "description-label": "", - "labels": "", - "metric-attributes": "", - "no-description": "", - "type-label": "", - "unit-label": "", - "unknown-type": "" - }, "metric-select": { "filter-by": "", + "native-histogram": "", "new-badge": "", "otel-switch": "" }, + "native-histogram-banner": { + "ch-heatmap": "", + "ch-histogram": "", + "click-histogram": "", + "hide-examples": "", + "learn-more": "", + "metric-examples": "", + "nh-heatmap": "", + "nh-histogram": "", + "now": "", + "previously": "", + "see-examples": "", + "sentence": "" + }, "recent-metrics": { "or-view-a-recent-exploration": "" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index f324380f834..02db11b3d08 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -34,12 +34,6 @@ "save-button": "" } }, - "actions-editor": { - "inline": { - "add-button": "", - "one-click-link": "" - } - }, "admin": { "anon-users": { "not-found": "" @@ -174,6 +168,24 @@ } }, "alerting": { + "alert": { + "alert-state": "", + "annotations": "", + "evaluation": "", + "evaluation-paused": "", + "evaluation-paused-description": "", + "last-evaluated": "", + "last-evaluation-duration": "", + "last-updated-at": "", + "last-updated-by": "", + "no-annotations": "", + "pending-period": "", + "rule": "", + "rule-identifier": "", + "rule-type": "", + "state-error-timeout": "", + "state-no-data": "" + }, "alert-recording-rule-form": { "evaluation-behaviour": { "description": { @@ -283,6 +295,7 @@ "contactPointFilter": { "label": "" }, + "copy-to-clipboard": "", "export": { "subtitle": { "formats": "", @@ -299,6 +312,13 @@ } } }, + "group-actions": { + "actions-trigger": "", + "delete": "", + "edit": "", + "export": "", + "reorder": "" + }, "list-view": { "empty": { "new-alert-rule": "", @@ -484,11 +504,23 @@ }, "rule-list": { "configure-datasource": "", + "ds-error-boundary": { + "description": "", + "title": "" + }, "filter-view": { "no-more-results": "", "no-rules-found": "" }, - "new-alert-rule": "" + "new-alert-rule": "", + "pagination": { + "next-page": "", + "previous-page": "" + }, + "return-button": { + "title": "" + }, + "rulerrule-loading-error": "" }, "rule-state": { "creating": "", @@ -690,7 +722,10 @@ "title": "" }, "custom-value": { - "label": "" + "description": "" + }, + "group": { + "undefined": "" }, "options": { "no-found": "" @@ -863,6 +898,82 @@ "redirect-link": "Lista en Alertas de Grafana", "subtitle": "Reglas de alerta relacionadas con este tablero" }, + "default-layout": { + "description": "", + "item-options": { + "repeat": { + "direction": { + "horizontal": "", + "title": "", + "vertical": "" + }, + "max": "", + "title": "", + "variable": { + "description": "", + "title": "" + } + } + }, + "name": "", + "row-actions": { + "delete": "", + "modal": { + "alt-action": "", + "text": "", + "title": "" + } + }, + "row-options": { + "button": { + "label": "" + }, + "form": { + "cancel": "", + "repeat-for": { + "label": "", + "learn-more": "", + "warning": { + "text": "" + } + }, + "title": "", + "update": "" + }, + "modal": { + "title": "" + } + } + }, + "edit-pane": { + "objects": { + "multi-select": { + "selection-number": "" + } + }, + "open": "", + "panels": { + "multi-select": { + "selection-number": "" + } + }, + "row": { + "header": { + "hide": "", + "title": "" + }, + "multi-select": { + "options-header": "", + "selection-number": "" + } + }, + "tab": { + "multi-select": { + "options-header": "", + "selection-number": "" + } + } + }, "empty": { "add-library-panel-body": "Añadir las visualizaciones que se comparten con otros tableros.", "add-library-panel-button": "Añadir panel de biblioteca", @@ -874,6 +985,9 @@ "import-a-dashboard-header": "Importar un tablero", "import-dashboard-button": "Importar panel de control" }, + "errors": { + "failed-to-load": "" + }, "inspect": { "data-tab": "Datos", "error-tab": "Error", @@ -926,19 +1040,130 @@ "rows": "Número total de filas", "table-title": "Estadísticas" }, + "options": { + "description": "", + "title": "", + "title-option": "" + }, + "panel-edit": { + "alerting-tab": { + "dashboard-not-saved": "", + "no-rules": "" + } + }, + "responsive-layout": { + "description": "", + "item-options": { + "hide-no-data": "", + "title": "" + }, + "name": "", + "options": { + "columns": "", + "fixed": "", + "min": "", + "one-column": "", + "rows": "", + "three-columns": "", + "two-columns": "" + } + }, + "rows-layout": { + "description": "", + "name": "", + "row": { + "collapse": "", + "expand": "", + "new": "", + "repeat": { + "learn-more": "", + "warning": "" + } + }, + "row-options": { + "height": { + "expand": "", + "hide-row-header": "", + "min": "", + "title": "" + }, + "repeat": { + "title": "", + "variable": { + "title": "" + } + }, + "title": "", + "title-option": "" + } + }, + "tabs-layout": { + "description": "", + "name": "", + "tab": { + "new": "" + }, + "tab-options": { + "title": "", + "title-option": "" + } + }, "toolbar": { "add": "Añadir", + "add-panel": "", + "add-panel-lib": "", + "add-row": "", + "add-tab": "", "alert-rules": "Reglas de alerta", + "back-to-dashboard": "", + "dashboard-settings": { + "label": "", + "tooltip": "" + }, + "discard-library-panel-changes": "", + "discard-panel": "", + "discard-panel-new": "", + "edit": { + "label": "", + "tooltip": "" + }, + "edit-dashboard-v2-schema": "", + "enter-edit-mode": { + "label": "", + "tooltip": "" + }, + "exit-edit-mode": { + "label": "", + "tooltip": "" + }, "mark-favorite": "Marcar como favorito", + "more-save-options": "", "open-original": "Abrir el panel de control original", "playlist-next": "Ir al siguiente panel de control", "playlist-previous": "Ir al panel de control anterior", "playlist-stop": "Detener la lista de reproducción", + "public-dashboard": "", "refresh": "Actualizar panel de control", "save": "Guardar panel de control", + "save-dashboard": { + "label": "", + "tooltip": "" + }, + "save-dashboard-copy": { + "label": "", + "tooltip": "" + }, + "save-dashboard-short": "", + "save-library-panel": "", "settings": "Ajustes del panel de control", - "share": "Compartir panel o panel de control", + "share": { + "label": "", + "tooltip": "" + }, "share-button": "Compartir", + "show-hidden-elements": "", + "switch-old-dashboard": "", + "unlink-library-panel": "", "unmark-favorite": "Deshacer marca como favorito" }, "validation": { @@ -946,6 +1171,14 @@ "invalid-json": "JSON no válido", "tags-expected-array": "etiquetas: matriz prevista", "tags-expected-strings": "etiquetas: matriz de cadenas prevista" + }, + "viz-panel": { + "options": { + "description": "", + "title": "", + "title-option": "", + "transparent-background": "" + } } }, "dashboard-import": { @@ -1048,6 +1281,12 @@ "angular-deprecation-description": "", "angular-deprecation-heading": "" }, + "panel-queries": { + "add-query-from-library": "" + }, + "query-library": { + "add-query-button": "" + }, "settings": { "variables": { "dependencies": { @@ -1125,40 +1364,11 @@ "scan-for-older-logs": "", "stop-scan": "" }, - "query-library": { - "add-edit-description": "", - "cancel": "", - "default-description": "", - "delete-query": "", - "delete-query-text": "", - "delete-query-title": "", - "private": "", - "public": "", - "query-deleted": "", - "query-template-add-error": "", - "query-template-added": "", - "query-template-edit-error": "", - "query-template-edited": "", - "save": "" - }, - "query-template-modal": { - "add-info": "", - "add-title": "", - "auto-star": "", - "data-source-name": "", - "description": "", - "edit-info": "", - "edit-title": "", - "query": "", - "visibility": "" - }, - "query-template-modall": { - "data-source-type": "" - }, "rich-history": { "close-tooltip": "Cerrar el historial de consultas", "datasource-a-z": "Fuente de datos A-Z", "datasource-z-a": "Fuente de datos Z-A", + "library-history-dropdown": "", "newest-first": "El más reciente primero", "oldest-first": "El más antiguo primero", "query-history": "Historial de consultas", @@ -1305,10 +1515,9 @@ "sortBy": "" }, "related-logs": { - "docsLink": "", + "LrrDocsLink": "", "openExploreLogs": "", - "relatedLogsUnavailableAfterDocsLink": "", - "relatedLogsUnavailableBeforeDocsLink": "", + "relatedLogsUnavailable": "", "warnExperimentalFeature": "" }, "viewBy": "" @@ -1357,6 +1566,24 @@ "send-custom-feedback": "" }, "grafana-ui": { + "action-editor": { + "button": { + "confirm": "", + "confirm-action": "" + }, + "inline": { + "add-action": "", + "edit-action": "" + }, + "modal": { + "action-body": "", + "action-method": "", + "action-query-params": "", + "action-title": "", + "action-title-placeholder": "", + "one-click-description": "" + } + }, "auto-save-field": { "saved": "", "saving": "" @@ -1373,11 +1600,21 @@ }, "data-link-editor-modal": { "cancel": "", + "one-click-description": "", "save": "" }, + "data-link-inline-editor": { + "one-click": "" + }, "data-links-inline-editor": { "add-link": "", - "one-click-link": "" + "edit-link": "", + "one-click": "", + "one-click-enabled": "", + "title-not-provided": "", + "tooltip-edit": "", + "tooltip-remove": "", + "url-not-provided": "" }, "data-source-http-settings": { "access-help": "", @@ -1458,7 +1695,12 @@ "right-axis-indicator": "" }, "viz-tooltip": { - "footer-add-annotation": "" + "actions-confirmation-input-placeholder": "", + "actions-confirmation-label": "", + "actions-confirmation-message": "", + "footer-add-annotation": "", + "footer-click-to-action": "", + "footer-click-to-navigate": "" } }, "graph": { @@ -1780,7 +2022,8 @@ }, "log-row-message": { "ellipsis": "", - "more": "" + "more": "", + "see-details": "" }, "log-rows": { "disable-popover": { @@ -1792,6 +2035,13 @@ "shortcut": "" } }, + "logs-navigation": { + "newer-logs": "", + "older-logs": "", + "scroll-bottom": "", + "scroll-top": "", + "start-of-range": "" + }, "popover-menu": { "copy": "", "disable-menu": "", @@ -1881,6 +2131,7 @@ "title": "" }, "migrated-counts": { + "alert_rule_groups": "", "alert_rules": "", "contact_points": "", "dashboards": "", @@ -1941,9 +2192,8 @@ "public-preview": { "button-text": "", "message": "", - "message-plugins": "", - "title": "", - "title-plugins": "" + "message-cloud": "", + "title": "" }, "resource-details": { "dismiss-button": "", @@ -1986,6 +2236,7 @@ }, "resource-type": { "alert_rule": "", + "alert_rule_group": "", "contact_point": "", "dashboard": "Panel de control", "datasource": "Fuente de datos", @@ -2031,6 +2282,15 @@ "title": "¿Por qué alojar con Grafana?" } }, + "multicombobox": { + "all": { + "title": "", + "title-filtered": "" + }, + "clear": { + "title": "" + } + }, "nav": { "add-new-connections": { "title": "Añadir nueva conexión" @@ -2384,7 +2644,8 @@ "list-label": "Navegación", "open": "", "undock": "Desanclar el menú" - } + }, + "rss-button": "" }, "news": { "drawer": { @@ -2516,19 +2777,35 @@ } }, "details": { + "connections-tab": { + "description": "" + }, "labels": { "contactGrafanaLabs": "", + "customLinks": "", + "customLinksTooltip": "", "dependencies": "", + "documentation": "", "downloads": "", "from": "", "installedVersion": "", "lastCommitDate": "", "latestVersion": "", - "links": "", + "license": "", + "raiseAnIssue": "", "reportAbuse": "", + "reportAbuseTooltip": "", + "repository": "", "signature": "", "status": "", "updatedAt": "" + }, + "modal": { + "cancel": "", + "copyEmail": "", + "description": "", + "node": "", + "title": "" } }, "empty-state": { @@ -2743,14 +3020,6 @@ "role-label": "Rol" } }, - "query-library": { - "datasource-names": "", - "delete-query-button": "", - "query-template-get-error": "", - "search": "", - "user-info-get-error": "", - "user-names": "" - }, "query-operation": { "header": { "collapse-row": "Contraer la fila de la consulta", @@ -2760,7 +3029,6 @@ "expand-row": "Expandir la fila de la consulta", "hide-response": "", "remove-query": "Eliminar consulta", - "save-to-query-library": "", "show-response": "", "toggle-edit-mode": "Alternar el modo de edición de texto" }, @@ -2862,6 +3130,10 @@ "title": "" }, "save-dashboards": { + "message-length": { + "info": "", + "title": "" + }, "name-exists": { "message-info": "", "message-suggestion": "", @@ -3145,6 +3417,7 @@ "home-dashboard-placeholder": "Panel de control por defecto", "locale-label": "Idioma", "locale-placeholder": "Cambiar idioma", + "theme-description": "", "theme-label": "Tema de interfaz de usuario", "week-start-label": "Inicio de la semana" }, @@ -3216,6 +3489,13 @@ "url-column-header": "URL de la instantánea", "view-button": "Vista" }, + "table": { + "container": { + "content": "", + "show-all-series": "", + "show-only-series": "" + } + }, "tag-filter": { "clear-button": "", "loading": "Cargando...", @@ -3309,20 +3589,26 @@ "start-your-metrics-exploration": "", "subtitle": "" }, - "metric-overview": { - "description-label": "", - "labels": "", - "metric-attributes": "", - "no-description": "", - "type-label": "", - "unit-label": "", - "unknown-type": "" - }, "metric-select": { "filter-by": "", + "native-histogram": "", "new-badge": "", "otel-switch": "" }, + "native-histogram-banner": { + "ch-heatmap": "", + "ch-histogram": "", + "click-histogram": "", + "hide-examples": "", + "learn-more": "", + "metric-examples": "", + "nh-heatmap": "", + "nh-histogram": "", + "now": "", + "previously": "", + "see-examples": "", + "sentence": "" + }, "recent-metrics": { "or-view-a-recent-exploration": "" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 2ea9883ffe1..33cfe2d4b40 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -34,12 +34,6 @@ "save-button": "" } }, - "actions-editor": { - "inline": { - "add-button": "", - "one-click-link": "" - } - }, "admin": { "anon-users": { "not-found": "" @@ -174,6 +168,24 @@ } }, "alerting": { + "alert": { + "alert-state": "", + "annotations": "", + "evaluation": "", + "evaluation-paused": "", + "evaluation-paused-description": "", + "last-evaluated": "", + "last-evaluation-duration": "", + "last-updated-at": "", + "last-updated-by": "", + "no-annotations": "", + "pending-period": "", + "rule": "", + "rule-identifier": "", + "rule-type": "", + "state-error-timeout": "", + "state-no-data": "" + }, "alert-recording-rule-form": { "evaluation-behaviour": { "description": { @@ -283,6 +295,7 @@ "contactPointFilter": { "label": "" }, + "copy-to-clipboard": "", "export": { "subtitle": { "formats": "", @@ -299,6 +312,13 @@ } } }, + "group-actions": { + "actions-trigger": "", + "delete": "", + "edit": "", + "export": "", + "reorder": "" + }, "list-view": { "empty": { "new-alert-rule": "", @@ -484,11 +504,23 @@ }, "rule-list": { "configure-datasource": "", + "ds-error-boundary": { + "description": "", + "title": "" + }, "filter-view": { "no-more-results": "", "no-rules-found": "" }, - "new-alert-rule": "" + "new-alert-rule": "", + "pagination": { + "next-page": "", + "previous-page": "" + }, + "return-button": { + "title": "" + }, + "rulerrule-loading-error": "" }, "rule-state": { "creating": "", @@ -690,7 +722,10 @@ "title": "" }, "custom-value": { - "label": "" + "description": "" + }, + "group": { + "undefined": "" }, "options": { "no-found": "" @@ -863,6 +898,82 @@ "redirect-link": "Liste dans Alertes Grafana", "subtitle": "Règles d'alerte liées à ce tableau de bord" }, + "default-layout": { + "description": "", + "item-options": { + "repeat": { + "direction": { + "horizontal": "", + "title": "", + "vertical": "" + }, + "max": "", + "title": "", + "variable": { + "description": "", + "title": "" + } + } + }, + "name": "", + "row-actions": { + "delete": "", + "modal": { + "alt-action": "", + "text": "", + "title": "" + } + }, + "row-options": { + "button": { + "label": "" + }, + "form": { + "cancel": "", + "repeat-for": { + "label": "", + "learn-more": "", + "warning": { + "text": "" + } + }, + "title": "", + "update": "" + }, + "modal": { + "title": "" + } + } + }, + "edit-pane": { + "objects": { + "multi-select": { + "selection-number": "" + } + }, + "open": "", + "panels": { + "multi-select": { + "selection-number": "" + } + }, + "row": { + "header": { + "hide": "", + "title": "" + }, + "multi-select": { + "options-header": "", + "selection-number": "" + } + }, + "tab": { + "multi-select": { + "options-header": "", + "selection-number": "" + } + } + }, "empty": { "add-library-panel-body": "Ajoutez des visualisations partagées avec d'autres tableaux de bord.", "add-library-panel-button": "Ajouter un panneau Bibliothèque", @@ -874,6 +985,9 @@ "import-a-dashboard-header": "Importer un tableau de bord", "import-dashboard-button": "Importer un tableau de bord" }, + "errors": { + "failed-to-load": "" + }, "inspect": { "data-tab": "Données", "error-tab": "Erreur", @@ -926,19 +1040,130 @@ "rows": "Nombre total de lignes", "table-title": "Statistiques" }, + "options": { + "description": "", + "title": "", + "title-option": "" + }, + "panel-edit": { + "alerting-tab": { + "dashboard-not-saved": "", + "no-rules": "" + } + }, + "responsive-layout": { + "description": "", + "item-options": { + "hide-no-data": "", + "title": "" + }, + "name": "", + "options": { + "columns": "", + "fixed": "", + "min": "", + "one-column": "", + "rows": "", + "three-columns": "", + "two-columns": "" + } + }, + "rows-layout": { + "description": "", + "name": "", + "row": { + "collapse": "", + "expand": "", + "new": "", + "repeat": { + "learn-more": "", + "warning": "" + } + }, + "row-options": { + "height": { + "expand": "", + "hide-row-header": "", + "min": "", + "title": "" + }, + "repeat": { + "title": "", + "variable": { + "title": "" + } + }, + "title": "", + "title-option": "" + } + }, + "tabs-layout": { + "description": "", + "name": "", + "tab": { + "new": "" + }, + "tab-options": { + "title": "", + "title-option": "" + } + }, "toolbar": { "add": "Ajouter", + "add-panel": "", + "add-panel-lib": "", + "add-row": "", + "add-tab": "", "alert-rules": "Règles d'alerte", + "back-to-dashboard": "", + "dashboard-settings": { + "label": "", + "tooltip": "" + }, + "discard-library-panel-changes": "", + "discard-panel": "", + "discard-panel-new": "", + "edit": { + "label": "", + "tooltip": "" + }, + "edit-dashboard-v2-schema": "", + "enter-edit-mode": { + "label": "", + "tooltip": "" + }, + "exit-edit-mode": { + "label": "", + "tooltip": "" + }, "mark-favorite": "Marquer comme favori", + "more-save-options": "", "open-original": "Ouvrir le tableau de bord d'origine", "playlist-next": "Accéder au tableau de bord suivant", "playlist-previous": "Accéder au tableau de bord précédent", "playlist-stop": "Arrêter la liste de lecture", + "public-dashboard": "", "refresh": "Actualiser le tableau de bord", "save": "Enregistrer le tableau de bord", + "save-dashboard": { + "label": "", + "tooltip": "" + }, + "save-dashboard-copy": { + "label": "", + "tooltip": "" + }, + "save-dashboard-short": "", + "save-library-panel": "", "settings": "Paramètres du tableau de bord", - "share": "Partager le tableau de bord ou le panneau", + "share": { + "label": "", + "tooltip": "" + }, "share-button": "Partager", + "show-hidden-elements": "", + "switch-old-dashboard": "", + "unlink-library-panel": "", "unmark-favorite": "Supprimer des favoris" }, "validation": { @@ -946,6 +1171,14 @@ "invalid-json": "JSON non valide", "tags-expected-array": "étiquettes attendues tableau", "tags-expected-strings": "étiquettes attendues tableau de chaînes" + }, + "viz-panel": { + "options": { + "description": "", + "title": "", + "title-option": "", + "transparent-background": "" + } } }, "dashboard-import": { @@ -1048,6 +1281,12 @@ "angular-deprecation-description": "", "angular-deprecation-heading": "" }, + "panel-queries": { + "add-query-from-library": "" + }, + "query-library": { + "add-query-button": "" + }, "settings": { "variables": { "dependencies": { @@ -1125,40 +1364,11 @@ "scan-for-older-logs": "", "stop-scan": "" }, - "query-library": { - "add-edit-description": "", - "cancel": "", - "default-description": "", - "delete-query": "", - "delete-query-text": "", - "delete-query-title": "", - "private": "", - "public": "", - "query-deleted": "", - "query-template-add-error": "", - "query-template-added": "", - "query-template-edit-error": "", - "query-template-edited": "", - "save": "" - }, - "query-template-modal": { - "add-info": "", - "add-title": "", - "auto-star": "", - "data-source-name": "", - "description": "", - "edit-info": "", - "edit-title": "", - "query": "", - "visibility": "" - }, - "query-template-modall": { - "data-source-type": "" - }, "rich-history": { "close-tooltip": "Fermer l'historique des requêtes", "datasource-a-z": "Source de données A-Z", "datasource-z-a": "Source de données Z-A", + "library-history-dropdown": "", "newest-first": "Plus récent en premier", "oldest-first": "Plus ancien en premier", "query-history": "Historique des requêtes", @@ -1305,10 +1515,9 @@ "sortBy": "" }, "related-logs": { - "docsLink": "", + "LrrDocsLink": "", "openExploreLogs": "", - "relatedLogsUnavailableAfterDocsLink": "", - "relatedLogsUnavailableBeforeDocsLink": "", + "relatedLogsUnavailable": "", "warnExperimentalFeature": "" }, "viewBy": "" @@ -1357,6 +1566,24 @@ "send-custom-feedback": "" }, "grafana-ui": { + "action-editor": { + "button": { + "confirm": "", + "confirm-action": "" + }, + "inline": { + "add-action": "", + "edit-action": "" + }, + "modal": { + "action-body": "", + "action-method": "", + "action-query-params": "", + "action-title": "", + "action-title-placeholder": "", + "one-click-description": "" + } + }, "auto-save-field": { "saved": "", "saving": "" @@ -1373,11 +1600,21 @@ }, "data-link-editor-modal": { "cancel": "", + "one-click-description": "", "save": "" }, + "data-link-inline-editor": { + "one-click": "" + }, "data-links-inline-editor": { "add-link": "", - "one-click-link": "" + "edit-link": "", + "one-click": "", + "one-click-enabled": "", + "title-not-provided": "", + "tooltip-edit": "", + "tooltip-remove": "", + "url-not-provided": "" }, "data-source-http-settings": { "access-help": "", @@ -1458,7 +1695,12 @@ "right-axis-indicator": "" }, "viz-tooltip": { - "footer-add-annotation": "" + "actions-confirmation-input-placeholder": "", + "actions-confirmation-label": "", + "actions-confirmation-message": "", + "footer-add-annotation": "", + "footer-click-to-action": "", + "footer-click-to-navigate": "" } }, "graph": { @@ -1780,7 +2022,8 @@ }, "log-row-message": { "ellipsis": "", - "more": "" + "more": "", + "see-details": "" }, "log-rows": { "disable-popover": { @@ -1792,6 +2035,13 @@ "shortcut": "" } }, + "logs-navigation": { + "newer-logs": "", + "older-logs": "", + "scroll-bottom": "", + "scroll-top": "", + "start-of-range": "" + }, "popover-menu": { "copy": "", "disable-menu": "", @@ -1881,6 +2131,7 @@ "title": "" }, "migrated-counts": { + "alert_rule_groups": "", "alert_rules": "", "contact_points": "", "dashboards": "", @@ -1941,9 +2192,8 @@ "public-preview": { "button-text": "", "message": "", - "message-plugins": "", - "title": "", - "title-plugins": "" + "message-cloud": "", + "title": "" }, "resource-details": { "dismiss-button": "", @@ -1986,6 +2236,7 @@ }, "resource-type": { "alert_rule": "", + "alert_rule_group": "", "contact_point": "", "dashboard": "Tableau de bord", "datasource": "Source de données", @@ -2031,6 +2282,15 @@ "title": "Pourquoi héberger avec Grafana ?" } }, + "multicombobox": { + "all": { + "title": "", + "title-filtered": "" + }, + "clear": { + "title": "" + } + }, "nav": { "add-new-connections": { "title": "Ajouter une nouvelle connexion" @@ -2384,7 +2644,8 @@ "list-label": "Navigation", "open": "", "undock": "Ancrer le menu" - } + }, + "rss-button": "" }, "news": { "drawer": { @@ -2516,19 +2777,35 @@ } }, "details": { + "connections-tab": { + "description": "" + }, "labels": { "contactGrafanaLabs": "", + "customLinks": "", + "customLinksTooltip": "", "dependencies": "", + "documentation": "", "downloads": "", "from": "", "installedVersion": "", "lastCommitDate": "", "latestVersion": "", - "links": "", + "license": "", + "raiseAnIssue": "", "reportAbuse": "", + "reportAbuseTooltip": "", + "repository": "", "signature": "", "status": "", "updatedAt": "" + }, + "modal": { + "cancel": "", + "copyEmail": "", + "description": "", + "node": "", + "title": "" } }, "empty-state": { @@ -2743,14 +3020,6 @@ "role-label": "Rôle" } }, - "query-library": { - "datasource-names": "", - "delete-query-button": "", - "query-template-get-error": "", - "search": "", - "user-info-get-error": "", - "user-names": "" - }, "query-operation": { "header": { "collapse-row": "Réduire la ligne de requête", @@ -2760,7 +3029,6 @@ "expand-row": "Développer la ligne de requête", "hide-response": "", "remove-query": "Supprimer la requête", - "save-to-query-library": "", "show-response": "", "toggle-edit-mode": "Activer/désactiver le mode édition de texte" }, @@ -2862,6 +3130,10 @@ "title": "" }, "save-dashboards": { + "message-length": { + "info": "", + "title": "" + }, "name-exists": { "message-info": "", "message-suggestion": "", @@ -3145,6 +3417,7 @@ "home-dashboard-placeholder": "Tableau de bord par défaut", "locale-label": "Langue", "locale-placeholder": "Choisir une langue", + "theme-description": "", "theme-label": "Thème de l'interface utilisateur", "week-start-label": "Début de la semaine" }, @@ -3216,6 +3489,13 @@ "url-column-header": "URL de l'instantané", "view-button": "Afficher" }, + "table": { + "container": { + "content": "", + "show-all-series": "", + "show-only-series": "" + } + }, "tag-filter": { "clear-button": "", "loading": "Chargement en cours...", @@ -3309,20 +3589,26 @@ "start-your-metrics-exploration": "", "subtitle": "" }, - "metric-overview": { - "description-label": "", - "labels": "", - "metric-attributes": "", - "no-description": "", - "type-label": "", - "unit-label": "", - "unknown-type": "" - }, "metric-select": { "filter-by": "", + "native-histogram": "", "new-badge": "", "otel-switch": "" }, + "native-histogram-banner": { + "ch-heatmap": "", + "ch-histogram": "", + "click-histogram": "", + "hide-examples": "", + "learn-more": "", + "metric-examples": "", + "nh-heatmap": "", + "nh-histogram": "", + "now": "", + "previously": "", + "see-examples": "", + "sentence": "" + }, "recent-metrics": { "or-view-a-recent-exploration": "" }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 5e0b7cdc2d4..4e648b574bc 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -34,12 +34,6 @@ "save-button": "" } }, - "actions-editor": { - "inline": { - "add-button": "", - "one-click-link": "" - } - }, "admin": { "anon-users": { "not-found": "" @@ -174,6 +168,24 @@ } }, "alerting": { + "alert": { + "alert-state": "", + "annotations": "", + "evaluation": "", + "evaluation-paused": "", + "evaluation-paused-description": "", + "last-evaluated": "", + "last-evaluation-duration": "", + "last-updated-at": "", + "last-updated-by": "", + "no-annotations": "", + "pending-period": "", + "rule": "", + "rule-identifier": "", + "rule-type": "", + "state-error-timeout": "", + "state-no-data": "" + }, "alert-recording-rule-form": { "evaluation-behaviour": { "description": { @@ -283,6 +295,7 @@ "contactPointFilter": { "label": "" }, + "copy-to-clipboard": "", "export": { "subtitle": { "formats": "", @@ -299,6 +312,13 @@ } } }, + "group-actions": { + "actions-trigger": "", + "delete": "", + "edit": "", + "export": "", + "reorder": "" + }, "list-view": { "empty": { "new-alert-rule": "", @@ -484,11 +504,23 @@ }, "rule-list": { "configure-datasource": "", + "ds-error-boundary": { + "description": "", + "title": "" + }, "filter-view": { "no-more-results": "", "no-rules-found": "" }, - "new-alert-rule": "" + "new-alert-rule": "", + "pagination": { + "next-page": "", + "previous-page": "" + }, + "return-button": { + "title": "" + }, + "rulerrule-loading-error": "" }, "rule-state": { "creating": "", @@ -690,7 +722,10 @@ "title": "" }, "custom-value": { - "label": "" + "description": "" + }, + "group": { + "undefined": "" }, "options": { "no-found": "" @@ -863,6 +898,82 @@ "redirect-link": "Lista no alerta do Grafana", "subtitle": "Regras de alerta relacionadas a este painel de controle" }, + "default-layout": { + "description": "", + "item-options": { + "repeat": { + "direction": { + "horizontal": "", + "title": "", + "vertical": "" + }, + "max": "", + "title": "", + "variable": { + "description": "", + "title": "" + } + } + }, + "name": "", + "row-actions": { + "delete": "", + "modal": { + "alt-action": "", + "text": "", + "title": "" + } + }, + "row-options": { + "button": { + "label": "" + }, + "form": { + "cancel": "", + "repeat-for": { + "label": "", + "learn-more": "", + "warning": { + "text": "" + } + }, + "title": "", + "update": "" + }, + "modal": { + "title": "" + } + } + }, + "edit-pane": { + "objects": { + "multi-select": { + "selection-number": "" + } + }, + "open": "", + "panels": { + "multi-select": { + "selection-number": "" + } + }, + "row": { + "header": { + "hide": "", + "title": "" + }, + "multi-select": { + "options-header": "", + "selection-number": "" + } + }, + "tab": { + "multi-select": { + "options-header": "", + "selection-number": "" + } + } + }, "empty": { "add-library-panel-body": "Adicione visualizações que são compartilhadas com outros painéis de controle.", "add-library-panel-button": "Adicionar painel de biblioteca", @@ -874,6 +985,9 @@ "import-a-dashboard-header": "Importar um painel de controle", "import-dashboard-button": "Importar painel de controle" }, + "errors": { + "failed-to-load": "" + }, "inspect": { "data-tab": "Dados", "error-tab": "Erro", @@ -926,19 +1040,130 @@ "rows": "Número total de linhas", "table-title": "Estatísticas" }, + "options": { + "description": "", + "title": "", + "title-option": "" + }, + "panel-edit": { + "alerting-tab": { + "dashboard-not-saved": "", + "no-rules": "" + } + }, + "responsive-layout": { + "description": "", + "item-options": { + "hide-no-data": "", + "title": "" + }, + "name": "", + "options": { + "columns": "", + "fixed": "", + "min": "", + "one-column": "", + "rows": "", + "three-columns": "", + "two-columns": "" + } + }, + "rows-layout": { + "description": "", + "name": "", + "row": { + "collapse": "", + "expand": "", + "new": "", + "repeat": { + "learn-more": "", + "warning": "" + } + }, + "row-options": { + "height": { + "expand": "", + "hide-row-header": "", + "min": "", + "title": "" + }, + "repeat": { + "title": "", + "variable": { + "title": "" + } + }, + "title": "", + "title-option": "" + } + }, + "tabs-layout": { + "description": "", + "name": "", + "tab": { + "new": "" + }, + "tab-options": { + "title": "", + "title-option": "" + } + }, "toolbar": { "add": "Adicionar", + "add-panel": "", + "add-panel-lib": "", + "add-row": "", + "add-tab": "", "alert-rules": "Regras de alerta", + "back-to-dashboard": "", + "dashboard-settings": { + "label": "", + "tooltip": "" + }, + "discard-library-panel-changes": "", + "discard-panel": "", + "discard-panel-new": "", + "edit": { + "label": "", + "tooltip": "" + }, + "edit-dashboard-v2-schema": "", + "enter-edit-mode": { + "label": "", + "tooltip": "" + }, + "exit-edit-mode": { + "label": "", + "tooltip": "" + }, "mark-favorite": "Marcar como favorito", + "more-save-options": "", "open-original": "Abrir painel de controle original", "playlist-next": "Ir para o próximo painel de controle", "playlist-previous": "Ir para o painel de controle anterior", "playlist-stop": "Parar lista de reprodução", + "public-dashboard": "", "refresh": "Atualizar painel de controle", "save": "Salvar painel de controle", + "save-dashboard": { + "label": "", + "tooltip": "" + }, + "save-dashboard-copy": { + "label": "", + "tooltip": "" + }, + "save-dashboard-short": "", + "save-library-panel": "", "settings": "Configurações do painel de controle", - "share": "Compartilhar painel de controle", + "share": { + "label": "", + "tooltip": "" + }, "share-button": "Compartilhar", + "show-hidden-elements": "", + "switch-old-dashboard": "", + "unlink-library-panel": "", "unmark-favorite": "Desmarcar como favorito" }, "validation": { @@ -946,6 +1171,14 @@ "invalid-json": "JSON inválido", "tags-expected-array": "matriz de tags esperada", "tags-expected-strings": "matriz de strings de tags esperada" + }, + "viz-panel": { + "options": { + "description": "", + "title": "", + "title-option": "", + "transparent-background": "" + } } }, "dashboard-import": { @@ -1048,6 +1281,12 @@ "angular-deprecation-description": "", "angular-deprecation-heading": "" }, + "panel-queries": { + "add-query-from-library": "" + }, + "query-library": { + "add-query-button": "" + }, "settings": { "variables": { "dependencies": { @@ -1125,40 +1364,11 @@ "scan-for-older-logs": "", "stop-scan": "" }, - "query-library": { - "add-edit-description": "", - "cancel": "", - "default-description": "", - "delete-query": "", - "delete-query-text": "", - "delete-query-title": "", - "private": "", - "public": "", - "query-deleted": "", - "query-template-add-error": "", - "query-template-added": "", - "query-template-edit-error": "", - "query-template-edited": "", - "save": "" - }, - "query-template-modal": { - "add-info": "", - "add-title": "", - "auto-star": "", - "data-source-name": "", - "description": "", - "edit-info": "", - "edit-title": "", - "query": "", - "visibility": "" - }, - "query-template-modall": { - "data-source-type": "" - }, "rich-history": { "close-tooltip": "Fechar histórico de consultas", "datasource-a-z": "Fonte de dados A-Z", "datasource-z-a": "Fonte de dados Z-A", + "library-history-dropdown": "", "newest-first": "Mais recentes primeiro", "oldest-first": "Mais antigos primeiro", "query-history": "Histórico de consultas", @@ -1305,10 +1515,9 @@ "sortBy": "" }, "related-logs": { - "docsLink": "", + "LrrDocsLink": "", "openExploreLogs": "", - "relatedLogsUnavailableAfterDocsLink": "", - "relatedLogsUnavailableBeforeDocsLink": "", + "relatedLogsUnavailable": "", "warnExperimentalFeature": "" }, "viewBy": "" @@ -1357,6 +1566,24 @@ "send-custom-feedback": "" }, "grafana-ui": { + "action-editor": { + "button": { + "confirm": "", + "confirm-action": "" + }, + "inline": { + "add-action": "", + "edit-action": "" + }, + "modal": { + "action-body": "", + "action-method": "", + "action-query-params": "", + "action-title": "", + "action-title-placeholder": "", + "one-click-description": "" + } + }, "auto-save-field": { "saved": "", "saving": "" @@ -1373,11 +1600,21 @@ }, "data-link-editor-modal": { "cancel": "", + "one-click-description": "", "save": "" }, + "data-link-inline-editor": { + "one-click": "" + }, "data-links-inline-editor": { "add-link": "", - "one-click-link": "" + "edit-link": "", + "one-click": "", + "one-click-enabled": "", + "title-not-provided": "", + "tooltip-edit": "", + "tooltip-remove": "", + "url-not-provided": "" }, "data-source-http-settings": { "access-help": "", @@ -1458,7 +1695,12 @@ "right-axis-indicator": "" }, "viz-tooltip": { - "footer-add-annotation": "" + "actions-confirmation-input-placeholder": "", + "actions-confirmation-label": "", + "actions-confirmation-message": "", + "footer-add-annotation": "", + "footer-click-to-action": "", + "footer-click-to-navigate": "" } }, "graph": { @@ -1780,7 +2022,8 @@ }, "log-row-message": { "ellipsis": "", - "more": "" + "more": "", + "see-details": "" }, "log-rows": { "disable-popover": { @@ -1792,6 +2035,13 @@ "shortcut": "" } }, + "logs-navigation": { + "newer-logs": "", + "older-logs": "", + "scroll-bottom": "", + "scroll-top": "", + "start-of-range": "" + }, "popover-menu": { "copy": "", "disable-menu": "", @@ -1881,6 +2131,7 @@ "title": "" }, "migrated-counts": { + "alert_rule_groups": "", "alert_rules": "", "contact_points": "", "dashboards": "", @@ -1941,9 +2192,8 @@ "public-preview": { "button-text": "", "message": "", - "message-plugins": "", - "title": "", - "title-plugins": "" + "message-cloud": "", + "title": "" }, "resource-details": { "dismiss-button": "", @@ -1986,6 +2236,7 @@ }, "resource-type": { "alert_rule": "", + "alert_rule_group": "", "contact_point": "", "dashboard": "Painel de controle", "datasource": "Fonte de dados", @@ -2031,6 +2282,15 @@ "title": "Por que hospedar com o Grafana?" } }, + "multicombobox": { + "all": { + "title": "", + "title-filtered": "" + }, + "clear": { + "title": "" + } + }, "nav": { "add-new-connections": { "title": "Adicionar nova conexão" @@ -2384,7 +2644,8 @@ "list-label": "Navegação", "open": "", "undock": "Desacoplar menu" - } + }, + "rss-button": "" }, "news": { "drawer": { @@ -2516,19 +2777,35 @@ } }, "details": { + "connections-tab": { + "description": "" + }, "labels": { "contactGrafanaLabs": "", + "customLinks": "", + "customLinksTooltip": "", "dependencies": "", + "documentation": "", "downloads": "", "from": "", "installedVersion": "", "lastCommitDate": "", "latestVersion": "", - "links": "", + "license": "", + "raiseAnIssue": "", "reportAbuse": "", + "reportAbuseTooltip": "", + "repository": "", "signature": "", "status": "", "updatedAt": "" + }, + "modal": { + "cancel": "", + "copyEmail": "", + "description": "", + "node": "", + "title": "" } }, "empty-state": { @@ -2743,14 +3020,6 @@ "role-label": "Função" } }, - "query-library": { - "datasource-names": "", - "delete-query-button": "", - "query-template-get-error": "", - "search": "", - "user-info-get-error": "", - "user-names": "" - }, "query-operation": { "header": { "collapse-row": "Recolher linha de consulta", @@ -2760,7 +3029,6 @@ "expand-row": "Expandir linha de consulta", "hide-response": "", "remove-query": "Remover consulta", - "save-to-query-library": "", "show-response": "", "toggle-edit-mode": "Alternar modo de edição de texto" }, @@ -2862,6 +3130,10 @@ "title": "" }, "save-dashboards": { + "message-length": { + "info": "", + "title": "" + }, "name-exists": { "message-info": "", "message-suggestion": "", @@ -3145,6 +3417,7 @@ "home-dashboard-placeholder": "Painel de controle padrão", "locale-label": "Idioma", "locale-placeholder": "Escolher idioma", + "theme-description": "", "theme-label": "Tema da interface", "week-start-label": "Início da semana" }, @@ -3216,6 +3489,13 @@ "url-column-header": "URL da captura", "view-button": "Visualizar" }, + "table": { + "container": { + "content": "", + "show-all-series": "", + "show-only-series": "" + } + }, "tag-filter": { "clear-button": "", "loading": "Carregando...", @@ -3309,20 +3589,26 @@ "start-your-metrics-exploration": "", "subtitle": "" }, - "metric-overview": { - "description-label": "", - "labels": "", - "metric-attributes": "", - "no-description": "", - "type-label": "", - "unit-label": "", - "unknown-type": "" - }, "metric-select": { "filter-by": "", + "native-histogram": "", "new-badge": "", "otel-switch": "" }, + "native-histogram-banner": { + "ch-heatmap": "", + "ch-histogram": "", + "click-histogram": "", + "hide-examples": "", + "learn-more": "", + "metric-examples": "", + "nh-heatmap": "", + "nh-histogram": "", + "now": "", + "previously": "", + "see-examples": "", + "sentence": "" + }, "recent-metrics": { "or-view-a-recent-exploration": "" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index c5cba66ace2..ba17087b1e7 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -34,12 +34,6 @@ "save-button": "" } }, - "actions-editor": { - "inline": { - "add-button": "", - "one-click-link": "" - } - }, "admin": { "anon-users": { "not-found": "" @@ -174,6 +168,24 @@ } }, "alerting": { + "alert": { + "alert-state": "", + "annotations": "", + "evaluation": "", + "evaluation-paused": "", + "evaluation-paused-description": "", + "last-evaluated": "", + "last-evaluation-duration": "", + "last-updated-at": "", + "last-updated-by": "", + "no-annotations": "", + "pending-period": "", + "rule": "", + "rule-identifier": "", + "rule-type": "", + "state-error-timeout": "", + "state-no-data": "" + }, "alert-recording-rule-form": { "evaluation-behaviour": { "description": { @@ -281,6 +293,7 @@ "contactPointFilter": { "label": "" }, + "copy-to-clipboard": "", "export": { "subtitle": { "formats": "", @@ -297,6 +310,13 @@ } } }, + "group-actions": { + "actions-trigger": "", + "delete": "", + "edit": "", + "export": "", + "reorder": "" + }, "list-view": { "empty": { "new-alert-rule": "", @@ -480,11 +500,23 @@ }, "rule-list": { "configure-datasource": "", + "ds-error-boundary": { + "description": "", + "title": "" + }, "filter-view": { "no-more-results": "", "no-rules-found": "" }, - "new-alert-rule": "" + "new-alert-rule": "", + "pagination": { + "next-page": "", + "previous-page": "" + }, + "return-button": { + "title": "" + }, + "rulerrule-loading-error": "" }, "rule-state": { "creating": "", @@ -681,7 +713,10 @@ "title": "" }, "custom-value": { - "label": "" + "description": "" + }, + "group": { + "undefined": "" }, "options": { "no-found": "" @@ -854,6 +889,82 @@ "redirect-link": "Grafana Alerting 中的列表", "subtitle": "与此仪表板相关的警报规则" }, + "default-layout": { + "description": "", + "item-options": { + "repeat": { + "direction": { + "horizontal": "", + "title": "", + "vertical": "" + }, + "max": "", + "title": "", + "variable": { + "description": "", + "title": "" + } + } + }, + "name": "", + "row-actions": { + "delete": "", + "modal": { + "alt-action": "", + "text": "", + "title": "" + } + }, + "row-options": { + "button": { + "label": "" + }, + "form": { + "cancel": "", + "repeat-for": { + "label": "", + "learn-more": "", + "warning": { + "text": "" + } + }, + "title": "", + "update": "" + }, + "modal": { + "title": "" + } + } + }, + "edit-pane": { + "objects": { + "multi-select": { + "selection-number": "" + } + }, + "open": "", + "panels": { + "multi-select": { + "selection-number": "" + } + }, + "row": { + "header": { + "hide": "", + "title": "" + }, + "multi-select": { + "options-header": "", + "selection-number": "" + } + }, + "tab": { + "multi-select": { + "options-header": "", + "selection-number": "" + } + } + }, "empty": { "add-library-panel-body": "添加与其他仪表板共享的可视化。", "add-library-panel-button": "添加库面板", @@ -865,6 +976,9 @@ "import-a-dashboard-header": "导入仪表板", "import-dashboard-button": "导入仪表板" }, + "errors": { + "failed-to-load": "" + }, "inspect": { "data-tab": "数据", "error-tab": "错误", @@ -917,19 +1031,130 @@ "rows": "总行数", "table-title": "统计信息" }, + "options": { + "description": "", + "title": "", + "title-option": "" + }, + "panel-edit": { + "alerting-tab": { + "dashboard-not-saved": "", + "no-rules": "" + } + }, + "responsive-layout": { + "description": "", + "item-options": { + "hide-no-data": "", + "title": "" + }, + "name": "", + "options": { + "columns": "", + "fixed": "", + "min": "", + "one-column": "", + "rows": "", + "three-columns": "", + "two-columns": "" + } + }, + "rows-layout": { + "description": "", + "name": "", + "row": { + "collapse": "", + "expand": "", + "new": "", + "repeat": { + "learn-more": "", + "warning": "" + } + }, + "row-options": { + "height": { + "expand": "", + "hide-row-header": "", + "min": "", + "title": "" + }, + "repeat": { + "title": "", + "variable": { + "title": "" + } + }, + "title": "", + "title-option": "" + } + }, + "tabs-layout": { + "description": "", + "name": "", + "tab": { + "new": "" + }, + "tab-options": { + "title": "", + "title-option": "" + } + }, "toolbar": { "add": "添加", + "add-panel": "", + "add-panel-lib": "", + "add-row": "", + "add-tab": "", "alert-rules": "警报规则", + "back-to-dashboard": "", + "dashboard-settings": { + "label": "", + "tooltip": "" + }, + "discard-library-panel-changes": "", + "discard-panel": "", + "discard-panel-new": "", + "edit": { + "label": "", + "tooltip": "" + }, + "edit-dashboard-v2-schema": "", + "enter-edit-mode": { + "label": "", + "tooltip": "" + }, + "exit-edit-mode": { + "label": "", + "tooltip": "" + }, "mark-favorite": "标记为收藏", + "more-save-options": "", "open-original": "打开原始仪表板", "playlist-next": "前往下一个仪表板", "playlist-previous": "前往上一个仪表板", "playlist-stop": "停止播放列表", + "public-dashboard": "", "refresh": "刷新仪表板", "save": "保存仪表板", + "save-dashboard": { + "label": "", + "tooltip": "" + }, + "save-dashboard-copy": { + "label": "", + "tooltip": "" + }, + "save-dashboard-short": "", + "save-library-panel": "", "settings": "仪表板设置", - "share": "分享仪表板或面板", + "share": { + "label": "", + "tooltip": "" + }, "share-button": "分享", + "show-hidden-elements": "", + "switch-old-dashboard": "", + "unlink-library-panel": "", "unmark-favorite": "取消标记为收藏" }, "validation": { @@ -937,6 +1162,14 @@ "invalid-json": "无有效的 JSON", "tags-expected-array": "标签预期数组", "tags-expected-strings": "标签预期字符串数组" + }, + "viz-panel": { + "options": { + "description": "", + "title": "", + "title-option": "", + "transparent-background": "" + } } }, "dashboard-import": { @@ -1039,6 +1272,12 @@ "angular-deprecation-description": "", "angular-deprecation-heading": "" }, + "panel-queries": { + "add-query-from-library": "" + }, + "query-library": { + "add-query-button": "" + }, "settings": { "variables": { "dependencies": { @@ -1116,40 +1355,11 @@ "scan-for-older-logs": "", "stop-scan": "" }, - "query-library": { - "add-edit-description": "", - "cancel": "", - "default-description": "", - "delete-query": "", - "delete-query-text": "", - "delete-query-title": "", - "private": "", - "public": "", - "query-deleted": "", - "query-template-add-error": "", - "query-template-added": "", - "query-template-edit-error": "", - "query-template-edited": "", - "save": "" - }, - "query-template-modal": { - "add-info": "", - "add-title": "", - "auto-star": "", - "data-source-name": "", - "description": "", - "edit-info": "", - "edit-title": "", - "query": "", - "visibility": "" - }, - "query-template-modall": { - "data-source-type": "" - }, "rich-history": { "close-tooltip": "关闭查询历史记录", "datasource-a-z": "数据源 A-Z", "datasource-z-a": "数据源 Z-A", + "library-history-dropdown": "", "newest-first": "最新在前", "oldest-first": "最早在前", "query-history": "查询历史记录", @@ -1296,10 +1506,9 @@ "sortBy": "" }, "related-logs": { - "docsLink": "", + "LrrDocsLink": "", "openExploreLogs": "", - "relatedLogsUnavailableAfterDocsLink": "", - "relatedLogsUnavailableBeforeDocsLink": "", + "relatedLogsUnavailable": "", "warnExperimentalFeature": "" }, "viewBy": "" @@ -1348,6 +1557,24 @@ "send-custom-feedback": "" }, "grafana-ui": { + "action-editor": { + "button": { + "confirm": "", + "confirm-action": "" + }, + "inline": { + "add-action": "", + "edit-action": "" + }, + "modal": { + "action-body": "", + "action-method": "", + "action-query-params": "", + "action-title": "", + "action-title-placeholder": "", + "one-click-description": "" + } + }, "auto-save-field": { "saved": "", "saving": "" @@ -1364,11 +1591,21 @@ }, "data-link-editor-modal": { "cancel": "", + "one-click-description": "", "save": "" }, + "data-link-inline-editor": { + "one-click": "" + }, "data-links-inline-editor": { "add-link": "", - "one-click-link": "" + "edit-link": "", + "one-click": "", + "one-click-enabled": "", + "title-not-provided": "", + "tooltip-edit": "", + "tooltip-remove": "", + "url-not-provided": "" }, "data-source-http-settings": { "access-help": "", @@ -1449,7 +1686,12 @@ "right-axis-indicator": "" }, "viz-tooltip": { - "footer-add-annotation": "" + "actions-confirmation-input-placeholder": "", + "actions-confirmation-label": "", + "actions-confirmation-message": "", + "footer-add-annotation": "", + "footer-click-to-action": "", + "footer-click-to-navigate": "" } }, "graph": { @@ -1770,7 +2012,8 @@ }, "log-row-message": { "ellipsis": "", - "more": "" + "more": "", + "see-details": "" }, "log-rows": { "disable-popover": { @@ -1782,6 +2025,13 @@ "shortcut": "" } }, + "logs-navigation": { + "newer-logs": "", + "older-logs": "", + "scroll-bottom": "", + "scroll-top": "", + "start-of-range": "" + }, "popover-menu": { "copy": "", "disable-menu": "", @@ -1871,6 +2121,7 @@ "title": "" }, "migrated-counts": { + "alert_rule_groups": "", "alert_rules": "", "contact_points": "", "dashboards": "", @@ -1931,9 +2182,8 @@ "public-preview": { "button-text": "", "message": "", - "message-plugins": "", - "title": "", - "title-plugins": "" + "message-cloud": "", + "title": "" }, "resource-details": { "dismiss-button": "", @@ -1976,6 +2226,7 @@ }, "resource-type": { "alert_rule": "", + "alert_rule_group": "", "contact_point": "", "dashboard": "仪表板", "datasource": "数据源", @@ -2021,6 +2272,15 @@ "title": "为什么使用 Grafana 托管?" } }, + "multicombobox": { + "all": { + "title": "", + "title-filtered": "" + }, + "clear": { + "title": "" + } + }, "nav": { "add-new-connections": { "title": "添加新连接" @@ -2374,7 +2634,8 @@ "list-label": "导航", "open": "", "undock": "取消停靠菜单" - } + }, + "rss-button": "" }, "news": { "drawer": { @@ -2506,19 +2767,35 @@ } }, "details": { + "connections-tab": { + "description": "" + }, "labels": { "contactGrafanaLabs": "", + "customLinks": "", + "customLinksTooltip": "", "dependencies": "", + "documentation": "", "downloads": "", "from": "", "installedVersion": "", "lastCommitDate": "", "latestVersion": "", - "links": "", + "license": "", + "raiseAnIssue": "", "reportAbuse": "", + "reportAbuseTooltip": "", + "repository": "", "signature": "", "status": "", "updatedAt": "" + }, + "modal": { + "cancel": "", + "copyEmail": "", + "description": "", + "node": "", + "title": "" } }, "empty-state": { @@ -2732,14 +3009,6 @@ "role-label": "角色" } }, - "query-library": { - "datasource-names": "", - "delete-query-button": "", - "query-template-get-error": "", - "search": "", - "user-info-get-error": "", - "user-names": "" - }, "query-operation": { "header": { "collapse-row": "折叠查询行", @@ -2749,7 +3018,6 @@ "expand-row": "展开查询行", "hide-response": "", "remove-query": "删除查询", - "save-to-query-library": "", "show-response": "", "toggle-edit-mode": "切换文本编辑模式" }, @@ -2848,6 +3116,10 @@ "title": "" }, "save-dashboards": { + "message-length": { + "info": "", + "title": "" + }, "name-exists": { "message-info": "", "message-suggestion": "", @@ -3131,6 +3403,7 @@ "home-dashboard-placeholder": "默认仪表板", "locale-label": "语言", "locale-placeholder": "选择语言", + "theme-description": "", "theme-label": "UI 主题", "week-start-label": "每周开始日" }, @@ -3202,6 +3475,13 @@ "url-column-header": "快照网址", "view-button": "查看" }, + "table": { + "container": { + "content": "", + "show-all-series": "", + "show-only-series": "" + } + }, "tag-filter": { "clear-button": "", "loading": "加载中...", @@ -3295,20 +3575,26 @@ "start-your-metrics-exploration": "", "subtitle": "" }, - "metric-overview": { - "description-label": "", - "labels": "", - "metric-attributes": "", - "no-description": "", - "type-label": "", - "unit-label": "", - "unknown-type": "" - }, "metric-select": { "filter-by": "", + "native-histogram": "", "new-badge": "", "otel-switch": "" }, + "native-histogram-banner": { + "ch-heatmap": "", + "ch-histogram": "", + "click-histogram": "", + "hide-examples": "", + "learn-more": "", + "metric-examples": "", + "nh-heatmap": "", + "nh-histogram": "", + "now": "", + "previously": "", + "see-examples": "", + "sentence": "" + }, "recent-metrics": { "or-view-a-recent-exploration": "" }, From 12bb50f97ff7c7482ce1c9c35d4821efeef33d55 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Fri, 14 Feb 2025 10:05:34 +0100 Subject: [PATCH 594/894] Advisor: Make evaluation interval and max history configurable (#100534) --- apps/advisor/pkg/app/app.go | 3 +- .../pkg/app/checkregistry/checkregistry.go | 6 ++ .../pkg/app/checkscheduler/checkscheduler.go | 65 +++++++++++++++---- .../app/checkscheduler/checkscheduler_test.go | 54 +++++++++++++-- .../checktyperegisterer.go | 3 +- pkg/registry/apps/advisor/register.go | 9 ++- 6 files changed, 119 insertions(+), 21 deletions(-) diff --git a/apps/advisor/pkg/app/app.go b/apps/advisor/pkg/app/app.go index f645466c4e1..a640682b0fc 100644 --- a/apps/advisor/pkg/app/app.go +++ b/apps/advisor/pkg/app/app.go @@ -19,10 +19,11 @@ import ( func New(cfg app.Config) (app.App, error) { // Read config - checkRegistry, ok := cfg.SpecificConfig.(checkregistry.CheckService) + specificConfig, ok := cfg.SpecificConfig.(checkregistry.AdvisorAppConfig) if !ok { return nil, fmt.Errorf("invalid config type") } + checkRegistry := specificConfig.CheckRegistry // Prepare storage client clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{}) diff --git a/apps/advisor/pkg/app/checkregistry/checkregistry.go b/apps/advisor/pkg/app/checkregistry/checkregistry.go index 7bbc138f5c6..1b690c70892 100644 --- a/apps/advisor/pkg/app/checkregistry/checkregistry.go +++ b/apps/advisor/pkg/app/checkregistry/checkregistry.go @@ -57,3 +57,9 @@ func (s *Service) Checks() []checks.Check { ), } } + +// AdvisorAppConfig is the configuration received from Grafana to run the app +type AdvisorAppConfig struct { + CheckRegistry CheckService + PluginConfig map[string]string +} diff --git a/apps/advisor/pkg/app/checkscheduler/checkscheduler.go b/apps/advisor/pkg/app/checkscheduler/checkscheduler.go index f344326a59f..0a2d2c4e0bf 100644 --- a/apps/advisor/pkg/app/checkscheduler/checkscheduler.go +++ b/apps/advisor/pkg/app/checkscheduler/checkscheduler.go @@ -4,11 +4,13 @@ import ( "context" "fmt" "sort" + "strconv" "time" "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/k8s" "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" @@ -16,24 +18,35 @@ import ( "k8s.io/klog/v2" ) -const evaluateChecksInterval = 24 * time.Hour -const maxChecks = 10 +const defaultEvaluationInterval = 24 * time.Hour +const defaultMaxHistory = 10 // Runner is a "runnable" app used to be able to expose and API endpoint // with the existing checks types. This does not need to be a CRUD resource, but it is // the only way existing at the moment to expose the check types. type Runner struct { - checkRegistry checkregistry.CheckService - client resource.Client + checkRegistry checkregistry.CheckService + client resource.Client + evaluationInterval time.Duration + maxHistory int } // NewRunner creates a new Runner. func New(cfg app.Config) (app.Runnable, error) { // Read config - checkRegistry, ok := cfg.SpecificConfig.(checkregistry.CheckService) + specificConfig, ok := cfg.SpecificConfig.(checkregistry.AdvisorAppConfig) if !ok { return nil, fmt.Errorf("invalid config type") } + checkRegistry := specificConfig.CheckRegistry + evalInterval, err := getEvaluationInterval(specificConfig.PluginConfig) + if err != nil { + return nil, err + } + maxHistory, err := getMaxHistory(specificConfig.PluginConfig) + if err != nil { + return nil, err + } // Prepare storage client clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{}) @@ -43,8 +56,10 @@ func New(cfg app.Config) (app.Runnable, error) { } return &Runner{ - checkRegistry: checkRegistry, - client: client, + checkRegistry: checkRegistry, + client: client, + evaluationInterval: evalInterval, + maxHistory: maxHistory, }, nil } @@ -64,7 +79,7 @@ func (r *Runner) Run(ctx context.Context) error { } } - nextSendInterval := time.Until(lastCreated.Add(evaluateChecksInterval)) + nextSendInterval := time.Until(lastCreated.Add(r.evaluationInterval)) if nextSendInterval < time.Minute { nextSendInterval = 1 * time.Minute } @@ -85,8 +100,8 @@ func (r *Runner) Run(ctx context.Context) error { klog.Error("Error cleaning up old check reports", "error", err) } - if nextSendInterval != evaluateChecksInterval { - nextSendInterval = evaluateChecksInterval + if nextSendInterval != r.evaluationInterval { + nextSendInterval = r.evaluationInterval } ticker.Reset(nextSendInterval) case <-ctx.Done(): @@ -155,7 +170,7 @@ func (r *Runner) cleanupChecks(ctx context.Context) error { } for _, checks := range checksByType { - if len(checks) > maxChecks { + if len(checks) > r.maxHistory { // Sort checks by creation time sort.Slice(checks, func(i, j int) bool { ti := checks[i].GetCreationTimestamp().Time @@ -163,7 +178,7 @@ func (r *Runner) cleanupChecks(ctx context.Context) error { return ti.Before(tj) }) // Delete the oldest checks - for i := 0; i < len(checks)-maxChecks; i++ { + for i := 0; i < len(checks)-r.maxHistory; i++ { check := checks[i] id := check.GetStaticMetadata().Identifier() err := r.client.Delete(ctx, id, resource.DeleteOptions{}) @@ -176,3 +191,29 @@ func (r *Runner) cleanupChecks(ctx context.Context) error { return nil } + +func getEvaluationInterval(pluginConfig map[string]string) (time.Duration, error) { + evaluationInterval := defaultEvaluationInterval + configEvaluationInterval, ok := pluginConfig["evaluation_interval"] + if ok { + var err error + evaluationInterval, err = gtime.ParseDuration(configEvaluationInterval) + if err != nil { + return 0, fmt.Errorf("invalid evaluation interval: %w", err) + } + } + return evaluationInterval, nil +} + +func getMaxHistory(pluginConfig map[string]string) (int, error) { + maxHistory := defaultMaxHistory + configMaxHistory, ok := pluginConfig["max_history"] + if ok { + var err error + maxHistory, err = strconv.Atoi(configMaxHistory) + if err != nil { + return 0, fmt.Errorf("invalid max history: %w", err) + } + } + return maxHistory, nil +} diff --git a/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go b/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go index b969e96745c..4fd5876b9ff 100644 --- a/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go +++ b/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go @@ -135,8 +135,8 @@ func TestRunner_cleanupChecks_WithinMax(t *testing.T) { func TestRunner_cleanupChecks_ErrorOnDelete(t *testing.T) { mockClient := &MockClient{ listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { - items := make([]advisorv0alpha1.Check, 0, maxChecks+1) - for i := 0; i < maxChecks+1; i++ { + items := make([]advisorv0alpha1.Check, 0, defaultMaxHistory+1) + for i := 0; i < defaultMaxHistory+1; i++ { item := advisorv0alpha1.Check{} item.ObjectMeta.SetLabels(map[string]string{ checks.TypeLabel: "mock", @@ -153,7 +153,8 @@ func TestRunner_cleanupChecks_ErrorOnDelete(t *testing.T) { } runner := &Runner{ - client: mockClient, + client: mockClient, + maxHistory: defaultMaxHistory, } err := runner.cleanupChecks(context.Background()) assert.ErrorContains(t, err, "delete error") @@ -161,8 +162,8 @@ func TestRunner_cleanupChecks_ErrorOnDelete(t *testing.T) { func TestRunner_cleanupChecks_Success(t *testing.T) { itemsDeleted := []string{} - items := make([]advisorv0alpha1.Check, 0, maxChecks+1) - for i := 0; i < maxChecks+1; i++ { + items := make([]advisorv0alpha1.Check, 0, defaultMaxHistory+1) + for i := 0; i < defaultMaxHistory+1; i++ { item := advisorv0alpha1.Check{} item.ObjectMeta.SetName(fmt.Sprintf("check-%d", i)) item.ObjectMeta.SetLabels(map[string]string{ @@ -187,13 +188,54 @@ func TestRunner_cleanupChecks_Success(t *testing.T) { } runner := &Runner{ - client: mockClient, + client: mockClient, + maxHistory: defaultMaxHistory, } err := runner.cleanupChecks(context.Background()) assert.NoError(t, err) assert.Equal(t, []string{"check-0"}, itemsDeleted) } +func Test_getEvaluationInterval(t *testing.T) { + t.Run("default", func(t *testing.T) { + interval, err := getEvaluationInterval(map[string]string{}) + assert.NoError(t, err) + assert.Equal(t, 24*time.Hour, interval) + }) + + t.Run("invalid", func(t *testing.T) { + interval, err := getEvaluationInterval(map[string]string{"evaluation_interval": "invalid"}) + assert.Error(t, err) + assert.Zero(t, interval) + }) + + t.Run("custom", func(t *testing.T) { + interval, err := getEvaluationInterval(map[string]string{"evaluation_interval": "1h"}) + assert.NoError(t, err) + assert.Equal(t, time.Hour, interval) + }) +} + +func Test_getMaxHistory(t *testing.T) { + t.Run("default", func(t *testing.T) { + history, err := getMaxHistory(map[string]string{}) + assert.NoError(t, err) + assert.Equal(t, 10, history) + }) + + t.Run("invalid", func(t *testing.T) { + history, err := getMaxHistory(map[string]string{"max_history": "invalid"}) + assert.Error(t, err) + assert.Zero(t, history) + }) + + t.Run("custom", func(t *testing.T) { + history, err := getMaxHistory(map[string]string{"max_history": "5"}) + assert.NoError(t, err) + assert.Equal(t, 5, history) + }) +} + type MockCheckService struct { checks []checks.Check } diff --git a/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go index fc5ff150a18..de250b4535e 100644 --- a/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go +++ b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go @@ -24,10 +24,11 @@ type Runner struct { // NewRunner creates a new Runner. func New(cfg app.Config) (app.Runnable, error) { // Read config - checkRegistry, ok := cfg.SpecificConfig.(checkregistry.CheckService) + specificConfig, ok := cfg.SpecificConfig.(checkregistry.AdvisorAppConfig) if !ok { return nil, fmt.Errorf("invalid config type") } + checkRegistry := specificConfig.CheckRegistry // Prepare storage client clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{}) diff --git a/pkg/registry/apps/advisor/register.go b/pkg/registry/apps/advisor/register.go index c989d82262e..352e364eff0 100644 --- a/pkg/registry/apps/advisor/register.go +++ b/pkg/registry/apps/advisor/register.go @@ -8,6 +8,7 @@ import ( advisorapp "github.com/grafana/grafana/apps/advisor/pkg/app" "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" "github.com/grafana/grafana/pkg/services/apiserver/builder/runner" + "github.com/grafana/grafana/pkg/setting" ) type AdvisorAppProvider struct { @@ -16,13 +17,19 @@ type AdvisorAppProvider struct { func RegisterApp( checkRegistry checkregistry.CheckService, + cfg *setting.Cfg, ) *AdvisorAppProvider { provider := &AdvisorAppProvider{} + pluginConfig := cfg.PluginSettings["grafana-advisor-app"] + specificConfig := checkregistry.AdvisorAppConfig{ + CheckRegistry: checkRegistry, + PluginConfig: pluginConfig, + } appCfg := &runner.AppBuilderConfig{ OpenAPIDefGetter: advisorv0alpha1.GetOpenAPIDefinitions, ManagedKinds: advisorapp.GetKinds(), Authorizer: advisorapp.GetAuthorizer(), - CustomConfig: any(checkRegistry), + CustomConfig: any(specificConfig), } provider.Provider = simple.NewAppProvider(apis.LocalManifest(), appCfg, advisorapp.New) return provider From d8b26b0a31db7ab0cccc7c48f5db11c2883e831d Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:39:57 +0100 Subject: [PATCH 595/894] Search: Search dashboards without a parent (#100615) * Search dashboards without a parent --- pkg/services/dashboards/service/dashboard_service.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index eea8dd574ad..d1f78e34cb4 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1690,6 +1690,16 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex } if len(query.FolderUIDs) > 0 { + // Grafana frontend issues a call to search for dashboards in "general" folder. General folder doesn't exists and + // should return all dashboards without a parent folder. + // We do something similar in the old sql search query https://github.com/grafana/grafana/blob/a58564a35efe8c05a21d8190b283af5bc0979d2a/pkg/services/sqlstore/searchstore/filters.go#L103 + for i := range query.FolderUIDs { + if query.FolderUIDs[i] == folder.GeneralFolderUID { + query.FolderUIDs[i] = "" + break + } + } + req := []*resource.Requirement{{ Key: resource.SEARCH_FIELD_FOLDER, Operator: string(selection.In), From dc3de1a1d57bda070e258111788af3c71055693b Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 14 Feb 2025 11:57:30 +0200 Subject: [PATCH 596/894] Badge: Add darkgrey color (#100699) --- packages/grafana-ui/src/components/Badge/Badge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Badge/Badge.tsx b/packages/grafana-ui/src/components/Badge/Badge.tsx index 9238c8a648c..73eca08b108 100644 --- a/packages/grafana-ui/src/components/Badge/Badge.tsx +++ b/packages/grafana-ui/src/components/Badge/Badge.tsx @@ -12,7 +12,7 @@ import { SkeletonComponent, attachSkeleton } from '../../utils/skeleton'; import { Icon } from '../Icon/Icon'; import { Tooltip } from '../Tooltip/Tooltip'; -export type BadgeColor = 'blue' | 'red' | 'green' | 'orange' | 'purple'; +export type BadgeColor = 'blue' | 'red' | 'green' | 'orange' | 'purple' | 'darkgrey'; export interface BadgeProps extends HTMLAttributes { text: React.ReactNode; From da66060f754e29467d9a653d8ac0242c107921cf Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 14 Feb 2025 11:58:42 +0200 Subject: [PATCH 597/894] Grafana/ui: Export UsersIndicator (#100698) grafana-ui: Export UsersIndicator --- packages/grafana-ui/src/components/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index b62e96627a0..a22b93a5bff 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -275,6 +275,7 @@ export { ButtonSelect } from './Dropdown/ButtonSelect'; export { Dropdown } from './Dropdown/Dropdown'; export { PluginSignatureBadge, type PluginSignatureBadgeProps } from './PluginSignatureBadge/PluginSignatureBadge'; export { UserIcon, type UserIconProps } from './UsersIndicator/UserIcon'; +export { UsersIndicator, type UsersIndicatorProps } from './UsersIndicator/UsersIndicator'; export { type UserView } from './UsersIndicator/types'; export { Avatar } from './UsersIndicator/Avatar'; // Export this until we've figured out a good approach to inline form styles. From f1b4678012ca5626c7e8acd9a47f880f3f51a4c6 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Fri, 14 Feb 2025 11:04:08 +0100 Subject: [PATCH 598/894] Alerting docs: update `Configure Webhook notifications` (#100650) * Alerting docs: update `Configure Webhook notifications` * fix typo * fix typo * Update docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md Co-authored-by: Matthew Jacobson * Update docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md Co-authored-by: Matthew Jacobson * fix typo * Add `Note` to configure either HTTP Basic Authentication or the Authorization request header * Use `inline` format for JSON keys --------- Co-authored-by: Matthew Jacobson --- .../integrations/webhook-notifier.md | 181 +++++++++++------- 1 file changed, 111 insertions(+), 70 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md index 6562b948095..1a7598891a1 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md @@ -28,13 +28,83 @@ refs: destination: /docs/grafana//alerting/configure-notifications/template-notifications/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/ + configure-contact-points: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/manage-contact-points/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/ --- -# Configure the webhook notifier for Alerting +# Configure webhook notifications -The webhook notification is a simple way to send information about a state change over HTTP to a custom endpoint. Using this notification you could integrate Grafana into a system of your choosing. +Use the webhook integration in contact points to send alert notifications to your webhook. -## Webhook JSON payload +The webhook integration is a flexible way to integrate alerts into your system. When a notification is triggered, it sends a JSON request with alert details and additional data to the webhook endpoint. + +## Configure webhook for a contact point + +To create a contact point with webhook integration, complete the following steps. + +1. Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**. +1. Click **+ Add contact point**. +1. Enter a name for the contact point. +1. From the **Integration** list, select **Webhook**. +1. In the **URL** field, copy in your Webhook URL. +1. (Optional) Configure [additional settings](#settings). +1. Click **Save contact point**. + +For more details on contact points, including how to test them and enable notifications, refer to [Configure contact points](ref:configure-contact-points). + +## Webhook settings + +| Option | Description | +| ------ | ---------------- | +| URL | The Webhook URL. | + +#### Optional settings + +| Option | Description | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| HTTP Method | Specifies the HTTP method to use: `POST` or `PUT`. | +| Basic Authentication Username | Username for HTTP Basic Authentication. | +| Basic Authentication Password | Password for HTTP Basic Authentication. | +| Authentication Header Scheme | Scheme for the `Authorization` Request Header. Default is `Bearer`. | +| Authentication Header Credentials | Credentials for the `Authorization` Request header. | +| Max Alerts | Maximum number of alerts to include in a notification. Any alerts exceeding this limit are ignored. `0` means no limit. | +| TLS | TLS configuration options, including CA certificate, client certificate, and client key. | + +{{< admonition type="note" >}} + +You can configure either HTTP Basic Authentication or the Authorization request header, but not both. + +{{< /admonition >}} + +#### Optional settings using templates + +Use the following settings to include custom data within the [JSON payload](#body). Both options support using [notification templates](ref:notification-templates). + +| Option | Description | +| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Title | Sends the value as a string in the `title` field of the [JSON payload](#body). Supports [notification templates](ref:notification-templates). | +| Message | Sends the value as a string in the `message` field of the [JSON payload](#body). Supports [notification templates](ref:notification-templates). | + +{{< admonition type="note" >}} +You can customize the `title` and `message` options to include custom messages and notification data using notification templates. These fields are always sent as strings in the JSON payload. + +However, you cannot customize the webhook data structure, such as adding or changing other JSON fields and HTTP headers, or sending data in a different format like XML. + +If you need to format these fields as JSON or modify other webhook request options, consider sending webhook notifications to a proxy server that adjusts the webhook request before forwarding it to the final destination. +{{< /admonition >}} + +#### Optional notification settings + +| Option | Description | +| ------------------------ | ------------------------------------------------------------------- | +| Disable resolved message | Enable this option to prevent notifications when an alert resolves. | + +## JSON payload + +The following example shows the payload of a webhook notification containing information about two firing alerts: ```json { @@ -106,76 +176,47 @@ The webhook notification is a simple way to send information about a state chang } ``` -## Webhook fields - ### Body -| Key | Type | Description | -| ----------------- | ------------------------- | ------------------------------------------------------------------------------- | -| receiver | string | Name of the webhook | -| status | string | Current status of the alert, `firing` or `resolved` | -| orgId | number | ID of the organization related to the payload | -| alerts | array of [alerts](#alert) | Alerts that are triggering | -| groupLabels | object | Labels that are used for grouping, map of string keys to string values | -| commonLabels | object | Labels that all alarms have in common, map of string keys to string values | -| commonAnnotations | object | Annotations that all alarms have in common, map of string keys to string values | -| externalURL | string | External URL to the Grafana instance sending this webhook | -| version | string | Version of the payload | -| groupKey | string | Key that is used for grouping | -| truncatedAlerts | number | Number of alerts that were truncated | -| title | string | Custom title | -| state | string | State of the alert group (either `alerting` or `ok`) | -| message | string | Custom message | +The JSON payload of webhook notifications includes the following key-value pairs: + +| Key | Type | Description | +| ------------------- | ------------------------- | -------------------------------------------------------------------------------- | +| `receiver` | string | Name of the contact point. | +| `status` | string | Current status of the alert, `firing` or `resolved`. | +| `orgId` | number | ID of the organization related to the payload. | +| `alerts` | array of [alerts](#alert) | Alerts that are triggering. | +| `groupLabels` | object | Labels that are used for grouping, map of string keys to string values. | +| `commonLabels` | object | Labels that all alarms have in common, map of string keys to string values. | +| `commonAnnotations` | object | Annotations that all alarms have in common, map of string keys to string values. | +| `externalURL` | string | External URL to the Grafana instance sending this webhook. | +| `version` | string | Version of the payload structure. | +| `groupKey` | string | Key that is used for grouping. | +| `truncatedAlerts` | number | Number of alerts that were truncated. | +| `state` | string | State of the alert group (either `alerting` or `ok`). | + +The following key-value pairs are also included in the JSON payload and can be configured in the [webhook settings using notification templates](#optional-settings-using-templates). + +| Key | Type | Description | +| --------- | ------ | -------------------------------------------------------------------------------------------------------------------- | +| `title` | string | Custom title. Configurable in [webhook settings using notification templates](#optional-settings-using-templates). | +| `message` | string | Custom message. Configurable in [webhook settings using notification templates](#optional-settings-using-templates). | ### Alert -| Key | Type | Description | -| ------------ | ------ | ---------------------------------------------------------------------------------- | -| status | string | Current status of the alert, `firing` or `resolved` | -| labels | object | Labels that are part of this alert, map of string keys to string values | -| annotations | object | Annotations that are part of this alert, map of string keys to string values | -| startsAt | string | Start time of the alert | -| endsAt | string | End time of the alert, default value when not resolved is `0001-01-01T00:00:00Z` | -| values | object | Values that triggered the current status | -| generatorURL | string | URL of the alert rule in the Grafana UI | -| fingerprint | string | The labels fingerprint, alarms with the same labels will have the same fingerprint | -| silenceURL | string | URL to silence the alert rule in the Grafana UI | -| dashboardURL | string | A link to the Grafana Dashboard if the alert has a Dashboard UID annotation | -| panelURL | string | A link to the panel if the alert has a Panel ID annotation | -| imageURL | string | URL of a screenshot of a panel assigned to the rule that created this notification | +The Alert object represents an alert included in the notification group, as provided by the [`alerts` field](#body). -{{< admonition type="note" >}} - -You can customize the `title` and `message` fields using [notification templates](ref:notification-templates). - -However, you cannot customize webhook data structure or format, including JSON fields or sending data in XML, nor can you change the webhook HTTP headers. - -{{< /admonition >}} - -## Procedure - -To create your Webhook integration in Grafana Alerting, complete the following steps. - -1. Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**. -1. Click **+ Add contact point**. -1. Enter a contact point name. -1. From the Integration list, select **Webhook**. -1. In the **URL** field, copy in your Webhook URL. -1. Click **Test** to check that your integration works. - - ** For Grafana Alertmanager only.** - -1. Click **Save contact point**. - -## Next steps - -The Webhook contact point is ready to receive alert notifications. - -To add this contact point to your alert, complete the following steps. - -1. In Grafana, navigate to **Alerting** > **Alert rules**. -1. Edit or create a new alert rule. -1. Scroll down to the **Configure labels and notifications** section. -1. Under Notifications, click **Select contact point**. -1. From the drop-down menu, select the previously created contact point. -1. **Click Save rule and exit**. +| Key | Type | Description | +| -------------- | ------ | ----------------------------------------------------------------------------------- | +| `status` | string | Current status of the alert, `firing` or `resolved`. | +| `labels` | object | Labels that are part of this alert, map of string keys to string values. | +| `annotations` | object | Annotations that are part of this alert, map of string keys to string values. | +| `startsAt` | string | Start time of the alert. | +| `endsAt` | string | End time of the alert, default value when not resolved is `0001-01-01T00:00:00Z`. | +| `values` | object | Values that triggered the current status. | +| `generatorURL` | string | URL of the alert rule in the Grafana UI. | +| `fingerprint` | string | The labels fingerprint, alarms with the same labels will have the same fingerprint. | +| `silenceURL` | string | URL to silence the alert rule in the Grafana UI. | +| `dashboardURL` | string | A link to the Grafana Dashboard if the alert has a Dashboard UID annotation. | +| `panelURL` | string | A link to the panel if the alert has a Panel ID annotation. | +| `imageURL` | string | URL of a screenshot of a panel assigned to the rule that created this notification. | From b1222be02e8f217836006946f155b2e2f45d8c68 Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Fri, 14 Feb 2025 11:18:59 +0100 Subject: [PATCH 599/894] unistore: add small buffer of watched events (#100431) * change log level * Add a small buffer when watching events --- pkg/storage/unified/search/bleve.go | 2 +- pkg/storage/unified/sql/backend.go | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 2b76bb97529..185e0388cd4 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -218,7 +218,7 @@ func (b *bleveBackend) cleanOldIndexes(dir string, skip string) { if err != nil { b.log.Error("Unable to remove old index folder", "directory", fpath, "error", err) } else { - b.log.Error("Removed old index folder", "directory", fpath) + b.log.Info("Removed old index folder", "directory", fpath) } } } diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 7a7d9990d6a..777164b9e2b 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -26,6 +26,7 @@ import ( const tracePrefix = "sql.resource." const defaultPollingInterval = 100 * time.Millisecond +const defaultWatchBufferSize = 100 // number of events to buffer in the watch stream type Backend interface { resource.StorageBackend @@ -37,6 +38,7 @@ type BackendOptions struct { DBProvider db.DBProvider Tracer trace.Tracer PollingInterval time.Duration + WatchBufferSize int } func NewBackend(opts BackendOptions) (Backend, error) { @@ -52,6 +54,9 @@ func NewBackend(opts BackendOptions) (Backend, error) { if pollingInterval == 0 { pollingInterval = defaultPollingInterval } + if opts.WatchBufferSize == 0 { + opts.WatchBufferSize = defaultWatchBufferSize + } return &backend{ done: ctx.Done(), cancel: cancel, @@ -59,6 +64,7 @@ func NewBackend(opts BackendOptions) (Backend, error) { tracer: opts.Tracer, dbProvider: opts.DBProvider, pollingInterval: pollingInterval, + watchBufferSize: opts.WatchBufferSize, batchLock: &batchLock{running: make(map[string]bool)}, }, nil } @@ -83,6 +89,7 @@ type backend struct { // watch streaming //stream chan *resource.WatchEvent pollingInterval time.Duration + watchBufferSize int } func (b *backend) Init(ctx context.Context) error { @@ -706,7 +713,7 @@ func (b *backend) WatchWriteEvents(ctx context.Context) (<-chan *resource.Writte return nil, fmt.Errorf("watch, get latest resource version: %w", err) } // Start the poller - stream := make(chan *resource.WrittenEvent) + stream := make(chan *resource.WrittenEvent, b.watchBufferSize) go b.poller(ctx, since, stream) return stream, nil } From cf2a7687e0bc388532b5b183ff0bb5e497b1ed58 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:26:29 +0000 Subject: [PATCH 600/894] Update `make docs` procedure (#100167) Co-authored-by: grafanabot Co-authored-by: Jack Baldry --- docs/docs.mk | 7 +++++++ docs/make-docs | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/docs/docs.mk b/docs/docs.mk index c0aae10ba50..68a00fcf6e0 100644 --- a/docs/docs.mk +++ b/docs/docs.mk @@ -120,3 +120,10 @@ update: ## Fetch the latest version of this Makefile and the `make-docs` script curl -s -LO https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/docs.mk curl -s -LO https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/make-docs chmod +x make-docs + +.PHONY: topic/% +topic/%: ## Create a topic from the Writers' Toolkit template. Specify the topic type as the target, for example, `make topic/task TOPIC_PATH=sources/my-new-topic.md`. +topic/%: + $(if $(TOPIC_PATH),,$(error "You must set the TOPIC_PATH variable to the path where the $(@F) topic will be created. For example: make $(@) TOPIC_PATH=sources/my-new-topic.md")) + mkdir -p $(dir $(TOPIC_PATH)) + curl -s -o $(TOPIC_PATH) https://raw.githubusercontent.com/grafana/writers-toolkit/refs/heads/main/docs/static/templates/$(@F)-template.md diff --git a/docs/make-docs b/docs/make-docs index e8111479d7c..a81ec7530f3 100755 --- a/docs/make-docs +++ b/docs/make-docs @@ -8,6 +8,12 @@ # [Semantic versioning](https://semver.org/) is used to help the reader identify the significance of changes. # Changes are relevant to this script and the support docs.mk GNU Make interface. # +# ## 8.5.0 (2025-02-13) +# +# ### Added +# +# - make topic/ TOPIC_PATH= target to create a new topic from the Writers' Toolkit templates. +# # ## 8.4.0 (2025-01-27) # # ### Fixed From 9d68c4f6653be7283176fed020b2274527d4f64e Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Fri, 14 Feb 2025 11:26:51 +0100 Subject: [PATCH 601/894] unified: allow customising the ProvideUnifiedStorageClient (#100704) * unified: allow customising the ProvideUnifiedStorageClient * fix go mod --- .../datamigrations/to_unified_storage.go | 17 +++++------ pkg/server/wire.go | 2 -- pkg/server/wireexts_oss.go | 3 ++ pkg/storage/unified/client.go | 28 ++++++++++--------- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go index 6b6673a9742..a7331cda28e 100644 --- a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go +++ b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go @@ -187,14 +187,15 @@ func promptYesNo(prompt string) (bool, error) { } func newUnifiedClient(cfg *setting.Cfg, sqlStore db.DB) (resource.ResourceClient, error) { - return unified.ProvideUnifiedStorageClient(cfg, - featuremgmt.WithFeatures(), // none?? - sqlStore, - tracing.NewNoopTracerService(), - prometheus.NewPedanticRegistry(), - authlib.FixedAccessClient(true), // always true! - nil, // document supplier - ) + return unified.ProvideUnifiedStorageClient(&unified.Options{ + Cfg: cfg, + Features: featuremgmt.WithFeatures(), // none?? + DB: sqlStore, + Tracer: tracing.NewNoopTracerService(), + Reg: prometheus.NewPedanticRegistry(), + Authzc: authlib.FixedAccessClient(true), // always true! + Docs: nil, // document supplier + }) } func newParquetClient(file *os.File) (resource.BatchStoreClient, error) { diff --git a/pkg/server/wire.go b/pkg/server/wire.go index f4e3f92e656..4525788869d 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -158,7 +158,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/unified" unifiedsearch "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" cloudmonitoring "github.com/grafana/grafana/pkg/tsdb/cloud-monitoring" @@ -214,7 +213,6 @@ var wireBasicSet = wire.NewSet( mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, - unified.ProvideUnifiedStorageClient, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*sdkhttpclient.Provider)), serverlock.ProvideService, diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 2ace4123b30..038a628f323 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -7,6 +7,7 @@ package server import ( "github.com/google/wire" + "github.com/grafana/grafana/pkg/storage/unified" search2 "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/infra/metrics" @@ -116,6 +117,8 @@ var wireExtsBasicSet = wire.NewSet( search2.ProvideDocumentBuilders, sandbox.ProvideService, wire.Bind(new(sandbox.Sandbox), new(*sandbox.Service)), + wire.Struct(new(unified.Options), "*"), + unified.ProvideUnifiedStorageClient, ) var wireExtsSet = wire.NewSet( diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index cd413c3a5d2..b8ecda9cbaf 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -39,29 +39,31 @@ func GetResourceClient(ctx context.Context) resource.ResourceClient { return pkgResourceClient } +type Options struct { + Cfg *setting.Cfg + Features featuremgmt.FeatureToggles + DB infraDB.DB + Tracer tracing.Tracer + Reg prometheus.Registerer + Authzc types.AccessClient + Docs resource.DocumentBuilderSupplier +} + // This adds a UnifiedStorage client into the wire dependency tree -func ProvideUnifiedStorageClient( - cfg *setting.Cfg, - features featuremgmt.FeatureToggles, - db infraDB.DB, - tracer tracing.Tracer, - reg prometheus.Registerer, - authzc types.AccessClient, - docs resource.DocumentBuilderSupplier, -) (resource.ResourceClient, error) { +func ProvideUnifiedStorageClient(opts *Options) (resource.ResourceClient, error) { // See: apiserver.ApplyGrafanaConfig(cfg, features, o) - apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver") + apiserverCfg := opts.Cfg.SectionWithEnvOverrides("grafana-apiserver") client, err := newClient(options.StorageOptions{ StorageType: options.StorageType(apiserverCfg.Key("storage_type").MustString(string(options.StorageTypeUnified))), - DataPath: apiserverCfg.Key("storage_path").MustString(filepath.Join(cfg.DataPath, "grafana-apiserver")), + DataPath: apiserverCfg.Key("storage_path").MustString(filepath.Join(opts.Cfg.DataPath, "grafana-apiserver")), Address: apiserverCfg.Key("address").MustString(""), // client address BlobStoreURL: apiserverCfg.Key("blob_url").MustString(""), - }, cfg, features, db, tracer, reg, authzc, docs) + }, opts.Cfg, opts.Features, opts.DB, opts.Tracer, opts.Reg, opts.Authzc, opts.Docs) if err == nil { // Used to get the folder stats client = federated.NewFederatedClient( client, // The original - legacysql.NewDatabaseProvider(db), + legacysql.NewDatabaseProvider(opts.DB), ) } From ba3a90d8fd8913d95cca522f732696a89b3b3d37 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 14 Feb 2025 11:38:38 +0100 Subject: [PATCH 602/894] Alerting: Fix loading states (#100641) --- .../features/alerting/unified/RuleViewer.tsx | 4 +-- .../alerting/unified/hooks/useCombinedRule.ts | 36 ++++++++++++------- .../unified/hooks/useIsRuleEditable.ts | 33 +++++++++++------ .../rule-editor/ExistingRuleEditor.tsx | 17 +++++---- 4 files changed, 59 insertions(+), 31 deletions(-) diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 9df84a41b18..43c38b699d0 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -36,7 +36,7 @@ const RuleViewer = (): JSX.Element => { }, [id]); // we then fetch the rule from the correct API endpoint(s) - const { loading, error, result: rule } = useCombinedRule({ ruleIdentifier: identifier, limitAlerts }); + const { loading, error, result: rule, uninitialized } = useCombinedRule({ ruleIdentifier: identifier, limitAlerts }); if (error) { return ( @@ -46,7 +46,7 @@ const RuleViewer = (): JSX.Element => { ); } - if (loading) { + if (loading || uninitialized) { return ( <> diff --git a/public/app/features/alerting/unified/hooks/useCombinedRule.ts b/public/app/features/alerting/unified/hooks/useCombinedRule.ts index 0841b794fb5..5d45c65137c 100644 --- a/public/app/features/alerting/unified/hooks/useCombinedRule.ts +++ b/public/app/features/alerting/unified/hooks/useCombinedRule.ts @@ -80,6 +80,7 @@ interface RequestState { result?: T; loading: boolean; error?: unknown; + uninitialized: boolean; } interface Props { @@ -99,6 +100,7 @@ export function useCombinedRule({ ruleIdentifier, limitAlerts }: Props): Request loading: isLoadingRuleLocation, error: ruleLocationError, result: ruleLocation, + uninitialized, } = useRuleLocation(ruleIdentifier); const { @@ -125,7 +127,12 @@ export function useCombinedRule({ ruleIdentifier, limitAlerts }: Props): Request const [ fetchRulerRuleGroup, - { currentData: rulerRuleGroup, isLoading: isLoadingRulerGroup, error: rulerRuleGroupError }, + { + currentData: rulerRuleGroup, + isLoading: isLoadingRulerGroup, + error: rulerRuleGroupError, + isUninitialized: ruleGroupUninitialized, + }, ] = alertRuleApi.endpoints.getRuleGroupForNamespace.useLazyQuery(); useEffect(() => { @@ -158,9 +165,10 @@ export function useCombinedRule({ ruleIdentifier, limitAlerts }: Props): Request }, [ruleIdentifier, ruleSourceName, promRuleNs, rulerRuleGroup, ruleSource, ruleLocation, namespaceName]); return { - loading: isLoadingDsFeatures || isLoadingPromRules || isLoadingRulerGroup, + loading: isLoadingDsFeatures || isLoadingPromRules || isLoadingRulerGroup || ruleGroupUninitialized, error: ruleLocationError ?? promRuleNsError ?? rulerRuleGroupError, result: rule, + uninitialized, }; } @@ -187,17 +195,19 @@ export function useRuleLocation(ruleIdentifier: RuleIdentifier): RequestState { @@ -297,9 +306,10 @@ export function useRuleWithLocation({ }, [ruleIdentifier, rulerRuleGroup, ruleSource, ruleLocation]); return { - loading: isLoadingRuleLocation || isLoadingDsFeatures || isLoadingRulerGroup || isUninitializedRulerGroup, + loading: isLoadingRuleLocation || isLoadingDsFeatures || isLoadingRulerGroup, error: ruleLocationError ?? rulerRuleGroupError, result: ruleWithLocation, + uninitialized, }; } diff --git a/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts b/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts index 50ca91b8188..bc5d8a182a2 100644 --- a/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts +++ b/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts @@ -16,19 +16,22 @@ interface ResultBag { } export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): ResultBag { - const { currentData: dsFeatures, isLoading } = featureDiscoveryApi.endpoints.discoverDsFeatures.useQuery({ - uid: getDatasourceAPIUid(rulesSourceName), - }); + const { currentData: dsFeatures, isLoading: loadingDataSourceFeatures } = + featureDiscoveryApi.endpoints.discoverDsFeatures.useQuery({ + uid: getDatasourceAPIUid(rulesSourceName), + }); const folderUID = rule && isGrafanaRulerRule(rule) ? rule.grafana_alert.namespace_uid : undefined; - const rulePermission = getRulesPermissions(rulesSourceName); - const { folder, loading } = useFolder(folderUID); + + const { folder, loading: loadingFolder } = useFolder(folderUID); if (!rule) { return { isEditable: false, isRemovable: false, loading: false }; } + const loading = loadingFolder || loadingDataSourceFeatures; + // Grafana rules can be edited if user can edit the folder they're in // When RBAC is disabled access to a folder is the only requirement for managing rules // When RBAC is enabled the appropriate alerting permissions need to be met @@ -39,13 +42,23 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): ); } - if (!folder) { - // Loading or invalid folder UID + // loading folder information + if (loadingFolder) { return { isRulerAvailable: true, isEditable: false, isRemovable: false, - loading, + loading: true, + }; + } + + // invalid folder UID + if (!folder) { + return { + isRulerAvailable: true, + isEditable: false, + isRemovable: false, + loading: false, }; } @@ -56,7 +69,7 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): isRulerAvailable: true, isEditable: canEditGrafanaRules, isRemovable: canRemoveGrafanaRules, - loading: loading || isLoading, + loading: loading, }; } @@ -69,6 +82,6 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): isRulerAvailable, isEditable: canEditCloudRules && isRulerAvailable, isRemovable: canRemoveCloudRules && isRulerAvailable, - loading: isLoading, + loading: loading, }; } diff --git a/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx index 7d1b9d92fcf..8bc21533774 100644 --- a/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx +++ b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx @@ -1,4 +1,5 @@ import { Alert, LoadingPlaceholder } from '@grafana/ui'; +import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { RuleIdentifier } from 'app/types/unified-alerting'; import { AlertWarning } from '../AlertWarning'; @@ -13,17 +14,21 @@ interface ExistingRuleEditorProps { } export function ExistingRuleEditor({ identifier }: ExistingRuleEditorProps) { + const ruleSourceName = ruleId.ruleIdentifierToRuleSourceName(identifier); + const { loading: loadingAlertRule, result: ruleWithLocation, error, + uninitialized, } = useRuleWithLocation({ ruleIdentifier: identifier }); - const ruleSourceName = ruleId.ruleIdentifierToRuleSourceName(identifier); - const { isEditable, loading: loadingEditable } = useIsRuleEditable(ruleSourceName, ruleWithLocation?.rule); - const loading = loadingAlertRule || loadingEditable; + // the loading of the editable state only happens once we've got a rule with location loaded, so we set it to true by default here + const loadingEditableState = Boolean(ruleWithLocation) ? loadingEditable : true; + const loading = loadingAlertRule || loadingEditableState || uninitialized; + const ruleNotFound = !Boolean(ruleWithLocation); if (loading) { return ; @@ -37,11 +42,11 @@ export function ExistingRuleEditor({ identifier }: ExistingRuleEditorProps) { ); } - if (!ruleWithLocation) { - return Sorry! This rule does not exist.; + if (ruleNotFound) { + return ; } - if (isEditable === false) { + if (isEditable === false && !loadingEditable) { return Sorry! You do not have permission to edit this rule.; } From d092998927ff0b094e78a1d3af559b95f422ede8 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Fri, 14 Feb 2025 12:05:03 +0100 Subject: [PATCH 603/894] Query Library: Move backend to enterprise (#100371) * Move files to enterprise * Remove last parts of QL api * Fix CODEOWNERS --- .github/CODEOWNERS | 2 +- pkg/apis/peakq/v0alpha1/doc.go | 6 - pkg/apis/peakq/v0alpha1/register.go | 52 - pkg/apis/peakq/v0alpha1/types.go | 32 - .../peakq/v0alpha1/zz_generated.deepcopy.go | 105 - .../peakq/v0alpha1/zz_generated.defaults.go | 19 - .../peakq/v0alpha1/zz_generated.openapi.go | 155 -- pkg/registry/apis/apis.go | 2 - pkg/registry/apis/peakq/register.go | 175 -- pkg/registry/apis/peakq/render.go | 107 - pkg/registry/apis/peakq/render_examples.go | 74 - .../apis/peakq/render_examples_test.go | 21 - pkg/registry/apis/wireset.go | 2 - .../peakq.grafana.app-v0alpha1.json | 2136 ----------------- pkg/tests/apis/openapi_test.go | 3 - pkg/tests/apis/peakq/peakq_test.go | 71 - .../apis/peakq/testdata/query-generate.yaml | 7 - public/app/core/reducers/root.ts | 2 - .../app/features/explore/spec/helper/mocks.ts | 20 + .../features/explore/spec/helper/setup.tsx | 11 +- .../helper}/testdata/identityDisplayList.ts | 0 .../spec/helper}/testdata/testQueryList.ts | 0 public/app/features/query-library/api/api.ts | 16 - .../query-library/api/endpoints.gen.ts | 475 ---- .../app/features/query-library/api/mappers.ts | 54 - .../app/features/query-library/api/mocks.ts | 13 - public/app/features/query-library/index.ts | 53 - public/app/features/query-library/types.ts | 42 - public/app/store/configureStore.ts | 2 - scripts/generate-rtk-apis.ts | 9 - 30 files changed, 31 insertions(+), 3635 deletions(-) delete mode 100644 pkg/apis/peakq/v0alpha1/doc.go delete mode 100644 pkg/apis/peakq/v0alpha1/register.go delete mode 100644 pkg/apis/peakq/v0alpha1/types.go delete mode 100644 pkg/apis/peakq/v0alpha1/zz_generated.deepcopy.go delete mode 100644 pkg/apis/peakq/v0alpha1/zz_generated.defaults.go delete mode 100644 pkg/apis/peakq/v0alpha1/zz_generated.openapi.go delete mode 100644 pkg/registry/apis/peakq/register.go delete mode 100644 pkg/registry/apis/peakq/render.go delete mode 100644 pkg/registry/apis/peakq/render_examples.go delete mode 100644 pkg/registry/apis/peakq/render_examples_test.go delete mode 100644 pkg/tests/apis/openapi_snapshots/peakq.grafana.app-v0alpha1.json delete mode 100644 pkg/tests/apis/peakq/peakq_test.go delete mode 100644 pkg/tests/apis/peakq/testdata/query-generate.yaml create mode 100644 public/app/features/explore/spec/helper/mocks.ts rename public/app/features/{query-library/api => explore/spec/helper}/testdata/identityDisplayList.ts (100%) rename public/app/features/{query-library/api => explore/spec/helper}/testdata/testQueryList.ts (100%) delete mode 100644 public/app/features/query-library/api/api.ts delete mode 100644 public/app/features/query-library/api/endpoints.gen.ts delete mode 100644 public/app/features/query-library/api/mappers.ts delete mode 100644 public/app/features/query-library/api/mocks.ts delete mode 100644 public/app/features/query-library/index.ts delete mode 100644 public/app/features/query-library/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f10a2d3b391..fc8c0e3187e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -515,7 +515,6 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/playlist/ @grafana/dashboards-squad /public/app/features/plugins/ @grafana/plugins-platform-frontend /public/app/features/profile/ @grafana/grafana-frontend-platform -/public/app/features/query-library/ @grafana/grafana-frontend-platform /public/app/features/runtime/ @ryantxu /public/app/features/query/ @grafana/dashboards-squad /public/app/features/sandbox/ @grafana/grafana-frontend-platform @@ -593,6 +592,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/explore/NodeGraph/ @grafana/observability-traces-and-profiling /public/app/features/explore/FlameGraph/ @grafana/observability-traces-and-profiling /public/app/features/explore/TraceView/ @grafana/observability-traces-and-profiling +/public/app/features/explore/QueryLibrary/ @grafana/grafana-frontend-platform /public/api-merged.json @grafana/grafana-backend-group /public/api-enterprise-spec.json @grafana/grafana-backend-group diff --git a/pkg/apis/peakq/v0alpha1/doc.go b/pkg/apis/peakq/v0alpha1/doc.go deleted file mode 100644 index 52a9e2be69c..00000000000 --- a/pkg/apis/peakq/v0alpha1/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// +k8s:deepcopy-gen=package -// +k8s:openapi-gen=true -// +k8s:defaulter-gen=TypeMeta -// +groupName=peakq.grafana.app - -package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" diff --git a/pkg/apis/peakq/v0alpha1/register.go b/pkg/apis/peakq/v0alpha1/register.go deleted file mode 100644 index e314355d107..00000000000 --- a/pkg/apis/peakq/v0alpha1/register.go +++ /dev/null @@ -1,52 +0,0 @@ -package v0alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - - "github.com/grafana/grafana/pkg/apimachinery/utils" -) - -const ( - GROUP = "peakq.grafana.app" - VERSION = "v0alpha1" - APIVERSION = GROUP + "/" + VERSION -) - -var QueryTemplateResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "querytemplates", "querytemplate", "QueryTemplate", - func() runtime.Object { return &QueryTemplate{} }, - func() runtime.Object { return &QueryTemplateList{} }, - utils.TableColumns{}, // default table converter -) - -var ( - // SchemeGroupVersion is group version used to register these objects - SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} - - // SchemaBuilder is used by standard codegen - SchemeBuilder runtime.SchemeBuilder - localSchemeBuilder = &SchemeBuilder - AddToScheme = localSchemeBuilder.AddToScheme -) - -func init() { - localSchemeBuilder.Register(addKnownTypes) -} - -// Adds the list of known types to the given scheme. -func addKnownTypes(scheme *runtime.Scheme) error { - scheme.AddKnownTypes(SchemeGroupVersion, - &QueryTemplate{}, - &QueryTemplateList{}, - &RenderedQuery{}, - ) - metav1.AddToGroupVersion(scheme, SchemeGroupVersion) - return nil -} - -// Resource takes an unqualified resource and returns a Group qualified GroupResource -func Resource(resource string) schema.GroupResource { - return SchemeGroupVersion.WithResource(resource).GroupResource() -} diff --git a/pkg/apis/peakq/v0alpha1/types.go b/pkg/apis/peakq/v0alpha1/types.go deleted file mode 100644 index 0c67564df35..00000000000 --- a/pkg/apis/peakq/v0alpha1/types.go +++ /dev/null @@ -1,32 +0,0 @@ -package v0alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template" -) - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type QueryTemplate struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec template.QueryTemplate `json:"spec,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type QueryTemplateList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []QueryTemplate `json:"items,omitempty"` -} - -// Dummy object that represents a real query object -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type RenderedQuery struct { - metav1.TypeMeta `json:",inline"` - - // +listType=atomic - Targets []template.Target `json:"targets,omitempty"` -} diff --git a/pkg/apis/peakq/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/peakq/v0alpha1/zz_generated.deepcopy.go deleted file mode 100644 index 6f50c376eb6..00000000000 --- a/pkg/apis/peakq/v0alpha1/zz_generated.deepcopy.go +++ /dev/null @@ -1,105 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by deepcopy-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - template "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template" - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *QueryTemplate) DeepCopyInto(out *QueryTemplate) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QueryTemplate. -func (in *QueryTemplate) DeepCopy() *QueryTemplate { - if in == nil { - return nil - } - out := new(QueryTemplate) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *QueryTemplate) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *QueryTemplateList) DeepCopyInto(out *QueryTemplateList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]QueryTemplate, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QueryTemplateList. -func (in *QueryTemplateList) DeepCopy() *QueryTemplateList { - if in == nil { - return nil - } - out := new(QueryTemplateList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *QueryTemplateList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RenderedQuery) DeepCopyInto(out *RenderedQuery) { - *out = *in - out.TypeMeta = in.TypeMeta - if in.Targets != nil { - in, out := &in.Targets, &out.Targets - *out = make([]template.Target, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RenderedQuery. -func (in *RenderedQuery) DeepCopy() *RenderedQuery { - if in == nil { - return nil - } - out := new(RenderedQuery) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RenderedQuery) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} diff --git a/pkg/apis/peakq/v0alpha1/zz_generated.defaults.go b/pkg/apis/peakq/v0alpha1/zz_generated.defaults.go deleted file mode 100644 index 238fc2f4edc..00000000000 --- a/pkg/apis/peakq/v0alpha1/zz_generated.defaults.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by defaulter-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// RegisterDefaults adds defaulters functions to the given scheme. -// Public to allow building arbitrary schemes. -// All generated defaulters are covering - they call all nested defaulters. -func RegisterDefaults(scheme *runtime.Scheme) error { - return nil -} diff --git a/pkg/apis/peakq/v0alpha1/zz_generated.openapi.go b/pkg/apis/peakq/v0alpha1/zz_generated.openapi.go deleted file mode 100644 index adb1e72521e..00000000000 --- a/pkg/apis/peakq/v0alpha1/zz_generated.openapi.go +++ /dev/null @@ -1,155 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by openapi-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - common "k8s.io/kube-openapi/pkg/common" - spec "k8s.io/kube-openapi/pkg/validation/spec" -) - -func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { - return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplate": schema_pkg_apis_peakq_v0alpha1_QueryTemplate(ref), - "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateList": schema_pkg_apis_peakq_v0alpha1_QueryTemplateList(ref), - "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.RenderedQuery": schema_pkg_apis_peakq_v0alpha1_RenderedQuery(ref), - } -} - -func schema_pkg_apis_peakq_v0alpha1_QueryTemplate(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.QueryTemplate"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.QueryTemplate", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_peakq_v0alpha1_QueryTemplateList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplate"), - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplate", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_peakq_v0alpha1_RenderedQuery(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Dummy object that represents a real query object", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "targets": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-type": "atomic", - }, - }, - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Target"), - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Target"}, - } -} diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index b8676893099..0d3327725ab 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/iam" - "github.com/grafana/grafana/pkg/registry/apis/peakq" "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/scope" @@ -31,7 +30,6 @@ func ProvideRegistryServiceSink( _ *featuretoggle.FeatureFlagAPIBuilder, _ *datasource.DataSourceAPIBuilder, _ *folders.FolderAPIBuilder, - _ *peakq.PeakQAPIBuilder, _ *iam.IdentityAccessManagementAPIBuilder, _ *scope.ScopeAPIBuilder, _ *query.QueryAPIBuilder, diff --git a/pkg/registry/apis/peakq/register.go b/pkg/registry/apis/peakq/register.go deleted file mode 100644 index 7edcb93bd7f..00000000000 --- a/pkg/registry/apis/peakq/register.go +++ /dev/null @@ -1,175 +0,0 @@ -package peakq - -import ( - "github.com/prometheus/client_golang/prometheus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/rest" - genericapiserver "k8s.io/apiserver/pkg/server" - "k8s.io/kube-openapi/pkg/common" - "k8s.io/kube-openapi/pkg/spec3" - "k8s.io/kube-openapi/pkg/validation/spec" - - peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" - "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/services/featuremgmt" -) - -var _ builder.APIGroupBuilder = (*PeakQAPIBuilder)(nil) - -// This is used just so wire has something unique to return -type PeakQAPIBuilder struct{} - -func NewPeakQAPIBuilder() *PeakQAPIBuilder { - return &PeakQAPIBuilder{} -} - -func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, reg prometheus.Registerer) *PeakQAPIBuilder { - if !featuremgmt.AnyEnabled(features, - featuremgmt.FlagQueryService, - featuremgmt.FlagQueryLibrary, - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - return nil // skip registration unless explicitly added (or all experimental are added) - } - builder := NewPeakQAPIBuilder() - apiregistration.RegisterAPI(builder) - return builder -} - -func (b *PeakQAPIBuilder) GetAuthorizer() authorizer.Authorizer { - return nil // default authorizer is fine -} - -func (b *PeakQAPIBuilder) GetGroupVersion() schema.GroupVersion { - return peakq.SchemeGroupVersion -} - -func (b *PeakQAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { - gv := peakq.SchemeGroupVersion - err := peakq.AddToScheme(scheme) - if err != nil { - return err - } - - // Link this version to the internal representation. - // This is used for server-side-apply (PATCH), and avoids the error: - // "no kind is registered for the type" - // addKnownTypes(scheme, schema.GroupVersion{ - // Group: peakq.GROUP, - // Version: runtime.APIVersionInternal, - // }) - metav1.AddToGroupVersion(scheme, gv) - return scheme.SetVersionPriority(gv) -} - -func (b *PeakQAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { - resourceInfo := peakq.QueryTemplateResourceInfo - storage := map[string]rest.Storage{} - - peakqStorage, err := grafanaregistry.NewRegistryStore(opts.Scheme, resourceInfo, opts.OptsGetter) - if err != nil { - return err - } - - storage[resourceInfo.StoragePath()] = peakqStorage - storage[resourceInfo.StoragePath("render")] = &renderREST{ - getter: peakqStorage, - } - - apiGroupInfo.VersionedResourcesStorageMap[peakq.VERSION] = storage - return nil -} - -func (b *PeakQAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return peakq.GetOpenAPIDefinitions -} - -// NOT A GREAT APPROACH... BUT will make a UI for statically defined -func (b *PeakQAPIBuilder) GetAPIRoutes() *builder.APIRoutes { - defs := peakq.GetOpenAPIDefinitions(func(path string) spec.Ref { return spec.Ref{} }) - renderedQuerySchema := defs["github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.RenderedQuery"].Schema - queryTemplateSpecSchema := defs["github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateSpec"].Schema - - params := []*spec3.Parameter{ - { - ParameterProps: spec3.ParameterProps{ - // Arbitrary name. It won't appear in the request URL, - // but will be used in code generated from this OAS spec - Name: "variables", - In: "query", - Schema: spec.MapProperty(spec.ArrayProperty(spec.StringProperty())), - Style: "form", - Explode: true, - Description: "Each variable is prefixed with var-{variable}={value}", - Example: map[string][]string{ - "var-metricName": {"up"}, - "var-another": {"first", "second"}, - }, - }, - }, - } - return &builder.APIRoutes{ - Root: []builder.APIRouteHandler{ - { - Path: "render", - Spec: &spec3.PathProps{ - Summary: "an example at the root level", - Description: "longer description here?", - Post: &spec3.Operation{ - OperationProps: spec3.OperationProps{ - Parameters: params, - RequestBody: &spec3.RequestBody{ - RequestBodyProps: spec3.RequestBodyProps{ - Content: map[string]*spec3.MediaType{ - "application/json": { - MediaTypeProps: spec3.MediaTypeProps{ - Schema: &queryTemplateSpecSchema, - // Example: basicTemplateSpec, - Examples: map[string]*spec3.Example{ - "test": { - ExampleProps: spec3.ExampleProps{ - Summary: "hello", - Value: basicTemplateSpec, - }, - }, - "test2": { - ExampleProps: spec3.ExampleProps{ - Summary: "hello2", - Value: basicTemplateSpec, - }, - }, - }, - }, - }, - }, - }, - }, - Responses: &spec3.Responses{ - ResponsesProps: spec3.ResponsesProps{ - StatusCodeResponses: map[int]*spec3.Response{ - 200: { - ResponseProps: spec3.ResponseProps{ - Description: "OK", - Content: map[string]*spec3.MediaType{ - "application/json": { - MediaTypeProps: spec3.MediaTypeProps{ - Schema: &renderedQuerySchema, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - Handler: renderPOSTHandler, - }, - }, - } -} diff --git a/pkg/registry/apis/peakq/render.go b/pkg/registry/apis/peakq/render.go deleted file mode 100644 index fd0791b4905..00000000000 --- a/pkg/registry/apis/peakq/render.go +++ /dev/null @@ -1,107 +0,0 @@ -package peakq - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/rest" - - peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" - "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template" -) - -type renderREST struct { - getter rest.Getter -} - -var _ = rest.Connecter(&renderREST{}) - -func (r *renderREST) New() runtime.Object { - return &peakq.RenderedQuery{} -} - -func (r *renderREST) Destroy() { -} - -func (r *renderREST) ConnectMethods() []string { - return []string{"GET"} -} - -func (r *renderREST) NewConnectOptions() (runtime.Object, bool, string) { - return nil, false, "" // true means you can use the trailing path as a variable -} - -func (r *renderREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - obj, err := r.getter.Get(ctx, name, &v1.GetOptions{}) - if err != nil { - return nil, err - } - t, ok := obj.(*peakq.QueryTemplate) - if !ok { - return nil, fmt.Errorf("expected template") - } - - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - input, err := makeVarMapFromParams(req.URL.Query()) - if err != nil { - responder.Error(err) - return - } - out, err := template.RenderTemplate(t.Spec, input) - if err != nil { - responder.Error(fmt.Errorf("failed to render: %w", err)) - return - } - responder.Object(http.StatusOK, &peakq.RenderedQuery{ - Targets: out, - }) - }), nil -} - -func renderPOSTHandler(w http.ResponseWriter, req *http.Request) { - input, err := makeVarMapFromParams(req.URL.Query()) - if err != nil { - _, _ = w.Write([]byte("ERROR: " + err.Error())) - w.WriteHeader(500) - return - } - - var qT peakq.QueryTemplate - err = json.NewDecoder(req.Body).Decode(&qT.Spec) - if err != nil { - _, _ = w.Write([]byte("ERROR: " + err.Error())) - w.WriteHeader(500) - return - } - results, err := template.RenderTemplate(qT.Spec, input) - if err != nil { - _, _ = w.Write([]byte("ERROR: " + err.Error())) - w.WriteHeader(500) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(peakq.RenderedQuery{ - Targets: results, - }) -} - -// Replicate the grafana dashboard URL syntax -// &var-abc=1&var=abc=2&var-xyz=3... -func makeVarMapFromParams(v url.Values) (map[string][]string, error) { - input := make(map[string][]string, len(v)) - for key, vals := range v { - if !strings.HasPrefix(key, "var-") { - continue - } - input[key[4:]] = vals - } - return input, nil -} diff --git a/pkg/registry/apis/peakq/render_examples.go b/pkg/registry/apis/peakq/render_examples.go deleted file mode 100644 index 0d78ec007eb..00000000000 --- a/pkg/registry/apis/peakq/render_examples.go +++ /dev/null @@ -1,74 +0,0 @@ -package peakq - -import ( - "github.com/grafana/grafana-plugin-sdk-go/data" - apidata "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" - - "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template" -) - -var basicTemplateSpec = template.QueryTemplate{ - Title: "Test", - Variables: []template.TemplateVariable{ - { - Key: "metricName", - DefaultValues: []string{`down`}, - }, - }, - Targets: []template.Target{ - { - DataType: data.FrameTypeUnknown, - //DataTypeVersion: data.FrameTypeVersion{0, 0}, - Variables: map[string][]template.VariableReplacement{ - "metricName": { - { - Path: "$.expr", - Position: &template.Position{ - Start: 0, - End: 10, - }, - }, - { - Path: "$.expr", - Position: &template.Position{ - Start: 13, - End: 23, - }, - }, - }, - }, - - Properties: apidata.NewDataQuery(map[string]any{ - "refId": "A", // TODO: Set when Where? - "datasource": map[string]any{ - "type": "prometheus", - "uid": "foo", // TODO: Probably a default templating thing to set this. - }, - "editorMode": "builder", - "expr": "metricName + metricName + 42", - "instant": true, - "range": false, - "exemplar": false, - }), - }, - }, -} - -var basicTemplateRenderedTargets = []template.Target{ - { - DataType: data.FrameTypeUnknown, - //DataTypeVersion: data.FrameTypeVersion{0, 0}, - Properties: apidata.NewDataQuery(map[string]any{ - "refId": "A", // TODO: Set when Where? - "datasource": map[string]any{ - "type": "prometheus", - "uid": "foo", // TODO: Probably a default templating thing to set this. - }, - "editorMode": "builder", - "expr": "up + up + 42", - "instant": true, - "range": false, - "exemplar": false, - }), - }, -} diff --git a/pkg/registry/apis/peakq/render_examples_test.go b/pkg/registry/apis/peakq/render_examples_test.go deleted file mode 100644 index 1b4b2d53a8f..00000000000 --- a/pkg/registry/apis/peakq/render_examples_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package peakq - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template" -) - -func TestRender(t *testing.T) { - rT, err := template.RenderTemplate(basicTemplateSpec, map[string][]string{"metricName": {"up"}}) - require.NoError(t, err) - require.Equal(t, - basicTemplateRenderedTargets[0].Properties.GetString("expr"), - rT[0].Properties.GetString("expr")) - b, _ := json.MarshalIndent(basicTemplateSpec, "", " ") - fmt.Println(string(b)) -} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 8c4ec829b3b..22734abadbb 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/iam" - "github.com/grafana/grafana/pkg/registry/apis/peakq" "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/scope" @@ -40,7 +39,6 @@ var WireSet = wire.NewSet( datasource.RegisterAPIService, folders.RegisterAPIService, iam.RegisterAPIService, - peakq.RegisterAPIService, provisioning.RegisterAPIService, service.RegisterAPIService, query.RegisterAPIService, diff --git a/pkg/tests/apis/openapi_snapshots/peakq.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/peakq.grafana.app-v0alpha1.json deleted file mode 100644 index 83f3b28ee14..00000000000 --- a/pkg/tests/apis/openapi_snapshots/peakq.grafana.app-v0alpha1.json +++ /dev/null @@ -1,2136 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "peakq.grafana.app/v0alpha1" - }, - "paths": { - "/apis/peakq.grafana.app/v0alpha1/": { - "get": { - "tags": [ - "API Discovery" - ], - "description": "Describe the available kubernetes resources", - "operationId": "getAPIResources", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" - } - } - } - } - } - } - }, - "/apis/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates": { - "get": { - "tags": [ - "QueryTemplate" - ], - "description": "list or watch objects of kind QueryTemplate", - "operationId": "listQueryTemplate", - "parameters": [ - { - "name": "allowWatchBookmarks", - "in": "query", - "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "continue", - "in": "query", - "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "labelSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "limit", - "in": "query", - "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "resourceVersion", - "in": "query", - "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersionMatch", - "in": "query", - "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "sendInitialEvents", - "in": "query", - "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "timeoutSeconds", - "in": "query", - "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "watch", - "in": "query", - "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" - } - }, - "application/json;stream=watch": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" - } - }, - "application/vnd.kubernetes.protobuf;stream=watch": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" - } - } - } - } - }, - "x-kubernetes-action": "list", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "QueryTemplate" - } - }, - "post": { - "tags": [ - "QueryTemplate" - ], - "description": "create a QueryTemplate", - "operationId": "createQueryTemplate", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - } - } - }, - "202": { - "description": "Accepted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - } - } - } - }, - "x-kubernetes-action": "post", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "QueryTemplate" - } - }, - "delete": { - "tags": [ - "QueryTemplate" - ], - "description": "delete collection of QueryTemplate", - "operationId": "deletecollectionQueryTemplate", - "parameters": [ - { - "name": "continue", - "in": "query", - "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "gracePeriodSeconds", - "in": "query", - "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "ignoreStoreReadErrorWithClusterBreakingPotential", - "in": "query", - "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "labelSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "limit", - "in": "query", - "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "orphanDependents", - "in": "query", - "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "propagationPolicy", - "in": "query", - "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersion", - "in": "query", - "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersionMatch", - "in": "query", - "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "sendInitialEvents", - "in": "query", - "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "timeoutSeconds", - "in": "query", - "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", - "schema": { - "type": "integer", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - } - } - } - }, - "x-kubernetes-action": "deletecollection", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "QueryTemplate" - } - }, - "parameters": [ - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ] - }, - "/apis/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates/{name}": { - "get": { - "tags": [ - "QueryTemplate" - ], - "description": "read the specified QueryTemplate", - "operationId": "getQueryTemplate", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - } - } - } - }, - "x-kubernetes-action": "get", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "QueryTemplate" - } - }, - "put": { - "tags": [ - "QueryTemplate" - ], - "description": "replace the specified QueryTemplate", - "operationId": "replaceQueryTemplate", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - } - } - } - }, - "x-kubernetes-action": "put", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "QueryTemplate" - } - }, - "delete": { - "tags": [ - "QueryTemplate" - ], - "description": "delete a QueryTemplate", - "operationId": "deleteQueryTemplate", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "gracePeriodSeconds", - "in": "query", - "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "ignoreStoreReadErrorWithClusterBreakingPotential", - "in": "query", - "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "orphanDependents", - "in": "query", - "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "propagationPolicy", - "in": "query", - "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - } - } - }, - "202": { - "description": "Accepted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - } - } - } - }, - "x-kubernetes-action": "delete", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "QueryTemplate" - } - }, - "patch": { - "tags": [ - "QueryTemplate" - ], - "description": "partially update the specified QueryTemplate", - "operationId": "updateQueryTemplate", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "force", - "in": "query", - "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/apply-patch+yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/strategic-merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - } - } - } - }, - "x-kubernetes-action": "patch", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "QueryTemplate" - } - }, - "parameters": [ - { - "name": "name", - "in": "path", - "description": "name of the QueryTemplate", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ] - }, - "/apis/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates/{name}/render": { - "get": { - "tags": [ - "QueryTemplate" - ], - "description": "connect GET requests to render of QueryTemplate", - "operationId": "getQueryTemplateRender", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "type": "string" - } - } - } - } - }, - "x-kubernetes-action": "connect", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "RenderedQuery" - } - }, - "parameters": [ - { - "name": "name", - "in": "path", - "description": "name of the RenderedQuery", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - } - ] - }, - "/apis/peakq.grafana.app/v0alpha1/querytemplates": { - "get": { - "tags": [ - "QueryTemplate" - ], - "description": "list or watch objects of kind QueryTemplate", - "operationId": "listQueryTemplateForAllNamespaces", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" - } - }, - "application/json;stream=watch": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" - } - }, - "application/vnd.kubernetes.protobuf;stream=watch": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" - } - } - } - } - }, - "x-kubernetes-action": "list", - "x-kubernetes-group-version-kind": { - "group": "peakq.grafana.app", - "version": "v0alpha1", - "kind": "QueryTemplate" - } - }, - "parameters": [ - { - "name": "allowWatchBookmarks", - "in": "query", - "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "continue", - "in": "query", - "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "labelSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "limit", - "in": "query", - "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersion", - "in": "query", - "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersionMatch", - "in": "query", - "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "sendInitialEvents", - "in": "query", - "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "timeoutSeconds", - "in": "query", - "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "watch", - "in": "query", - "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ] - }, - "/apis/peakq.grafana.app/v0alpha1/render": { - "summary": "an example at the root level", - "description": "longer description here?", - "post": { - "parameters": [ - { - "name": "variables", - "in": "query", - "description": "Each variable is prefixed with var-{variable}={value}", - "style": "form", - "explode": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "var-another": [ - "first", - "second" - ], - "var-metricName": [ - "up" - ] - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {}, - "examples": { - "test": { - "summary": "hello", - "value": { - "title": "Test", - "vars": [ - { - "key": "metricName", - "defaultValues": [ - "down" - ] - } - ], - "targets": [ - { - "variables": { - "metricName": [ - { - "path": "$.expr", - "position": { - "start": 0, - "end": 10 - } - }, - { - "path": "$.expr", - "position": { - "start": 13, - "end": 23 - } - } - ] - }, - "properties": { - "refId": "A", - "datasource": { - "type": "prometheus", - "uid": "foo" - }, - "instant": true, - "range": false, - "exemplar": false, - "editorMode": "builder", - "expr": "metricName + metricName + 42" - } - } - ] - } - }, - "test2": { - "summary": "hello2", - "value": { - "title": "Test", - "vars": [ - { - "key": "metricName", - "defaultValues": [ - "down" - ] - } - ], - "targets": [ - { - "variables": { - "metricName": [ - { - "path": "$.expr", - "position": { - "start": 0, - "end": 10 - } - }, - { - "path": "$.expr", - "position": { - "start": 13, - "end": 23 - } - } - ] - }, - "properties": { - "refId": "A", - "datasource": { - "type": "prometheus", - "uid": "foo" - }, - "editorMode": "builder", - "expr": "metricName + metricName + 42", - "instant": true, - "range": false, - "exemplar": false - } - } - ] - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "description": "Dummy object that represents a real query object", - "type": "object", - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "targets": { - "type": "array", - "items": { - "default": {} - }, - "x-kubernetes-list-type": "atomic" - } - } - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataQuery": { - "description": "Generic query properties", - "type": "object", - "properties": { - "datasource": { - "description": "The datasource", - "type": "object", - "required": [ - "type" - ], - "properties": { - "apiVersion": { - "description": "The apiserver version", - "type": "string" - }, - "type": { - "description": "The datasource plugin type", - "type": "string" - }, - "uid": { - "description": "Datasource UID (NOTE: name in k8s)", - "type": "string" - } - }, - "additionalProperties": false - }, - "hide": { - "description": "true if query is disabled (ie should not be returned to the dashboard)\nNOTE: this does not always imply that the query should not be executed since\nthe results from a hidden query may be used as the input to other queries (SSE etc)", - "type": "boolean" - }, - "intervalMs": { - "description": "Interval is the suggested duration between time points in a time series query.\nNOTE: the values for intervalMs is not saved in the query model. It is typically calculated\nfrom the interval required to fill a pixels in the visualization", - "type": "number" - }, - "maxDataPoints": { - "description": "MaxDataPoints is the maximum number of data points that should be returned from a time series query.\nNOTE: the values for maxDataPoints is not saved in the query model. It is typically calculated\nfrom the number of pixels visible in a visualization", - "type": "integer" - }, - "queryType": { - "description": "QueryType is an optional identifier for the type of query.\nIt can be used to distinguish different types of queries.", - "type": "string" - }, - "refId": { - "description": "RefID is the unique identifier of the query, set by the frontend call.", - "type": "string" - }, - "resultAssertions": { - "description": "Optionally define expected query result behavior", - "type": "object", - "required": [ - "typeVersion" - ], - "properties": { - "maxFrames": { - "description": "Maximum frame count", - "type": "integer" - }, - "type": { - "description": "Type asserts that the frame matches a known type structure.\n\n\nPossible enum values:\n - `\"\"` \n - `\"timeseries-wide\"` \n - `\"timeseries-long\"` \n - `\"timeseries-many\"` \n - `\"timeseries-multi\"` \n - `\"directory-listing\"` \n - `\"table\"` \n - `\"numeric-wide\"` \n - `\"numeric-multi\"` \n - `\"numeric-long\"` \n - `\"log-lines\"` ", - "type": "string", - "enum": [ - "", - "timeseries-wide", - "timeseries-long", - "timeseries-many", - "timeseries-multi", - "directory-listing", - "table", - "numeric-wide", - "numeric-multi", - "numeric-long", - "log-lines" - ], - "x-enum-description": {} - }, - "typeVersion": { - "description": "TypeVersion is the version of the Type property. Versions greater than 0.0 correspond to the dataplane\ncontract documentation https://grafana.github.io/dataplane/contract/.", - "type": "array", - "maxItems": 2, - "minItems": 2, - "items": { - "type": "integer" - } - } - }, - "additionalProperties": false - }, - "timeRange": { - "description": "TimeRange represents the query range\nNOTE: unlike generic /ds/query, we can now send explicit time values in each query\nNOTE: the values for timeRange are not saved in a dashboard, they are constructed on the fly", - "type": "object", - "required": [ - "from", - "to" - ], - "properties": { - "from": { - "description": "From is the start time of the query.", - "type": "string", - "default": "now-6h", - "examples": [ - "now-1h" - ] - }, - "to": { - "description": "To is the end time of the query.", - "type": "string", - "default": "now", - "examples": [ - "now" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": true, - "$schema": "https://json-schema.org/draft-04/schema" - }, - "com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured": { - "type": "object", - "additionalProperties": true, - "x-kubernetes-preserve-unknown-fields": true - }, - "com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate": { - "type": "object", - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" - } - ] - }, - "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.QueryTemplate" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "peakq.grafana.app", - "kind": "QueryTemplate", - "version": "v0alpha1" - } - ] - }, - "com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList": { - "type": "object", - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "items": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" - } - ] - } - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "peakq.grafana.app", - "kind": "QueryTemplateList", - "version": "v0alpha1" - } - ] - }, - "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Position": { - "description": "Position is where to do replacement in the targets during render.", - "type": "object", - "required": [ - "start", - "end" - ], - "properties": { - "end": { - "description": "End is the byte offset of the end of the variable.", - "type": "integer", - "format": "int64", - "default": 0 - }, - "start": { - "description": "Start is the byte offset within TargetKey's property of the variable. It is the start location for replacements).", - "type": "integer", - "format": "int64", - "default": 0 - } - } - }, - "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.QueryTemplate": { - "type": "object", - "required": [ - "targets" - ], - "properties": { - "description": { - "description": "Longer description for why it is interesting", - "type": "string" - }, - "targets": { - "description": "Output variables", - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Target" - } - ] - }, - "x-kubernetes-list-type": "set" - }, - "title": { - "description": "A display name", - "type": "string" - }, - "vars": { - "description": "The variables that can be used to render", - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.TemplateVariable" - } - ] - }, - "x-kubernetes-list-map-keys": [ - "key" - ], - "x-kubernetes-list-type": "map" - } - } - }, - "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Target": { - "type": "object", - "required": [ - "variables", - "properties" - ], - "properties": { - "dataType": { - "description": "DataType is the returned Dataplane type from the query.", - "type": "string" - }, - "properties": { - "description": "Query target", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataQuery" - } - ] - }, - "variables": { - "description": "Variables that will be replaced in the query", - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.VariableReplacement" - } - ] - } - } - } - } - }, - "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.TemplateVariable": { - "description": "TemplateVariable is the definition of a variable that will be interpolated in targets.", - "type": "object", - "required": [ - "key" - ], - "properties": { - "defaultValues": { - "description": "DefaultValue is the value to be used when there is no selected value during render.", - "type": "array", - "items": { - "type": "string", - "default": "" - }, - "x-kubernetes-list-type": "atomic" - }, - "key": { - "description": "Key is the name of the variable.", - "type": "string", - "default": "" - }, - "valueListDefinition": { - "description": "ValueListDefinition is the object definition used by the FE to get a list of possible values to select for render.", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" - } - ] - } - } - }, - "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.VariableReplacement": { - "description": "QueryVariable is the definition of a variable that will be interpolated in targets.", - "type": "object", - "required": [ - "path" - ], - "properties": { - "format": { - "description": "How values should be interpolated\n\nPossible enum values:\n - `\"csv\"` Formats variables with multiple values as a comma-separated string.\n - `\"doublequote\"` Formats single- and multi-valued variables into a comma-separated string\n - `\"json\"` Formats variables with multiple values as a comma-separated string.\n - `\"pipe\"` Formats variables with multiple values into a pipe-separated string.\n - `\"raw\"` Formats variables with multiple values into comma-separated string. This is the default behavior when no format is specified\n - `\"singlequote\"` Formats single- and multi-valued variables into a comma-separated string", - "type": "string", - "enum": [ - "csv", - "doublequote", - "json", - "pipe", - "raw", - "singlequote" - ] - }, - "path": { - "description": "Path is the location of the property within a target. The format for this is not figured out yet (Maybe JSONPath?). Idea: [\"string\", int, \"string\"] where int indicates array offset", - "type": "string", - "default": "" - }, - "position": { - "description": "Positions is a list of where to perform the interpolation within targets during render. The first string is the Idx of the target as a string, since openAPI does not support ints as map keys", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Position" - } - ] - } - } - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { - "description": "APIResource specifies the name of a resource and whether it is namespaced.", - "type": "object", - "required": [ - "name", - "singularName", - "namespaced", - "kind", - "verbs" - ], - "properties": { - "categories": { - "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", - "type": "array", - "items": { - "type": "string", - "default": "" - }, - "x-kubernetes-list-type": "atomic" - }, - "group": { - "description": "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", - "type": "string" - }, - "kind": { - "description": "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", - "type": "string", - "default": "" - }, - "name": { - "description": "name is the plural name of the resource.", - "type": "string", - "default": "" - }, - "namespaced": { - "description": "namespaced indicates if a resource is namespaced or not.", - "type": "boolean", - "default": false - }, - "shortNames": { - "description": "shortNames is a list of suggested short names of the resource.", - "type": "array", - "items": { - "type": "string", - "default": "" - }, - "x-kubernetes-list-type": "atomic" - }, - "singularName": { - "description": "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", - "type": "string", - "default": "" - }, - "storageVersionHash": { - "description": "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", - "type": "string" - }, - "verbs": { - "description": "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", - "type": "array", - "items": { - "type": "string", - "default": "" - } - }, - "version": { - "description": "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", - "type": "string" - } - } - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { - "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", - "type": "object", - "required": [ - "groupVersion", - "resources" - ], - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "groupVersion": { - "description": "groupVersion is the group and version this APIResourceList is for.", - "type": "string", - "default": "" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "resources": { - "description": "resources contains the name of the resources and if they are namespaced.", - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResource" - } - ] - }, - "x-kubernetes-list-type": "atomic" - } - } - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions": { - "description": "DeleteOptions may be provided when deleting an API object.", - "type": "object", - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "dryRun": { - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "type": "array", - "items": { - "type": "string", - "default": "" - }, - "x-kubernetes-list-type": "atomic" - }, - "gracePeriodSeconds": { - "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", - "type": "integer", - "format": "int64" - }, - "ignoreStoreReadErrorWithClusterBreakingPotential": { - "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", - "type": "boolean" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "orphanDependents": { - "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", - "type": "boolean" - }, - "preconditions": { - "description": "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions" - } - ] - }, - "propagationPolicy": { - "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", - "type": "string" - } - } - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { - "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", - "type": "object" - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { - "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", - "type": "object", - "properties": { - "continue": { - "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", - "type": "string" - }, - "remainingItemCount": { - "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", - "type": "integer", - "format": "int64" - }, - "resourceVersion": { - "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", - "type": "string" - }, - "selfLink": { - "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", - "type": "string" - } - } - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { - "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", - "type": "object", - "properties": { - "apiVersion": { - "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", - "type": "string" - }, - "fieldsType": { - "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", - "type": "string" - }, - "fieldsV1": { - "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" - } - ] - }, - "manager": { - "description": "Manager is an identifier of the workflow managing these fields.", - "type": "string" - }, - "operation": { - "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", - "type": "string" - }, - "subresource": { - "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", - "type": "string" - }, - "time": { - "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" - } - ] - } - } - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { - "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", - "type": "object", - "properties": { - "annotations": { - "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", - "type": "object", - "additionalProperties": { - "type": "string", - "default": "" - } - }, - "creationTimestamp": { - "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" - } - ] - }, - "deletionGracePeriodSeconds": { - "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", - "type": "integer", - "format": "int64" - }, - "deletionTimestamp": { - "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" - } - ] - }, - "finalizers": { - "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", - "type": "array", - "items": { - "type": "string", - "default": "" - }, - "x-kubernetes-list-type": "set", - "x-kubernetes-patch-strategy": "merge" - }, - "generateName": { - "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", - "type": "string" - }, - "generation": { - "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", - "type": "integer", - "format": "int64" - }, - "labels": { - "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", - "type": "object", - "additionalProperties": { - "type": "string", - "default": "" - } - }, - "managedFields": { - "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" - } - ] - }, - "x-kubernetes-list-type": "atomic" - }, - "name": { - "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", - "type": "string" - }, - "namespace": { - "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", - "type": "string" - }, - "ownerReferences": { - "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" - } - ] - }, - "x-kubernetes-list-map-keys": [ - "uid" - ], - "x-kubernetes-list-type": "map", - "x-kubernetes-patch-merge-key": "uid", - "x-kubernetes-patch-strategy": "merge" - }, - "resourceVersion": { - "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", - "type": "string" - }, - "selfLink": { - "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", - "type": "string" - }, - "uid": { - "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", - "type": "string" - } - } - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { - "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", - "type": "object", - "required": [ - "apiVersion", - "kind", - "name", - "uid" - ], - "properties": { - "apiVersion": { - "description": "API version of the referent.", - "type": "string", - "default": "" - }, - "blockOwnerDeletion": { - "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", - "type": "boolean" - }, - "controller": { - "description": "If true, this reference points to the managing controller.", - "type": "boolean" - }, - "kind": { - "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string", - "default": "" - }, - "name": { - "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", - "type": "string", - "default": "" - }, - "uid": { - "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", - "type": "string", - "default": "" - } - }, - "x-kubernetes-map-type": "atomic" - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.Patch": { - "description": "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", - "type": "object" - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions": { - "description": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", - "type": "object", - "properties": { - "resourceVersion": { - "description": "Specifies the target ResourceVersion", - "type": "string" - }, - "uid": { - "description": "Specifies the target UID.", - "type": "string" - } - } - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.Status": { - "description": "Status is a return value for calls that don't return other objects.", - "type": "object", - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "code": { - "description": "Suggested HTTP return code for this status, 0 if not set.", - "type": "integer", - "format": "int32" - }, - "details": { - "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails" - } - ], - "x-kubernetes-list-type": "atomic" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "message": { - "description": "A human-readable description of the status of this operation.", - "type": "string" - }, - "metadata": { - "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" - } - ] - }, - "reason": { - "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", - "type": "string" - }, - "status": { - "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", - "type": "string" - } - } - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause": { - "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", - "type": "object", - "properties": { - "field": { - "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", - "type": "string" - }, - "message": { - "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", - "type": "string" - }, - "reason": { - "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", - "type": "string" - } - } - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails": { - "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", - "type": "object", - "properties": { - "causes": { - "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause" - } - ] - }, - "x-kubernetes-list-type": "atomic" - }, - "group": { - "description": "The group attribute of the resource associated with the status StatusReason.", - "type": "string" - }, - "kind": { - "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "name": { - "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", - "type": "string" - }, - "retryAfterSeconds": { - "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", - "type": "integer", - "format": "int32" - }, - "uid": { - "description": "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", - "type": "string" - } - } - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { - "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", - "type": "string", - "format": "date-time" - }, - "io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": { - "description": "Event represents a single event to a watched resource.", - "type": "object", - "required": [ - "type", - "object" - ], - "properties": { - "object": { - "description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension" - } - ] - }, - "type": { - "type": "string", - "default": "" - } - } - }, - "io.k8s.apimachinery.pkg.runtime.RawExtension": { - "description": "RawExtension is used to hold extensions in external versions.\n\nTo use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types.\n\n// Internal package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.Object `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// External package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// On the wire, the JSON will look something like this:\n\n\t{\n\t\t\"kind\":\"MyAPIObject\",\n\t\t\"apiVersion\":\"v1\",\n\t\t\"myPlugin\": {\n\t\t\t\"kind\":\"PluginA\",\n\t\t\t\"aOption\":\"foo\",\n\t\t},\n\t}\n\nSo what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.)", - "type": "object" - } - } - } -} \ No newline at end of file diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index 90682fefdb8..62c6af1a70f 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -64,9 +64,6 @@ func TestIntegrationOpenAPIs(t *testing.T) { }, { Group: "folder.grafana.app", Version: "v0alpha1", - }, { - Group: "peakq.grafana.app", - Version: "v0alpha1", }, { Group: "iam.grafana.app", Version: "v0alpha1", diff --git a/pkg/tests/apis/peakq/peakq_test.go b/pkg/tests/apis/peakq/peakq_test.go deleted file mode 100644 index 6b95b6a98d6..00000000000 --- a/pkg/tests/apis/peakq/peakq_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package peakq - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/tests/apis" - "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/grafana/grafana/pkg/tests/testsuite" -) - -func TestMain(m *testing.M) { - testsuite.Run(m) -} - -func TestIntegrationPeakQ(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, // required for experimental APIs - EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service - }, - }) - - t.Run("Check discovery client", func(t *testing.T) { - disco := helper.NewDiscoveryClient() - resources, err := disco.ServerResourcesForGroupVersion("peakq.grafana.app/v0alpha1") - require.NoError(t, err) - - v1Disco, err := json.MarshalIndent(resources, "", " ") - require.NoError(t, err) - //fmt.Printf("%s", string(v1Disco)) - require.JSONEq(t, `{ - "kind": "APIResourceList", - "apiVersion": "v1", - "groupVersion": "peakq.grafana.app/v0alpha1", - "resources": [ - { - "name": "querytemplates", - "singularName": "querytemplate", - "namespaced": true, - "kind": "QueryTemplate", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ] - }, - { - "name": "querytemplates/render", - "singularName": "", - "namespaced": true, - "kind": "RenderedQuery", - "verbs": [ - "get" - ] - } - ] - }`, string(v1Disco)) - }) -} diff --git a/pkg/tests/apis/peakq/testdata/query-generate.yaml b/pkg/tests/apis/peakq/testdata/query-generate.yaml deleted file mode 100644 index efbcaccbe1f..00000000000 --- a/pkg/tests/apis/peakq/testdata/query-generate.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: peakq.grafana.app/v0alpha1 -kind: QueryTemplate -metadata: - generateName: x # anything is ok here... except yes or true -- they become boolean! -spec: - title: Generated query template - description: A description from here diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index 320e62bc9c4..620dff7c699 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -30,7 +30,6 @@ import templatingReducers from 'app/features/variables/state/keyedVariablesReduc import { alertingApi } from '../../features/alerting/unified/api/alertingApi'; import { iamApi } from '../../features/iam/api/api'; import { userPreferencesAPI } from '../../features/preferences/api'; -import { queryLibraryApi } from '../../features/query-library/api/api'; import { cleanUpAction } from '../actions/cleanUp'; const rootReducers = { @@ -60,7 +59,6 @@ const rootReducers = { [publicDashboardApi.reducerPath]: publicDashboardApi.reducer, [browseDashboardsAPI.reducerPath]: browseDashboardsAPI.reducer, [cloudMigrationAPI.reducerPath]: cloudMigrationAPI.reducer, - [queryLibraryApi.reducerPath]: queryLibraryApi.reducer, [iamApi.reducerPath]: iamApi.reducer, [userPreferencesAPI.reducerPath]: userPreferencesAPI.reducer, }; diff --git a/public/app/features/explore/spec/helper/mocks.ts b/public/app/features/explore/spec/helper/mocks.ts new file mode 100644 index 00000000000..160762f25e2 --- /dev/null +++ b/public/app/features/explore/spec/helper/mocks.ts @@ -0,0 +1,20 @@ +import { getAPIBaseURL } from '../../../../api/utils'; + +import { getIdentityDisplayList } from './testdata/identityDisplayList'; +import { getTestQueryList } from './testdata/testQueryList'; + +// This is not ideal but not sure how to fix it right now as this is duplicated from the API definition which is in +// Enterprise and so we cannot import it here. +// We have some tests for testing QL inside Explore. The whole Explore setup is in OSS, and it needs these mocks but the +// test itself is in Enterprise. Ideally we would inject the mocks in the tests somehow. +export const BASE_URL = getAPIBaseURL('peakq.grafana.app', 'v0alpha1'); + +export const mockData = { + all: { + url: BASE_URL, + response: getTestQueryList(), + }, + identityDisplay: { + response: getIdentityDisplayList(), + }, +}; diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index 78aa044cab3..89b555b3126 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -37,7 +37,6 @@ import { GrafanaContext } from 'app/core/context/GrafanaContext'; import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute'; import { Echo } from 'app/core/services/echo/Echo'; import { setLastUsedDatasourceUID } from 'app/core/utils/explore'; -import { IdentityServiceMocks, QueryLibraryMocks } from 'app/features/query-library'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { configureStore } from 'app/store/configureStore'; @@ -49,6 +48,16 @@ import { initialUserState } from '../../../profile/state/reducers'; import ExplorePage from '../../ExplorePage'; import { QueriesDrawerContextProvider } from '../../QueriesDrawer/QueriesDrawerContext'; +import { mockData } from './mocks'; + +export const QueryLibraryMocks = { + data: mockData.all, +}; + +export const IdentityServiceMocks = { + data: mockData.identityDisplay, +}; + type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; type SetupOptions = { diff --git a/public/app/features/query-library/api/testdata/identityDisplayList.ts b/public/app/features/explore/spec/helper/testdata/identityDisplayList.ts similarity index 100% rename from public/app/features/query-library/api/testdata/identityDisplayList.ts rename to public/app/features/explore/spec/helper/testdata/identityDisplayList.ts diff --git a/public/app/features/query-library/api/testdata/testQueryList.ts b/public/app/features/explore/spec/helper/testdata/testQueryList.ts similarity index 100% rename from public/app/features/query-library/api/testdata/testQueryList.ts rename to public/app/features/explore/spec/helper/testdata/testQueryList.ts diff --git a/public/app/features/query-library/api/api.ts b/public/app/features/query-library/api/api.ts deleted file mode 100644 index ec318cb7e84..00000000000 --- a/public/app/features/query-library/api/api.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createApi } from '@reduxjs/toolkit/query/react'; - -import { createBaseQuery } from '../../../api/createBaseQuery'; -import { getAPIBaseURL } from '../../../api/utils'; - -// Currently, we are loading all query templates -// Organizations can have maximum of 1000 query templates -export const QUERY_LIBRARY_GET_LIMIT = 1000; - -export const BASE_URL = getAPIBaseURL('peakq.grafana.app', 'v0alpha1'); - -export const queryLibraryApi = createApi({ - baseQuery: createBaseQuery({ baseURL: BASE_URL }), - reducerPath: 'queryLibraryAPI', - endpoints: () => ({}), -}); diff --git a/public/app/features/query-library/api/endpoints.gen.ts b/public/app/features/query-library/api/endpoints.gen.ts deleted file mode 100644 index 28e1e0f8354..00000000000 --- a/public/app/features/query-library/api/endpoints.gen.ts +++ /dev/null @@ -1,475 +0,0 @@ -import { queryLibraryApi as api } from './api'; -export const addTagTypes = ['QueryTemplate'] as const; -const injectedRtkApi = api - .enhanceEndpoints({ - addTagTypes, - }) - .injectEndpoints({ - endpoints: (build) => ({ - listQueryTemplate: build.query({ - query: (queryArg) => ({ - url: `/querytemplates`, - params: { - pretty: queryArg.pretty, - allowWatchBookmarks: queryArg.allowWatchBookmarks, - continue: queryArg['continue'], - fieldSelector: queryArg.fieldSelector, - labelSelector: queryArg.labelSelector, - limit: queryArg.limit, - resourceVersion: queryArg.resourceVersion, - resourceVersionMatch: queryArg.resourceVersionMatch, - sendInitialEvents: queryArg.sendInitialEvents, - timeoutSeconds: queryArg.timeoutSeconds, - watch: queryArg.watch, - }, - }), - providesTags: ['QueryTemplate'], - }), - createQueryTemplate: build.mutation({ - query: (queryArg) => ({ - url: `/querytemplates`, - method: 'POST', - body: queryArg.queryTemplate, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - }, - }), - invalidatesTags: ['QueryTemplate'], - }), - deleteQueryTemplate: build.mutation({ - query: (queryArg) => ({ - url: `/querytemplates/${queryArg.name}`, - method: 'DELETE', - body: queryArg.deleteOptions, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - gracePeriodSeconds: queryArg.gracePeriodSeconds, - ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, - orphanDependents: queryArg.orphanDependents, - propagationPolicy: queryArg.propagationPolicy, - }, - }), - invalidatesTags: ['QueryTemplate'], - }), - updateQueryTemplate: build.mutation({ - query: (queryArg) => ({ - url: `/querytemplates/${queryArg.name}`, - method: 'PATCH', - body: queryArg.patch, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - force: queryArg.force, - }, - }), - invalidatesTags: ['QueryTemplate'], - }), - }), - overrideExisting: false, - }); -export { injectedRtkApi as generatedQueryLibraryApi }; -export type ListQueryTemplateApiResponse = /** status 200 OK */ QueryTemplateList; -export type ListQueryTemplateApiArg = { - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ - allowWatchBookmarks?: boolean; - /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". - - This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ - continue?: string; - /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ - fieldSelector?: string; - /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ - labelSelector?: string; - /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. - - The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ - limit?: number; - /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersion?: string; - /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersionMatch?: string; - /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. - - When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan - is interpreted as "data at least as new as the provided `resourceVersion`" - and the bookmark event is send when the state is synced - to a `resourceVersion` at least as fresh as the one provided by the ListOptions. - If `resourceVersion` is unset, this is interpreted as "consistent read" and the - bookmark event is send when the state is synced at least to the moment - when request started being processed. - - `resourceVersionMatch` set to any other value or unset - Invalid error is returned. - - Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ - sendInitialEvents?: boolean; - /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ - timeoutSeconds?: number; - /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ - watch?: boolean; -}; -export type CreateQueryTemplateApiResponse = /** status 200 OK */ - | QueryTemplate - | /** status 201 Created */ QueryTemplate - | /** status 202 Accepted */ QueryTemplate; -export type CreateQueryTemplateApiArg = { - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ - fieldManager?: string; - /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ - fieldValidation?: string; - queryTemplate: QueryTemplate; -}; -export type DeleteQueryTemplateApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; -export type DeleteQueryTemplateApiArg = { - /** name of the QueryTemplate */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ - gracePeriodSeconds?: number; - /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ - ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; - /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ - orphanDependents?: boolean; - /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ - propagationPolicy?: string; - deleteOptions: DeleteOptions; -}; -export type UpdateQueryTemplateApiResponse = /** status 200 OK */ - | QueryTemplate - | /** status 201 Created */ QueryTemplate; -export type UpdateQueryTemplateApiArg = { - /** name of the QueryTemplate */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ - fieldManager?: string; - /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ - fieldValidation?: string; - /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ - force?: boolean; - patch: Patch; -}; -export type Time = string; -export type FieldsV1 = object; -export type ManagedFieldsEntry = { - /** APIVersion defines the version of this resource that this field set applies to. The format is "group/version" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted. */ - apiVersion?: string; - /** FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: "FieldsV1" */ - fieldsType?: string; - /** FieldsV1 holds the first JSON version format as described in the "FieldsV1" type. */ - fieldsV1?: FieldsV1; - /** Manager is an identifier of the workflow managing these fields. */ - manager?: string; - /** Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'. */ - operation?: string; - /** Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource. */ - subresource?: string; - /** Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over. */ - time?: Time; -}; -export type OwnerReference = { - /** API version of the referent. */ - apiVersion: string; - /** If true, AND if the owner has the "foregroundDeletion" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs "delete" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned. */ - blockOwnerDeletion?: boolean; - /** If true, this reference points to the managing controller. */ - controller?: boolean; - /** Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind: string; - /** Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ - name: string; - /** UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ - uid: string; -}; -export type ObjectMeta = { - /** Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations */ - annotations?: { - [key: string]: string; - }; - /** CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. - - Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ - creationTimestamp?: Time; - /** Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only. */ - deletionGracePeriodSeconds?: number; - /** DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested. - - Populated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ - deletionTimestamp?: Time; - /** Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list. */ - finalizers?: string[]; - /** GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server. - - If this field is specified and the generated name exists, the server will return a 409. - - Applied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency */ - generateName?: string; - /** A sequence number representing a specific generation of the desired state. Populated by the system. Read-only. */ - generation?: number; - /** Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels */ - labels?: { - [key: string]: string; - }; - /** ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like "ci-cd". The set of fields is always in the version that the workflow used when modifying the object. */ - managedFields?: ManagedFieldsEntry[]; - /** Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ - name?: string; - /** Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty. - - Must be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces */ - namespace?: string; - /** List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller. */ - ownerReferences?: OwnerReference[]; - /** An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources. - - Populated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */ - resourceVersion?: string; - /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ - selfLink?: string; - /** UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations. - - Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ - uid?: string; -}; -export type DataQuery = { - /** The datasource */ - datasource?: { - /** The apiserver version */ - apiVersion?: string; - /** The datasource plugin type */ - type: string; - /** Datasource UID (NOTE: name in k8s) */ - uid?: string; - }; - /** true if query is disabled (ie should not be returned to the dashboard) - NOTE: this does not always imply that the query should not be executed since - the results from a hidden query may be used as the input to other queries (SSE etc) */ - hide?: boolean; - /** Interval is the suggested duration between time points in a time series query. - NOTE: the values for intervalMs is not saved in the query model. It is typically calculated - from the interval required to fill a pixels in the visualization */ - intervalMs?: number; - /** MaxDataPoints is the maximum number of data points that should be returned from a time series query. - NOTE: the values for maxDataPoints is not saved in the query model. It is typically calculated - from the number of pixels visible in a visualization */ - maxDataPoints?: number; - /** QueryType is an optional identifier for the type of query. - It can be used to distinguish different types of queries. */ - queryType?: string; - /** RefID is the unique identifier of the query, set by the frontend call. */ - refId?: string; - /** Optionally define expected query result behavior */ - resultAssertions?: { - /** Maximum frame count */ - maxFrames?: number; - /** Type asserts that the frame matches a known type structure. - - - Possible enum values: - - `""` - - `"timeseries-wide"` - - `"timeseries-long"` - - `"timeseries-many"` - - `"timeseries-multi"` - - `"directory-listing"` - - `"table"` - - `"numeric-wide"` - - `"numeric-multi"` - - `"numeric-long"` - - `"log-lines"` */ - type?: - | '' - | 'timeseries-wide' - | 'timeseries-long' - | 'timeseries-many' - | 'timeseries-multi' - | 'directory-listing' - | 'table' - | 'numeric-wide' - | 'numeric-multi' - | 'numeric-long' - | 'log-lines'; - /** TypeVersion is the version of the Type property. Versions greater than 0.0 correspond to the dataplane - contract documentation https://grafana.github.io/dataplane/contract/. */ - typeVersion: number[]; - }; - /** TimeRange represents the query range - NOTE: unlike generic /ds/query, we can now send explicit time values in each query - NOTE: the values for timeRange are not saved in a dashboard, they are constructed on the fly */ - timeRange?: { - /** From is the start time of the query. */ - from: string; - /** To is the end time of the query. */ - to: string; - }; - [key: string]: any; -}; -export type TemplatePosition = { - /** End is the byte offset of the end of the variable. */ - end: number; - /** Start is the byte offset within TargetKey's property of the variable. It is the start location for replacements). */ - start: number; -}; -export type TemplateVariableReplacement = { - /** How values should be interpolated - - Possible enum values: - - `"csv"` Formats variables with multiple values as a comma-separated string. - - `"doublequote"` Formats single- and multi-valued variables into a comma-separated string - - `"json"` Formats variables with multiple values as a comma-separated string. - - `"pipe"` Formats variables with multiple values into a pipe-separated string. - - `"raw"` Formats variables with multiple values into comma-separated string. This is the default behavior when no format is specified - - `"singlequote"` Formats single- and multi-valued variables into a comma-separated string */ - format?: 'csv' | 'doublequote' | 'json' | 'pipe' | 'raw' | 'singlequote'; - /** Path is the location of the property within a target. The format for this is not figured out yet (Maybe JSONPath?). Idea: ["string", int, "string"] where int indicates array offset */ - path: string; - /** Positions is a list of where to perform the interpolation within targets during render. The first string is the Idx of the target as a string, since openAPI does not support ints as map keys */ - position?: TemplatePosition; -}; -export type TemplateTarget = { - /** DataType is the returned Dataplane type from the query. */ - dataType?: string; - /** Query target */ - properties: DataQuery; - /** Variables that will be replaced in the query */ - variables: { - [key: string]: TemplateVariableReplacement[]; - }; -}; -export type Unstructured = { - [key: string]: any; -}; -export type TemplateTemplateVariable = { - /** DefaultValue is the value to be used when there is no selected value during render. */ - defaultValues?: string[]; - /** Key is the name of the variable. */ - key: string; - /** ValueListDefinition is the object definition used by the FE to get a list of possible values to select for render. */ - valueListDefinition?: Unstructured; -}; -export type TemplateQueryTemplate = { - /** Longer description for why it is interesting */ - description?: string; - /** Output variables */ - targets: TemplateTarget[]; - /** A display name */ - title?: string; - /** The variables that can be used to render */ - vars?: TemplateTemplateVariable[]; -}; -export type QueryTemplate = { - /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: ObjectMeta; - spec?: TemplateQueryTemplate; -}; -export type ListMeta = { - /** continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. */ - continue?: string; - /** remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact. */ - remainingItemCount?: number; - /** String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */ - resourceVersion?: string; - /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ - selfLink?: string; -}; -export type QueryTemplateList = { - /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - items?: QueryTemplate[]; - /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: ListMeta; -}; -export type StatusCause = { - /** The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional. - - Examples: - "name" - the field "name" on the current resource - "items[0].name" - the field "name" on the first array entry in "items" */ - field?: string; - /** A human-readable description of the cause of the error. This field may be presented as-is to a reader. */ - message?: string; - /** A machine-readable description of the cause of the error. If this value is empty there is no information available. */ - reason?: string; -}; -export type StatusDetails = { - /** The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes. */ - causes?: StatusCause[]; - /** The group attribute of the resource associated with the status StatusReason. */ - group?: string; - /** The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - /** The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described). */ - name?: string; - /** If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action. */ - retryAfterSeconds?: number; - /** UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ - uid?: string; -}; -export type Status = { - /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** Suggested HTTP return code for this status, 0 if not set. */ - code?: number; - /** Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type. */ - details?: StatusDetails; - /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - /** A human-readable description of the status of this operation. */ - message?: string; - /** Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - metadata?: ListMeta; - /** A machine-readable description of why this operation is in the "Failure" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it. */ - reason?: string; - /** Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status */ - status?: string; -}; -export type Preconditions = { - /** Specifies the target ResourceVersion */ - resourceVersion?: string; - /** Specifies the target UID. */ - uid?: string; -}; -export type DeleteOptions = { - /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string[]; - /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ - gracePeriodSeconds?: number; - /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ - ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; - /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ - orphanDependents?: boolean; - /** Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned. */ - preconditions?: Preconditions; - /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ - propagationPolicy?: string; -}; -export type Patch = object; diff --git a/public/app/features/query-library/api/mappers.ts b/public/app/features/query-library/api/mappers.ts deleted file mode 100644 index 26cd684c207..00000000000 --- a/public/app/features/query-library/api/mappers.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; - -import { AnnoKeyCreatedBy } from '../../apiserver/types'; -import { AddQueryTemplateCommand, QueryTemplate } from '../types'; - -import { ListQueryTemplateApiResponse, QueryTemplate as QT } from './endpoints.gen'; - -export const convertDataQueryResponseToQueryTemplates = (result: ListQueryTemplateApiResponse): QueryTemplate[] => { - if (!result.items) { - return []; - } - return result.items.map((spec) => { - return { - uid: spec.metadata?.name ?? '', - title: spec.spec?.title ?? '', - targets: - spec.spec?.targets.map((target) => ({ - ...target.properties, - refId: target.properties.refId ?? '', - })) ?? [], - createdAtTimestamp: new Date(spec.metadata?.creationTimestamp ?? '').getTime(), - user: { - uid: spec.metadata?.annotations?.[AnnoKeyCreatedBy] ?? '', - }, - }; - }); -}; - -export const convertAddQueryTemplateCommandToDataQuerySpec = (addQueryTemplateCommand: AddQueryTemplateCommand): QT => { - const { title, targets } = addQueryTemplateCommand; - return { - metadata: { - /** - * Server will append to whatever is passed here, but just to be safe we generate a uuid - * More info https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#idempotency - */ - generateName: uuidv4(), - }, - spec: { - title: title, - vars: [], // TODO: Detect variables in #86838 - targets: targets.map((dataQuery) => ({ - variables: {}, - properties: { - ...dataQuery, - datasource: { - ...dataQuery.datasource, - type: dataQuery.datasource?.type ?? '', - }, - }, - })), - }, - }; -}; diff --git a/public/app/features/query-library/api/mocks.ts b/public/app/features/query-library/api/mocks.ts deleted file mode 100644 index ed88eeba17c..00000000000 --- a/public/app/features/query-library/api/mocks.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { BASE_URL } from './api'; -import { getIdentityDisplayList } from './testdata/identityDisplayList'; -import { getTestQueryList } from './testdata/testQueryList'; - -export const mockData = { - all: { - url: BASE_URL, - response: getTestQueryList(), - }, - identityDisplay: { - response: getIdentityDisplayList(), - }, -}; diff --git a/public/app/features/query-library/index.ts b/public/app/features/query-library/index.ts deleted file mode 100644 index 7f0c6f1c325..00000000000 --- a/public/app/features/query-library/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * This is a temporary place for Query Library API and data types. - * To be exposed via grafana-runtime/data in the future. - * - * Query Library is an experimental feature, the API and components are subject to change - * - * @alpha - */ - -import { QUERY_LIBRARY_GET_LIMIT } from './api/api'; -import { generatedQueryLibraryApi } from './api/endpoints.gen'; -import { mockData } from './api/mocks'; - -export const { - useCreateQueryTemplateMutation, - useDeleteQueryTemplateMutation, - useListQueryTemplateQuery, - useUpdateQueryTemplateMutation, -} = generatedQueryLibraryApi.enhanceEndpoints({ - endpoints: { - // Need to mutate the generated query to force query limit - listQueryTemplate: (endpointDefinition) => { - const originalQuery = endpointDefinition.query; - if (originalQuery) { - endpointDefinition.query = (requestOptions) => - originalQuery({ - ...requestOptions, - limit: QUERY_LIBRARY_GET_LIMIT, - }); - } - }, - // Need to mutate the generated query to set the Content-Type header correctly - updateQueryTemplate: (endpointDefinition) => { - const originalQuery = endpointDefinition.query; - if (originalQuery) { - endpointDefinition.query = (requestOptions) => ({ - ...originalQuery(requestOptions), - headers: { - 'Content-Type': 'application/merge-patch+json', - }, - }); - } - }, - }, -}); - -export const QueryLibraryMocks = { - data: mockData.all, -}; - -export const IdentityServiceMocks = { - data: mockData.identityDisplay, -}; diff --git a/public/app/features/query-library/types.ts b/public/app/features/query-library/types.ts deleted file mode 100644 index 30a6410c520..00000000000 --- a/public/app/features/query-library/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { DataQuery } from '@grafana/schema'; - -export type DataQueryTarget = { - variables: object; // TODO: Detect variables in #86838 - properties: DataQuery; -}; - -export type DataQuerySpec = { - title: string; - vars: object[]; // TODO: Detect variables in #86838 - targets: DataQueryTarget[]; -}; - -export type DataQueryPartialSpec = Partial; - -export type QueryTemplate = { - uid: string; - title: string; - targets: DataQuery[]; - createdAtTimestamp: number; - user?: User; -}; - -export type AddQueryTemplateCommand = { - title: string; - targets: DataQuery[]; -}; - -export type EditQueryTemplateCommand = { - uid: string; - partialSpec: DataQueryPartialSpec; -}; - -export type DeleteQueryTemplateCommand = { - uid: string; -}; - -export type User = { - uid: string; - displayName?: string; - avatarUrl?: string; -}; diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 2102c936b5f..5757615ad61 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -12,7 +12,6 @@ import { buildInitialState } from '../core/reducers/navModel'; import { addReducer, createRootReducer } from '../core/reducers/root'; import { alertingApi } from '../features/alerting/unified/api/alertingApi'; import { iamApi } from '../features/iam/api/api'; -import { queryLibraryApi } from '../features/query-library/api/api'; import { setStore } from './store'; @@ -40,7 +39,6 @@ export function configureStore(initialState?: Partial) { publicDashboardApi.middleware, browseDashboardsAPI.middleware, cloudMigrationAPI.middleware, - queryLibraryApi.middleware, userPreferencesAPI.middleware, iamApi.middleware, ...extraMiddleware diff --git a/scripts/generate-rtk-apis.ts b/scripts/generate-rtk-apis.ts index 932c393a8cf..691e7aeaa1f 100644 --- a/scripts/generate-rtk-apis.ts +++ b/scripts/generate-rtk-apis.ts @@ -48,15 +48,6 @@ const config: ConfigFile = { flattenArg: false, tag: true, }, - '../public/app/features/query-library/api/endpoints.gen.ts': { - schemaFile: '../data/openapi/peakq.grafana.app-v0alpha1.json', - apiFile: '../public/app/features/query-library/api/api.ts', - apiImport: 'queryLibraryApi', - filterEndpoints: ['listQueryTemplate', 'createQueryTemplate', 'deleteQueryTemplate', 'updateQueryTemplate'], - exportName: 'generatedQueryLibraryApi', - flattenArg: false, - tag: true, - }, }, }; From 1856d47e475d0f273a81c4680ab02bda7a054fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Fri, 14 Feb 2025 12:34:52 +0100 Subject: [PATCH 604/894] Remove GetResourceClient hack from unified package. (#100636) * Remove GetResourceClient hack from unified package. --- pkg/api/dashboard_test.go | 6 +-- pkg/api/folder_bench_test.go | 6 +-- pkg/registry/apis/dashboard/search.go | 16 +++--- pkg/registry/apis/dashboard/search_test.go | 49 +++++++++---------- .../apis/dashboard/v0alpha1/register.go | 2 +- pkg/services/accesscontrol/acimpl/service.go | 4 +- .../accesscontrol/acimpl/service_test.go | 1 - .../ossaccesscontrol/testutil/testutil.go | 4 +- .../accesscontrol/accesscontrol_test.go | 4 +- .../annotationsimpl/annotations_test.go | 8 +-- pkg/services/apiserver/client/client.go | 16 +++--- .../database/database_folder_test.go | 2 +- .../dashboards/database/database_test.go | 4 +- .../dashboards/service/dashboard_service.go | 3 +- .../dashboard_service_integration_test.go | 20 ++++++-- .../service/service_test.go | 1 + .../dashboardversion/dashverimpl/dashver.go | 6 ++- pkg/services/folder/folderimpl/folder.go | 4 ++ pkg/services/folder/folderimpl/folder_test.go | 9 ++-- .../folderimpl/folder_unifiedstorage_test.go | 2 +- .../libraryelements/libraryelements_test.go | 13 ++--- .../librarypanels/librarypanels_test.go | 8 +-- .../ngalert/api/api_provisioning_test.go | 2 +- .../ngalert/provisioning/alert_rules_test.go | 2 +- pkg/services/ngalert/testutil/testutil.go | 4 +- .../dashboards/file_reader_test.go | 2 +- .../provisioning/dashboards/validator_test.go | 2 +- .../publicdashboards/api/query_test.go | 1 + .../publicdashboards/service/service_test.go | 4 +- pkg/services/quota/quotaimpl/quota_test.go | 4 +- .../extsvcaccounts/service_test.go | 3 +- .../sqlstore/permissions/dashboard_test.go | 2 +- .../permissions/dashboards_bench_test.go | 2 +- pkg/storage/unified/client.go | 17 ------- .../federated/federatedtests/stats_test.go | 2 +- pkg/storage/unified/resource/search_client.go | 10 ++-- 36 files changed, 119 insertions(+), 126 deletions(-) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 36a329e5faa..55cff3d3c13 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -832,11 +832,11 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr quotaService := quotatest.New(false, nil) folderSvc := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) if dashboardService == nil { dashboardService, err = service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, - ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, ) require.NoError(t, err) dashboardService.(dashboards.PermissionsRegistrationService).RegisterDashboardPermissions(dashboardPermissions) @@ -844,7 +844,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr dashboardProvisioningService, err := service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, - ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, ) require.NoError(t, err) diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index 42fc5d52d55..6ad9174981f 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -462,10 +462,10 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog fStore := folderimpl.ProvideStore(sc.db) folderServiceWithFlagOn := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, - nil, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) acSvc := acimpl.ProvideOSSService( sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(), - features, tracing.InitializeTracerForTest(), sc.db, permreg.ProvidePermissionRegistry(), nil, folderServiceWithFlagOn, + features, tracing.InitializeTracerForTest(), sc.db, permreg.ProvidePermissionRegistry(), nil, ) folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( cfg, features, routing.NewRouteRegister(), sc.db, ac, license, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets) @@ -473,7 +473,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl( sc.cfg, dashStore, folderStore, features, folderPermissions, ac, - folderServiceWithFlagOn, fStore, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil, + folderServiceWithFlagOn, fStore, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil, nil, ) require.NoError(b, err) diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 9edd79f5f83..7dec39f22ee 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -11,8 +11,6 @@ import ( "strconv" "strings" - "github.com/grafana/grafana/pkg/storage/unified" - "github.com/grafana/grafana/pkg/storage/unified/search" "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,6 +18,8 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" + "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/dashboard" dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" @@ -38,13 +38,13 @@ import ( // The DTO returns everything the UI needs in a single request type SearchHandler struct { log log.Logger - client func(context.Context) resource.ResourceIndexClient + client resource.ResourceIndexClient tracer trace.Tracer features featuremgmt.FeatureToggles } -func NewSearchHandler(tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient, features featuremgmt.FeatureToggles) *SearchHandler { - searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, unified.GetResourceClient, legacyDashboardSearcher) +func NewSearchHandler(tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient, resourceClient resource.ResourceClient, features featuremgmt.FeatureToggles) *SearchHandler { + searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, resourceClient, legacyDashboardSearcher) return &SearchHandler{ client: searchClient, log: log.New("grafana-apiserver.dashboards.search"), @@ -360,7 +360,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { searchRequest.Options.Fields = append(searchRequest.Options.Fields, namesFilter...) } - result, err := s.client(ctx).Search(ctx, searchRequest) + result, err := s.client.Search(ctx, searchRequest) if err != nil { errhttp.Write(ctx, err, w) return @@ -439,7 +439,7 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use }, } // get all dashboards user has access to, along with their parent folder uid - dashboardResult, err := s.client(ctx).Search(ctx, dashboardSearchRequest) + dashboardResult, err := s.client.Search(ctx, dashboardSearchRequest) if err != nil { return sharedDashboards, err } @@ -482,7 +482,7 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use }}, }, } - foldersResult, err := s.client(ctx).Search(ctx, folderSearchRequest) + foldersResult, err := s.client.Search(ctx, folderSearchRequest) if err != nil { return sharedDashboards, err } diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index c92b39efdd0..5d3ccc942e1 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -7,6 +7,10 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/rest" @@ -17,15 +21,11 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" ) func TestSearchFallback(t *testing.T) { t.Run("should hit legacy search handler on mode 0", func(t *testing.T) { mockClient := &MockClient{} - mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -33,8 +33,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode0}, }, } - searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil) - searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -53,7 +53,6 @@ func TestSearchFallback(t *testing.T) { t.Run("should hit legacy search handler on mode 1", func(t *testing.T) { mockClient := &MockClient{} - mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -61,8 +60,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode1}, }, } - searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil) - searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -81,7 +80,6 @@ func TestSearchFallback(t *testing.T) { t.Run("should hit legacy search handler on mode 2", func(t *testing.T) { mockClient := &MockClient{} - mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -89,8 +87,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode2}, }, } - searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil) - searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -109,7 +107,6 @@ func TestSearchFallback(t *testing.T) { t.Run("should hit unified storage search handler on mode 3", func(t *testing.T) { mockClient := &MockClient{} - mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -117,8 +114,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode3}, }, } - searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil) - searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -137,7 +134,6 @@ func TestSearchFallback(t *testing.T) { t.Run("should hit unified storage search handler on mode 4", func(t *testing.T) { mockClient := &MockClient{} - mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -145,8 +141,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode4}, }, } - searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil) - searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -165,7 +161,6 @@ func TestSearchFallback(t *testing.T) { t.Run("should hit unified storage search handler on mode 5", func(t *testing.T) { mockClient := &MockClient{} - mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient } mockLegacyClient := &MockClient{} cfg := &setting.Cfg{ @@ -173,8 +168,8 @@ func TestSearchFallback(t *testing.T) { "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode5}, }, } - searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil) - searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil) + searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/search", nil) @@ -201,7 +196,7 @@ func TestSearchHandler(t *testing.T) { // Initialize the search handler with the mock client searchHandler := SearchHandler{ log: log.New("test", "test"), - client: func(context.Context) resource.ResourceIndexClient { return mockClient }, + client: mockClient, tracer: tracing.NewNoopTracerService(), features: features, } @@ -230,7 +225,7 @@ func TestSearchHandler(t *testing.T) { // Initialize the search handler with the mock client searchHandler := SearchHandler{ log: log.New("test", "test"), - client: func(context.Context) resource.ResourceIndexClient { return mockClient }, + client: mockClient, tracer: tracing.NewNoopTracerService(), features: features, } @@ -259,7 +254,7 @@ func TestSearchHandler(t *testing.T) { // Initialize the search handler with the mock client searchHandler := SearchHandler{ log: log.New("test", "test"), - client: func(context.Context) resource.ResourceIndexClient { return mockClient }, + client: mockClient, tracer: tracing.NewNoopTracerService(), features: features, } @@ -311,7 +306,7 @@ func TestSearchHandler(t *testing.T) { // Initialize the search handler with the mock client searchHandler := SearchHandler{ log: log.New("test", "test"), - client: func(context.Context) resource.ResourceIndexClient { return mockClient }, + client: mockClient, tracer: tracing.NewNoopTracerService(), features: features, } @@ -350,7 +345,7 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { features := featuremgmt.WithFeatures() searchHandler := SearchHandler{ log: log.New("test", "test"), - client: func(context.Context) resource.ResourceIndexClient { return mockClient }, + client: mockClient, tracer: tracing.NewNoopTracerService(), features: features, } @@ -432,7 +427,7 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { features := featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearchPermissionFiltering) searchHandler := SearchHandler{ log: log.New("test", "test"), - client: func(context.Context) resource.ResourceIndexClient { return mockClient }, + client: mockClient, tracer: tracing.NewNoopTracerService(), features: features, } diff --git a/pkg/registry/apis/dashboard/v0alpha1/register.go b/pkg/registry/apis/dashboard/v0alpha1/register.go index b85e5f49329..815f32815ab 100644 --- a/pkg/registry/apis/dashboard/v0alpha1/register.go +++ b/pkg/registry/apis/dashboard/v0alpha1/register.go @@ -82,7 +82,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, features: features, accessControl: accessControl, unified: unified, - search: dashboard.NewSearchHandler(tracing, cfg, legacyDashboardSearcher, features), + search: dashboard.NewSearchHandler(tracing, cfg, legacyDashboardSearcher, unified, features), legacy: &dashboard.DashboardStorage{ Resource: dashboardv0alpha1.DashboardResourceInfo, diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 26f3e5cd70c..d98c5dc8c2a 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -54,7 +54,7 @@ func ProvideService( cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService, accessControl accesscontrol.AccessControl, userService user.Service, actionResolver accesscontrol.ActionResolver, features featuremgmt.FeatureToggles, tracer tracing.Tracer, permRegistry permreg.PermissionRegistry, - lock *serverlock.ServerLockService, folderService folder.Service, + lock *serverlock.ServerLockService, ) (*Service, error) { service := ProvideOSSService( cfg, @@ -66,7 +66,6 @@ func ProvideService( db, permRegistry, lock, - folderService, ) api.NewAccessControlAPI(routeRegister, accessControl, service, userService, features).RegisterAPIEndpoints() @@ -89,7 +88,6 @@ func ProvideOSSService( cfg *setting.Cfg, store accesscontrol.Store, actionResolver accesscontrol.ActionResolver, cache *localcache.CacheService, features featuremgmt.FeatureToggles, tracer tracing.Tracer, db db.DB, permRegistry permreg.PermissionRegistry, lock *serverlock.ServerLockService, - folderService folder.Service, ) *Service { s := &Service{ actionResolver: actionResolver, diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go index daaffdea54f..74acae53652 100644 --- a/pkg/services/accesscontrol/acimpl/service_test.go +++ b/pkg/services/accesscontrol/acimpl/service_test.go @@ -73,7 +73,6 @@ func TestUsageMetrics(t *testing.T) { nil, permreg.ProvidePermissionRegistry(), nil, - nil, ) assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"]) }) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go b/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go index 6bd3f39e6d5..3af111020d4 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go @@ -47,12 +47,12 @@ func ProvideFolderPermissions( folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) fService := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) acSvc := acimpl.ProvideOSSService( cfg, acdb.ProvideService(sqlStore), actionSets, localcache.ProvideService(), features, tracing.InitializeTracerForTest(), sqlStore, permreg.ProvidePermissionRegistry(), - nil, fService, + nil, ) orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) diff --git a/pkg/services/annotations/accesscontrol/accesscontrol_test.go b/pkg/services/annotations/accesscontrol/accesscontrol_test.go index 4070d1d6b5f..da8f266e6cf 100644 --- a/pkg/services/annotations/accesscontrol/accesscontrol_test.go +++ b/pkg/services/annotations/accesscontrol/accesscontrol_test.go @@ -50,9 +50,9 @@ func TestIntegrationAuthorize(t *testing.T) { ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) folderSvc := folderimpl.ProvideService( fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, - nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil) require.NoError(t, err) dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index 0ac44989ec8..6f08803bd64 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -62,9 +62,9 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) folderSvc := folderimpl.ProvideService( fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, - nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil) require.NoError(t, err) dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) repo := ProvideService(sql, cfg, features, tagService, tracing.InitializeTracerForTest(), ruleStore, dashSvc) @@ -245,9 +245,9 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { folderStore := folderimpl.ProvideDashboardFolderStore(sql) folderSvc := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, - nil, sql, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sql, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, features, accesscontrolmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil) require.NoError(t, err) dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) cfg.AnnotationMaximumTagsLength = 60 diff --git a/pkg/services/apiserver/client/client.go b/pkg/services/apiserver/client/client.go index efeee1c573d..67fc5f1c48f 100644 --- a/pkg/services/apiserver/client/client.go +++ b/pkg/services/apiserver/client/client.go @@ -14,6 +14,9 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" + k8sUser "k8s.io/apiserver/pkg/authentication/user" + k8sRequest "k8s.io/apiserver/pkg/endpoints/request" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher" @@ -21,10 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/storage/unified/resource" - k8sUser "k8s.io/apiserver/pkg/authentication/user" - k8sRequest "k8s.io/apiserver/pkg/endpoints/request" ) type K8sHandler interface { @@ -46,15 +46,15 @@ type k8sHandler struct { namespacer request.NamespaceMapper gvr schema.GroupVersionResource restConfig func(context.Context) *rest.Config - searcher func(context.Context) resource.ResourceIndexClient + searcher resource.ResourceIndexClient userService user.Service } func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, - restConfig func(context.Context) *rest.Config, dashStore dashboards.Store, userSvc user.Service) K8sHandler { + restConfig func(context.Context) *rest.Config, dashStore dashboards.Store, userSvc user.Service, resourceClient resource.ResourceClient) K8sHandler { legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore) key := gvr.Resource + "." + gvr.Group // the unified storage key in the config.ini is resource + group - searchClient := resource.NewSearchClient(cfg, key, unified.GetResourceClient, legacySearcher) + searchClient := resource.NewSearchClient(cfg, key, resourceClient, legacySearcher) return &k8sHandler{ namespacer: namespacer, @@ -191,12 +191,12 @@ func (h *k8sHandler) Search(ctx context.Context, orgID int64, in *resource.Resou } } - return h.searcher(ctx).Search(ctx, in) + return h.searcher.Search(ctx, in) } func (h *k8sHandler) GetStats(ctx context.Context, orgID int64) (*resource.ResourceStatsResponse, error) { // goes directly through grpc, so doesn't need the new context - return h.searcher(ctx).GetStats(ctx, &resource.ResourceStatsRequest{ + return h.searcher.GetStats(ctx, &resource.ResourceStatsRequest{ Namespace: h.GetNamespace(orgID), Kinds: []string{ h.gvr.Group + "/" + h.gvr.Resource, diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index 9ec6d2f5708..579325ca285 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -302,7 +302,7 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) { folderStore := folderimpl.ProvideStore(sqlStore) folderSvc := folderimpl.ProvideService( folderStore, mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) parentUID := "" for i := 0; ; i++ { diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 79b94fa7ab7..3cddfb9c622 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -928,7 +928,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) { fStore := folderimpl.ProvideStore(sqlStore) folderServiceWithFlagOn := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) user := &user.SignedInUser{ OrgID: 1, @@ -1048,7 +1048,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) { folderServiceWithFlagOn := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) user := &user.SignedInUser{ OrgID: 1, diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index d1f78e34cb4..f8ca348421e 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -95,8 +95,9 @@ func ProvideDashboardServiceImpl( ac accesscontrol.AccessControl, folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer, restConfigProvider apiserver.RestConfigProvider, userService user.Service, quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper, + resourceClient resource.ResourceClient, ) (*DashboardServiceImpl, error) { - k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashboardStore, userService) + k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashboardStore, userService, resourceClient) dashSvc := &DashboardServiceImpl{ cfg: cfg, diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index 470ba022383..7d91fafba95 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -895,7 +895,8 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc publicDashboardFakeService, cfg, nil, - tracer) + tracer, + nil) dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() dashboardService, err := ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, @@ -910,6 +911,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc quotaService, nil, nil, + nil, ) dashboardService.RegisterDashboardPermissions(dashboardPermissions) require.NoError(t, err) @@ -981,7 +983,8 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt publicDashboardFakeService, cfg, nil, - tracer) + tracer, + nil) dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() dashboardPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) @@ -998,6 +1001,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt quotaService, nil, nil, + nil, ) require.NoError(t, err) service.RegisterDashboardPermissions(dashboardPermissions) @@ -1030,7 +1034,8 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto publicDashboardFakeService, cfg, nil, - tracer) + tracer, + nil) service, err := ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, featuremgmt.WithFeatures(), @@ -1044,6 +1049,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto quotaService, nil, nil, + nil, ) require.NoError(t, err) service.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) @@ -1095,7 +1101,8 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string publicDashboardFakeService, cfg, nil, - tracer) + tracer, + nil) service, err := ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, @@ -1109,6 +1116,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string quotaService, nil, nil, + nil, ) require.NoError(t, err) service.RegisterDashboardPermissions(dashboardPermissions) @@ -1166,7 +1174,8 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da publicDashboardFakeService, cfg, nil, - tracer) + tracer, + nil) folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) service, err := ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, @@ -1181,6 +1190,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da quotaService, nil, nil, + nil, ) require.NoError(t, err) service.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index b125e159ed4..14558d2fcdc 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -116,6 +116,7 @@ func TestValidateDashboardExists(t *testing.T) { quotatest.New(false, nil), nil, nil, + nil, ) require.NoError(t, err) s := ProvideService(dsStore, secretsService, dashSvc) diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 8b83d379f5a..7cd929b26f2 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -7,6 +7,9 @@ import ( "strconv" "strings" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/components/simplejson" @@ -21,8 +24,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) const ( @@ -55,6 +56,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.Dash restConfigProvider.GetRestConfig, dashboardStore, userService, + unified, ), dashSvc: dashboardService, log: log.New("dashboard-version"), diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 0b4dc311fa4..7b4561fabee 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -44,6 +44,7 @@ import ( "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/util" ) @@ -85,6 +86,7 @@ func ProvideService( cfg *setting.Cfg, r prometheus.Registerer, tracer tracing.Tracer, + resourceClient resource.ResourceClient, ) *Service { srv := &Service{ log: slog.Default().With("logger", "folder-service"), @@ -115,6 +117,7 @@ func ProvideService( apiserver.GetRestConfig, dashboardStore, userService, + resourceClient, ) unifiedStore := ProvideUnifiedStore(k8sHandler, userService) @@ -131,6 +134,7 @@ func ProvideService( apiserver.GetRestConfig, dashboardStore, userService, + resourceClient, ) srv.dashboardK8sClient = dashHandler } diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 87b8787f8fb..2d01db69950 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -67,7 +67,7 @@ func TestIntegrationProvideFolderService(t *testing.T) { store := ProvideStore(db) ProvideService( store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), - nil, nil, nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, nil, nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2) }) @@ -495,7 +495,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { }) publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) - dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService) + dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil) require.NoError(t, err) dashSrv.RegisterDashboardPermissions(dashboardPermissions) @@ -581,7 +581,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff, - folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService) + folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil) require.NoError(t, err) dashSrv.RegisterDashboardPermissions(dashboardPermissions) alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b) @@ -724,7 +724,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { tc.service.store = nestedFolderStore publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) - dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service, tc.service.store, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService) + dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service, tc.service.store, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil) require.NoError(t, err) dashSrv.RegisterDashboardPermissions(dashboardPermissions) @@ -1516,6 +1516,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { quotaService, nil, nil, + nil, ) require.NoError(t, err) dashboardService.RegisterDashboardPermissions(dashboardPermissions) diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 8a7e99a991c..26d7941f95e 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -182,7 +182,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { features := featuremgmt.WithFeatures(featuresArr...) dashboardStore := dashboards.NewFakeDashboardStore(t) - k8sCli := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.FolderResourceInfo.GroupVersionResource(), restCfgProvider.GetRestConfig, dashboardStore, userService) + k8sCli := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.FolderResourceInfo.GroupVersionResource(), restCfgProvider.GetRestConfig, dashboardStore, userService, nil) unifiedStore := ProvideUnifiedStore(k8sCli, userService) ctx := context.Background() diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 8fddbc1e1f2..bef38a44ce8 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -347,7 +347,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash fStore := folderimpl.ProvideStore(sqlStore) folderSvc := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) _, err = folderSvc.Create(context.Background(), &folder.CreateFolderCommand{UID: folderUID, SignedInUser: &user, Title: folderUID + "-title"}) require.NoError(t, err) service, err := dashboardservice.ProvideDashboardServiceImpl( @@ -361,6 +361,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash quotaService, nil, nil, + nil, ) require.NoError(t, err) service.RegisterDashboardPermissions(dashboardPermissions) @@ -384,7 +385,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string, folderSvc fold store := folderimpl.ProvideStore(sc.sqlStore) folderSvc = folderimpl.ProvideService( store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) t.Logf("Creating folder with title %q and UID uid_for_%s", title, title) } ctx := identity.WithRequester(context.Background(), &sc.user) @@ -448,12 +449,12 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena fStore := folderimpl.ProvideStore(sqlStore) folderSvc := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) dashboardService, svcErr := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, ac, folderSvc, fStore, - nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, + nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, ) require.NoError(t, svcErr) dashboardService.RegisterDashboardPermissions(dashboardPermissions) @@ -517,7 +518,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo publicDash.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) folderSvc := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), publicDash, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), publicDash, cfg, nil, tracing.InitializeTracerForTest(), nil) alertStore, err := ngstore.ProvideDBStore(cfg, features, sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) err = folderSvc.RegisterService(alertStore) @@ -526,7 +527,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo cfg, dashboardStore, folderStore, features, folderPermissions, ac, folderSvc, fStore, - nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, + nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, ) require.NoError(t, dashSvcErr) dashService.RegisterDashboardPermissions(dashboardPermissions) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 75980513f1b..25e3a3f3108 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -736,7 +736,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash cfg, dashboardStore, folderStore, features, acmock.NewMockedPermissionsService(), ac, foldertest.NewFakeService(), folder.NewFakeStore(), - nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, + nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, ) require.NoError(t, err) service.RegisterDashboardPermissions(dashPermissionService) @@ -758,7 +758,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder fStore := folderimpl.ProvideStore(sc.sqlStore) s := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) t.Logf("Creating folder with title and UID %q", title) ctx := identity.WithRequester(context.Background(), sc.user) @@ -834,7 +834,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo cfg, dashStore, folderStore, features, acmock.NewMockedPermissionsService(), ac, folderSvc, folder.NewFakeStore(), - nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, + nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, ) require.NoError(t, err) dashService.RegisterDashboardPermissions(dashPermissionService) @@ -846,7 +846,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo folderService := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, features, ac, dashService) service := LibraryPanelService{ diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index f35c6236258..895c4940342 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -1921,7 +1921,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { fStore := folderimpl.ProvideStore(sqlStore) folderService := folderimpl.ProvideService( fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) store := store.DBstore{ Logger: log, SQLStore: sqlStore, diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index 6ac4cfa1f89..230d1480345 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -1606,7 +1606,7 @@ func TestProvisiongWithFullpath(t *testing.T) { fStore := folderimpl.ProvideStore(sqlStore) folderService := folderimpl.ProvideService( fStore, ac, inProcBus, dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) ruleService := createAlertRuleService(t, folderService) var orgID int64 = 1 diff --git a/pkg/services/ngalert/testutil/testutil.go b/pkg/services/ngalert/testutil/testutil.go index b7175b5dca6..b9e4e19ba99 100644 --- a/pkg/services/ngalert/testutil/testutil.go +++ b/pkg/services/ngalert/testutil/testutil.go @@ -32,7 +32,7 @@ func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStor tb.Helper() fStore := folderimpl.ProvideStore(db) return folderimpl.ProvideService(fStore, ac, bus, dashboardStore, folderStore, nil, db, - features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) } func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) { @@ -63,7 +63,7 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa cfg, dashboardStore, fs, features, folderPermissions, ac, foldertest.NewFakeService(), folder.NewFakeStore(), - nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, + nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, ) require.NoError(tb, err) dashboardService.RegisterDashboardPermissions(dashboardPermissions) diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index bdc8aebc26a..4be2d2c01a1 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -131,7 +131,7 @@ func TestDashboardFileReader(t *testing.T) { folderStore := folderimpl.ProvideDashboardFolderStore(sql) folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(), - supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest()) + supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest(), nil) t.Run("Reading dashboards from disk", func(t *testing.T) { t.Run("Can read default dashboard", func(t *testing.T) { diff --git a/pkg/services/provisioning/dashboards/validator_test.go b/pkg/services/provisioning/dashboards/validator_test.go index 0b13f3092fa..86026f5dd62 100644 --- a/pkg/services/provisioning/dashboards/validator_test.go +++ b/pkg/services/provisioning/dashboards/validator_test.go @@ -50,7 +50,7 @@ func TestDuplicatesValidator(t *testing.T) { folderStore := folderimpl.ProvideDashboardFolderStore(sql) folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(), - supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest()) + supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest(), nil) t.Run("Duplicates validator should collect info about duplicate UIDs and titles within folders", func(t *testing.T) { const folderName = "duplicates-validator-folder" diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index e81d34c6d37..6739d9f1db4 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -327,6 +327,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) cfg, dashboardStoreService, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), ac, foldertest.NewFakeService(), folder.NewFakeStore(), nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, + nil, ) require.NoError(t, err) dashService.RegisterDashboardPermissions(dashPermissionService) diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index 6605c391488..9328ff3ac3d 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -1397,9 +1397,9 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { folderStore := folderimpl.ProvideDashboardFolderStore(testDB) folderSvc := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, - nil, testDB, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, testDB, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) - dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil) + dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil) require.NoError(t, err) dashboardService.RegisterDashboardPermissions(&actest.FakePermissionsService{}) fakeGuardian := &guardian.FakeDashboardGuardian{ diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 5f70f869e3a..5ead693e8b8 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -495,9 +495,9 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) folderSvc := folderimpl.ProvideService( fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, - nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) dashService, err := dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), - ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil) + ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil) require.NoError(t, err) dashService.RegisterDashboardPermissions(acmock.NewMockedPermissionsService()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go index f0f2fe1d7f7..8dde8f430c5 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go @@ -55,8 +55,7 @@ func setupTestEnv(t *testing.T) *TestEnv { acSvc: acimpl.ProvideOSSService( cfg, env.AcStore, &resourcepermissions.FakeActionSetSvc{}, localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest(), nil, - permreg.ProvidePermissionRegistry(), nil, nil, - ), + permreg.ProvidePermissionRegistry(), nil), defaultOrgID: autoAssignOrgID, logger: logger, metrics: newMetrics(nil, autoAssignOrgID, env.SaSvc, logger), diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 739ff8619bf..006ae7eb102 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -824,7 +824,7 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol fStore := folderimpl.ProvideStore(db) folderSvc := folderimpl.ProvideService( fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(db), - nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) // create parent folder parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ diff --git a/pkg/services/sqlstore/permissions/dashboards_bench_test.go b/pkg/services/sqlstore/permissions/dashboards_bench_test.go index c82b3468c5e..1a9228666e4 100644 --- a/pkg/services/sqlstore/permissions/dashboards_bench_test.go +++ b/pkg/services/sqlstore/permissions/dashboards_bench_test.go @@ -81,7 +81,7 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe fStore := folderimpl.ProvideStore(store) folderSvc := folderimpl.ProvideService( fStore, mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), - nil, store, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, store, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true}) diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index b8ecda9cbaf..50f144e39ab 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -28,17 +28,6 @@ import ( const resourceStoreAudience = "resourceStore" -var ( - // internal provider of the package level resource client - pkgResourceClient resource.ResourceClient - ready = make(chan struct{}) -) - -func GetResourceClient(ctx context.Context) resource.ResourceClient { - <-ready - return pkgResourceClient -} - type Options struct { Cfg *setting.Cfg Features featuremgmt.FeatureToggles @@ -67,12 +56,6 @@ func ProvideUnifiedStorageClient(opts *Options) (resource.ResourceClient, error) ) } - // only set the package level restConfig once - if pkgResourceClient == nil { - pkgResourceClient = client - close(ready) - } - return client, err } diff --git a/pkg/storage/unified/federated/federatedtests/stats_test.go b/pkg/storage/unified/federated/federatedtests/stats_test.go index bdb54e1f00d..6249de47afe 100644 --- a/pkg/storage/unified/federated/federatedtests/stats_test.go +++ b/pkg/storage/unified/federated/federatedtests/stats_test.go @@ -52,7 +52,7 @@ func TestDirectSQLStats(t *testing.T) { fStore := folderimpl.ProvideStore(db) folderSvc := folderimpl.ProvideService( fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(db), - nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil) // create parent folder diff --git a/pkg/storage/unified/resource/search_client.go b/pkg/storage/unified/resource/search_client.go index 8615a8cbed2..deb97bddea2 100644 --- a/pkg/storage/unified/resource/search_client.go +++ b/pkg/storage/unified/resource/search_client.go @@ -1,22 +1,20 @@ package resource import ( - "context" - "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/setting" ) -func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient func(context.Context) ResourceClient, legacyClient ResourceIndexClient) func(context.Context) ResourceIndexClient { +func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient ResourceClient, legacyClient ResourceIndexClient) ResourceIndexClient { config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey] if !ok { - return func(ctx context.Context) ResourceIndexClient { return legacyClient } + return legacyClient } switch config.DualWriterMode { case rest.Mode0, rest.Mode1, rest.Mode2: - return func(ctx context.Context) ResourceIndexClient { return legacyClient } + return legacyClient default: - return func(ctx context.Context) ResourceIndexClient { return unifiedClient(ctx) } + return unifiedClient } } From 9e3872f8dda1887bb9cbacd1c937807c1c047ab8 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:38:32 +0100 Subject: [PATCH 605/894] Alerting: Disable create rule menu item from panel when unifiedAlerting is disabled (#100701) * add config.unifiedAlertingEnabled check to render create alerts menu item from panels * Disable create rule from panel when unifiedAlerting is disabled * fix test and lint * fix test --- .../alerting/unified/PanelAlertTabContent.test.tsx | 3 +++ .../features/alerting/unified/PanelAlertTabContent.tsx | 3 ++- .../PanelDataPane/PanelDataAlertingTab.test.tsx | 10 +++++----- .../panel-edit/PanelDataPane/PanelDataAlertingTab.tsx | 7 ++++++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx index 488719b2e65..d48f5de0341 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx @@ -3,6 +3,7 @@ import { byTestId, byText } from 'testing-library-selector'; import { PromOptions } from '@grafana/prometheus'; import { setPluginLinksHook } from '@grafana/runtime'; +import config from 'app/core/config'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; @@ -212,6 +213,7 @@ describe('PanelAlertTabContent', () => { mockAlertRuleApi(server).prometheusRuleNamespaces(GRAFANA_RULES_SOURCE_NAME, promResponse); mockAlertRuleApi(server).rulerRules(GRAFANA_RULES_SOURCE_NAME, rulerResponse); + config.unifiedAlertingEnabled = true; }); it('Will take into account panel maxDataPoints', async () => { @@ -329,6 +331,7 @@ describe('PanelAlertTabContent', () => { }); it('Will render alerts belonging to panel and a button to create alert from panel queries', async () => { + config.unifiedAlertingEnabled = true; renderAlertTabContent(dashboard, panel); const rows = await ui.row.findAll(); diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.tsx index f4d527640a6..b0bd57380a9 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { config } from '@grafana/runtime'; import { Alert, LoadingPlaceholder, ScrollContainer, useStyles2 } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import { contextSrv } from 'app/core/services/context_srv'; @@ -27,7 +28,7 @@ export const PanelAlertTabContent = ({ dashboard, panel }: Props) => { poll: true, }); const permissions = getRulesPermissions('grafana'); - const canCreateRules = contextSrv.hasPermission(permissions.create); + const canCreateRules = config.unifiedAlertingEnabled && contextSrv.hasPermission(permissions.create); const alert = errors.length ? ( diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx index 1d9e2764ce8..8995a7a0c30 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx @@ -4,7 +4,7 @@ import { byTestId } from 'testing-library-selector'; import { DataSourceApi } from '@grafana/data'; import { PromOptions, PrometheusDatasource } from '@grafana/prometheus'; -import { locationService, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime'; +import { config, locationService, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; import * as ruler from 'app/features/alerting/unified/api/ruler'; import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; @@ -21,7 +21,7 @@ import { mockRulerRuleGroup, } from 'app/features/alerting/unified/mocks'; import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; -import * as config from 'app/features/alerting/unified/utils/config'; +import * as configDS from 'app/features/alerting/unified/utils/config'; import { Annotation } from 'app/features/alerting/unified/utils/constants'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; @@ -44,7 +44,7 @@ import { PanelDataAlertingTab, PanelDataAlertingTabRendered } from './PanelDataA jest.mock('app/features/alerting/unified/api/prometheus'); jest.mock('app/features/alerting/unified/api/ruler'); -jest.spyOn(config, 'getAllDataSources'); +jest.spyOn(configDS, 'getAllDataSources'); jest.spyOn(ruleActionButtons, 'matchesWidth').mockReturnValue(false); jest.spyOn(ruler, 'rulerUrlBuilder'); jest.spyOn(alertingAbilities, 'useAlertRuleAbility'); @@ -70,7 +70,7 @@ dataSources.prometheus.meta.alerting = true; dataSources.default.meta.alerting = true; const mocks = { - getAllDataSources: jest.mocked(config.getAllDataSources), + getAllDataSources: jest.mocked(configDS.getAllDataSources), useAlertRuleAbilityMock: jest.mocked(alertingAbilities.useAlertRuleAbility), rulerBuilderMock: jest.mocked(ruler.rulerUrlBuilder), }; @@ -309,7 +309,7 @@ describe('PanelAlertTabContent', () => { // after updating to RTKQ, the response is already returning the alerts belonging to the panel it('Will render alerts belonging to panel and a button to create alert from panel queries', async () => { dashboard.panels = [panel]; - + config.unifiedAlertingEnabled = true; renderAlertTab(dashboard, dashboard); const rows = await ui.row.findAll(); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.tsx index 5ccbd2af8fe..095e8c776d6 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes'; import { Alert, LoadingPlaceholder, Tab, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; @@ -46,7 +47,11 @@ export class PanelDataAlertingTab extends SceneObjectBase Date: Fri, 14 Feb 2025 11:52:34 +0000 Subject: [PATCH 606/894] New Logs Panel: Add infinite scrolling support (#99773) * Create Infinite Scroll wrapper component * Logs list: refactor event subscriber * Infinitely load logs * Move renderer to Infinite Scroll component * Implement infinite scroll state * Switch internal implementation to use the existing infinite scrolling component logic * Integrate with logs panel * Move scrolling management to infinite scrolling component * LogList: change subscription dependency to prevent unnecessary runs * Infinite scroll: remove autoscrolling * Logs Panel: fix dependencies to prevent re-renders on refresh * Infinite scroll: introduce pre-scroll state * LogList: expose initial log position prop * Infinite scroll: less work on scroll and autoscroll behavior * Remove console * Fix imports * Add infinite scroll translations * Fix imports * Add visual delimiter for new pages and increase gap * Remove log * Chore: rename interface to LogListModel * Hover: decrease opacity * Fix no-logs state * Prettier * Infinite scroll: move scroll delimiter * Load more message: make it clickable --- .../panelcfg/x/LogsNewPanelCfg_types.gen.ts | 1 + .../panelcfg/x/LogsNewPanelCfg_types.gen.ts | 23 ++ public/app/features/explore/Logs/Logs.tsx | 4 +- .../logs/components/InfiniteScroll.tsx | 8 +- .../logs/components/panel/InfiniteScroll.tsx | 213 ++++++++++++++++++ .../logs/components/panel/LogLine.tsx | 40 +++- .../logs/components/panel/LogLineMessage.tsx | 27 +++ .../logs/components/panel/LogList.tsx | 97 ++++---- .../logs/components/panel/processing.ts | 9 +- .../logs/components/panel/virtualization.ts | 11 +- .../app/plugins/panel/logs-new/LogsPanel.tsx | 76 +++++-- .../app/plugins/panel/logs-new/panelcfg.cue | 1 + .../plugins/panel/logs-new/panelcfg.gen.ts | 1 + public/app/plugins/panel/logs-new/plugin.json | 2 +- public/app/plugins/panel/logs/LogsPanel.tsx | 2 +- public/app/plugins/panel/logs/module.tsx | 2 +- public/locales/en-US/grafana.json | 4 + public/locales/pseudo-LOCALE/grafana.json | 4 + 18 files changed, 438 insertions(+), 87 deletions(-) create mode 100644 packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts create mode 100644 public/app/features/logs/components/panel/InfiniteScroll.tsx create mode 100644 public/app/features/logs/components/panel/LogLineMessage.tsx diff --git a/packages/grafana-schema/src/raw/composable/logs(new)/panelcfg/x/LogsNewPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logs(new)/panelcfg/x/LogsNewPanelCfg_types.gen.ts index 91589b9cea1..b758575dba2 100644 --- a/packages/grafana-schema/src/raw/composable/logs(new)/panelcfg/x/LogsNewPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logs(new)/panelcfg/x/LogsNewPanelCfg_types.gen.ts @@ -16,6 +16,7 @@ export interface Options { dedupStrategy: common.LogsDedupStrategy; enableInfiniteScrolling?: boolean; enableLogDetails: boolean; + onNewLogsReceived?: unknown; showTime: boolean; sortOrder: common.LogsSortOrder; wrapLogMessage: boolean; diff --git a/packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts new file mode 100644 index 00000000000..b758575dba2 --- /dev/null +++ b/packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts @@ -0,0 +1,23 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// public/app/plugins/gen.go +// Using jennies: +// TSTypesJenny +// PluginTsTypesJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +import * as common from '@grafana/schema'; + +export const pluginVersion = "11.6.0-pre"; + +export interface Options { + dedupStrategy: common.LogsDedupStrategy; + enableInfiniteScrolling?: boolean; + enableLogDetails: boolean; + onNewLogsReceived?: unknown; + showTime: boolean; + sortOrder: common.LogsSortOrder; + wrapLogMessage: boolean; +} diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index fb05d6bee91..ae6c8e1f7d0 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -1063,7 +1063,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { /> )} - {visualisationType === 'logs' && config.featureToggles.newLogsPanel && ( + {visualisationType === 'logs' && hasData && config.featureToggles.newLogsPanel && ( <>
          {logsContainerRef.current && ( @@ -1072,9 +1072,11 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { containerElement={logsContainerRef.current} eventBus={eventBus} forceEscape={forceEscape} + loadMore={loadMoreLogs} logs={dedupedRows} showTime={showTime} sortOrder={logsSortOrder} + timeRange={props.range} timeZone={timeZone} wrapLogMessage={wrapLogMessage} /> diff --git a/public/app/features/logs/components/InfiniteScroll.tsx b/public/app/features/logs/components/InfiniteScroll.tsx index 4fffcf4c62e..36d748d2db1 100644 --- a/public/app/features/logs/components/InfiniteScroll.tsx +++ b/public/app/features/logs/components/InfiniteScroll.tsx @@ -214,12 +214,12 @@ const outOfRangeMessage = (
          ); -enum ScrollDirection { +export enum ScrollDirection { Top = -1, Bottom = 1, NoScroll = 0, } -function shouldLoadMore( +export function shouldLoadMore( event: Event | WheelEvent, lastEvent: Event | WheelEvent | null, countRef: MutableRefObject, @@ -284,7 +284,7 @@ function shouldIgnoreChainOfEvents( return true; } -function getVisibleRange(rows: LogRowModel[]) { +export function getVisibleRange(rows: LogRowModel[]) { const firstTimeStamp = rows[0].timeEpochMs; const lastTimeStamp = rows[rows.length - 1].timeEpochMs; @@ -326,7 +326,7 @@ function canScrollTop( return canScroll ? getPrevRange(visibleRange, currentRange) : undefined; } -function canScrollBottom( +export function canScrollBottom( visibleRange: AbsoluteTimeRange, currentRange: TimeRange, timeZone: TimeZone, diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx new file mode 100644 index 00000000000..4f0e7a9584b --- /dev/null +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -0,0 +1,213 @@ +import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; +import { usePrevious } from 'react-use'; +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 { t } from 'app/core/internationalization'; + +import { canScrollBottom, getVisibleRange, ScrollDirection, shouldLoadMore } from '../InfiniteScroll'; + +import { LogLine } from './LogLine'; +import { LogLineMessage } from './LogLineMessage'; +import { LogListModel } from './processing'; + +interface ChildrenProps { + itemCount: number; + getItemKey: (index: number) => string; + onItemsRendered: (props: ListOnItemsRenderedProps) => void; + Renderer: (props: ListChildComponentProps) => ReactNode; +} + +interface Props { + children: (props: ChildrenProps) => ReactNode; + handleOverflow: (index: number, id: string, height: number) => void; + loadMore?: (range: AbsoluteTimeRange) => void; + logs: LogListModel[]; + scrollElement: HTMLDivElement | null; + setInitialScrollPosition: () => void; + showTime: boolean; + sortOrder: LogsSortOrder; + timeRange: TimeRange; + timeZone: string; + wrapLogMessage: boolean; +} + +type InfiniteLoaderState = 'idle' | 'out-of-bounds' | 'pre-scroll' | 'loading'; + +export const InfiniteScroll = ({ + children, + handleOverflow, + loadMore, + logs, + scrollElement, + setInitialScrollPosition, + showTime, + sortOrder, + timeRange, + timeZone, + wrapLogMessage, +}: Props) => { + const [infiniteLoaderState, setInfiniteLoaderState] = useState('idle'); + const [autoScroll, setAutoScroll] = useState(false); + const prevLogs = usePrevious(logs); + const prevSortOrder = usePrevious(sortOrder); + const lastScroll = useRef(scrollElement?.scrollTop || 0); + const lastEvent = useRef(null); + const countRef = useRef(0); + const lastLogOfPage = useRef([]); + + useEffect(() => { + // Logs have not changed, ignore effect + if (!prevLogs || prevLogs === logs) { + return; + } + // New logs are from infinite scrolling + if (infiniteLoaderState === 'loading') { + // out-of-bounds if no new logs returned + setInfiniteLoaderState(logs.length === prevLogs.length ? 'out-of-bounds' : 'idle'); + } else { + lastLogOfPage.current = []; + setAutoScroll(true); + } + }, [infiniteLoaderState, logs, prevLogs]); + + useEffect(() => { + if (prevSortOrder && prevSortOrder !== sortOrder) { + setInfiniteLoaderState('idle'); + } + }, [prevSortOrder, sortOrder]); + + useEffect(() => { + if (autoScroll) { + setInitialScrollPosition(); + setAutoScroll(false); + } + }, [autoScroll, setInitialScrollPosition]); + + const onLoadMore = useCallback(() => { + const newRange = canScrollBottom(getVisibleRange(logs), timeRange, timeZone, sortOrder); + if (!newRange) { + setInfiniteLoaderState('out-of-bounds'); + return; + } + lastLogOfPage.current.push(logs[logs.length - 1].uid); + setInfiniteLoaderState('loading'); + loadMore?.(newRange); + + reportInteraction('grafana_logs_infinite_scrolling', { + direction: 'bottom', + sort_order: sortOrder, + }); + }, [loadMore, logs, sortOrder, timeRange, timeZone]); + + useEffect(() => { + if (!scrollElement || !loadMore || !config.featureToggles.logsInfiniteScrolling) { + return; + } + + function handleScroll(event: Event | WheelEvent) { + if (!scrollElement || !loadMore || !logs.length || infiniteLoaderState !== 'pre-scroll') { + return; + } + const scrollDirection = shouldLoadMore(event, lastEvent.current, countRef, scrollElement, lastScroll.current); + lastEvent.current = event; + lastScroll.current = scrollElement.scrollTop; + if (scrollDirection === ScrollDirection.Bottom) { + onLoadMore(); + } + } + + scrollElement.addEventListener('scroll', handleScroll); + scrollElement.addEventListener('wheel', handleScroll); + + return () => { + scrollElement.removeEventListener('scroll', handleScroll); + scrollElement.removeEventListener('wheel', handleScroll); + }; + }, [infiniteLoaderState, loadMore, logs.length, onLoadMore, scrollElement]); + + const Renderer = useCallback( + ({ index, style }: ListChildComponentProps) => { + if (!logs[index] && infiniteLoaderState !== 'idle') { + return ( + + {getMessageFromInfiniteLoaderState(infiniteLoaderState, sortOrder)} + + ); + } + return ( + + ); + }, + [handleOverflow, infiniteLoaderState, logs, onLoadMore, showTime, sortOrder, wrapLogMessage] + ); + + const onItemsRendered = useCallback( + (props: ListOnItemsRenderedProps) => { + if (!scrollElement || infiniteLoaderState === 'loading' || infiniteLoaderState === 'out-of-bounds') { + return; + } + if (scrollElement.scrollHeight <= scrollElement.clientHeight) { + return; + } + const lastLogIndex = logs.length - 1; + const preScrollIndex = logs.length - 2; + if (props.visibleStopIndex >= lastLogIndex) { + setInfiniteLoaderState('pre-scroll'); + } else if (props.visibleStartIndex < preScrollIndex) { + setInfiniteLoaderState('idle'); + } + }, + [infiniteLoaderState, logs.length, scrollElement] + ); + + const getItemKey = useCallback((index: number) => (logs[index] ? logs[index].uid : index.toString()), [logs]); + + const itemCount = logs.length && loadMore && infiniteLoaderState !== 'idle' ? logs.length + 1 : logs.length; + + return <>{children({ getItemKey, itemCount, onItemsRendered, Renderer })}; +}; + +function getMessageFromInfiniteLoaderState(state: InfiniteLoaderState, order: LogsSortOrder) { + switch (state) { + case 'out-of-bounds': + return t('logs.infinite-scroll.end-of-range', 'End of the selected time range.'); + case 'loading': + return ( + <> + {order === LogsSortOrder.Ascending + ? t('logs.infinite-scroll.load-newer', 'Loading newer logs...') + : t('logs.infinite-scroll.load-older', 'Loading older logs...')}{' '} + + + ); + case 'pre-scroll': + return t('logs.infinite-scroll.load-more', 'Scroll to load more'); + default: + return null; + } +} + +function getLogLineVariant(logs: LogListModel[], index: number, lastLogOfPage: string[]) { + if (!lastLogOfPage.length || !logs[index - 1]) { + return undefined; + } + const prevLog = logs[index - 1]; + for (const uid of lastLogOfPage) { + if (prevLog.uid === uid) { + // First log of an infinite scrolling page + return 'infinite-scroll'; + } + } + return undefined; +} diff --git a/public/app/features/logs/components/panel/LogLine.tsx b/public/app/features/logs/components/panel/LogLine.tsx index 02b2b464dff..bba06ff1ab0 100644 --- a/public/app/features/logs/components/panel/LogLine.tsx +++ b/public/app/features/logs/components/panel/LogLine.tsx @@ -4,19 +4,20 @@ import { CSSProperties, useEffect, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useTheme2 } from '@grafana/ui'; -import { ProcessedLogModel } from './processing'; +import { LogListModel } from './processing'; import { hasUnderOrOverflow } from './virtualization'; interface Props { index: number; - log: ProcessedLogModel; + log: LogListModel; showTime: boolean; style: CSSProperties; onOverflow?: (index: number, id: string, height: number) => void; + variant?: 'infinite-scroll'; wrapLogMessage: boolean; } -export const LogLine = ({ index, log, style, onOverflow, showTime, wrapLogMessage }: Props) => { +export const LogLine = ({ index, log, style, onOverflow, showTime, variant, wrapLogMessage }: Props) => { const theme = useTheme2(); const styles = getStyles(theme); const logLineRef = useRef(null); @@ -33,7 +34,7 @@ export const LogLine = ({ index, log, style, onOverflow, showTime, wrapLogMessag }, [index, log.uid, onOverflow, style.height]); return ( -
          +
          {showTime && {log.timestamp}} {log.logLevel && {log.logLevel}} @@ -43,7 +44,7 @@ export const LogLine = ({ index, log, style, onOverflow, showTime, wrapLogMessag ); }; -const getStyles = (theme: GrafanaTheme2) => { +export const getStyles = (theme: GrafanaTheme2) => { const colors = { critical: '#B877D9', error: '#FF5286', @@ -60,8 +61,23 @@ const getStyles = (theme: GrafanaTheme2) => { fontSize: theme.typography.fontSize, wordBreak: 'break-all', '&:hover': { - opacity: 0.9, + opacity: 0.7, }, + '&.infinite-scroll': { + '&::before': { + borderTop: `solid 1px ${theme.colors.border.strong}`, + content: '""', + height: 0, + left: 0, + position: 'absolute', + top: -3, + width: '100%', + }, + }, + }), + logLineMessage: css({ + fontFamily: theme.typography.fontFamily, + textAlign: 'center', }), timestamp: css({ color: theme.colors.text.secondary, @@ -73,6 +89,9 @@ const getStyles = (theme: GrafanaTheme2) => { '&.level-error': { color: colors.error, }, + '&.level-info': { + color: colors.info, + }, '&.level-warning': { color: colors.warning, }, @@ -101,16 +120,21 @@ const getStyles = (theme: GrafanaTheme2) => { color: colors.debug, }, }), + loadMoreButton: css({ + background: 'transparent', + border: 'none', + display: 'inline', + }), overflows: css({ outline: 'solid 1px red', }), unwrappedLogLine: css({ whiteSpace: 'pre', - paddingBottom: theme.spacing(0.5), + paddingBottom: theme.spacing(0.75), }), wrappedLogLine: css({ whiteSpace: 'pre-wrap', - paddingBottom: theme.spacing(0.5), + paddingBottom: theme.spacing(0.75), }), }; }; diff --git a/public/app/features/logs/components/panel/LogLineMessage.tsx b/public/app/features/logs/components/panel/LogLineMessage.tsx new file mode 100644 index 00000000000..2bdff1c03f5 --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineMessage.tsx @@ -0,0 +1,27 @@ +import { CSSProperties, ReactNode } from 'react'; + +import { useTheme2 } from '@grafana/ui'; + +import { getStyles } from './LogLine'; + +interface Props { + children: ReactNode; + onClick?: () => void; + style: CSSProperties; +} + +export const LogLineMessage = ({ children, onClick, style }: Props) => { + const theme = useTheme2(); + const styles = getStyles(theme); + return ( +
          + {onClick ? ( + + ) : ( + children + )} +
          + ); +}; diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index 8c08e96bfef..9e42cb73214 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -1,12 +1,12 @@ import { debounce } from 'lodash'; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { ListChildComponentProps, VariableSizeList } from 'react-window'; +import { VariableSizeList } from 'react-window'; -import { CoreApp, EventBus, LogRowModel, LogsSortOrder } from '@grafana/data'; +import { AbsoluteTimeRange, CoreApp, EventBus, LogRowModel, LogsSortOrder, TimeRange } from '@grafana/data'; import { useTheme2 } from '@grafana/ui'; -import { LogLine } from './LogLine'; -import { preProcessLogs, ProcessedLogModel } from './processing'; +import { InfiniteScroll } from './InfiniteScroll'; +import { preProcessLogs, LogListModel } from './processing'; import { getLogLineSize, init as initVirtualization, @@ -21,8 +21,11 @@ interface Props { containerElement: HTMLDivElement; eventBus: EventBus; forceEscape?: boolean; + initialScrollPosition?: 'top' | 'bottom'; + loadMore?: (range: AbsoluteTimeRange) => void; showTime: boolean; sortOrder: LogsSortOrder; + timeRange: TimeRange; timeZone: string; wrapLogMessage: boolean; } @@ -30,41 +33,40 @@ interface Props { export const LogList = ({ app, containerElement, - logs, eventBus, forceEscape = false, + initialScrollPosition = 'top', + loadMore, + logs, showTime, sortOrder, + timeRange, timeZone, wrapLogMessage, }: Props) => { - const [processedLogs, setProcessedLogs] = useState([]); + const [processedLogs, setProcessedLogs] = useState([]); const [listHeight, setListHeight] = useState( app === CoreApp.Explore ? window.innerHeight * 0.75 : containerElement.clientHeight ); const theme = useTheme2(); const listRef = useRef(null); const widthRef = useRef(containerElement.clientWidth); + const scrollRef = useRef(null); useEffect(() => { initVirtualization(theme); }, [theme]); useEffect(() => { - const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) => { - if (e.payload.scrollTo === 'top') { - listRef.current?.scrollTo(0); - } else { - listRef.current?.scrollToItem(processedLogs.length - 1); - } - }); + const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) => + handleScrollToEvent(e, logs.length, listRef.current) + ); return () => subscription.unsubscribe(); - }, [eventBus, processedLogs.length]); + }, [eventBus, logs.length]); useEffect(() => { setProcessedLogs(preProcessLogs(logs, { wrap: wrapLogMessage, escape: forceEscape, order: sortOrder, timeZone })); listRef.current?.resetAfterIndex(0); - listRef.current?.scrollTo(0); }, [forceEscape, logs, sortOrder, timeZone, wrapLogMessage]); useEffect(() => { @@ -97,21 +99,9 @@ export const LogList = ({ [containerElement] ); - const Renderer = useCallback( - ({ index, style }: ListChildComponentProps) => { - return ( - - ); - }, - [handleOverflow, processedLogs, showTime, wrapLogMessage] - ); + const handleScrollPosition = useCallback(() => { + listRef.current?.scrollToItem(initialScrollPosition === 'top' ? 0 : logs.length - 1); + }, [initialScrollPosition, logs.length]); if (!containerElement || listHeight == null) { // Wait for container to be rendered @@ -119,17 +109,42 @@ export const LogList = ({ } return ( - processedLogs[index].uid} - layout="vertical" - ref={listRef} - style={{ overflowY: 'scroll' }} - width="100%" + - {Renderer} - + {({ getItemKey, itemCount, onItemsRendered, Renderer }) => ( + + {Renderer} + + )} + ); }; + +function handleScrollToEvent(event: ScrollToLogsEvent, logsCount: number, list: VariableSizeList | null) { + if (event.payload.scrollTo === 'top') { + list?.scrollTo(0); + } else { + list?.scrollToItem(logsCount - 1); + } +} diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 68986505ccc..77f525614b5 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -4,7 +4,7 @@ import { escapeUnescapedString, sortLogRows } from '../../utils'; import { measureTextWidth } from './virtualization'; -export interface ProcessedLogModel extends LogRowModel { +export interface LogListModel extends LogRowModel { body: string; timestamp: string; dimensions: LogDimensions; @@ -25,7 +25,7 @@ interface PreProcessOptions { export const preProcessLogs = ( logs: LogRowModel[], { escape, order, timeZone, wrap }: PreProcessOptions -): ProcessedLogModel[] => { +): LogListModel[] => { const orderedLogs = sortLogRows(logs, order); return orderedLogs.map((log) => preProcessLog(log, { wrap, escape, timeZone, expanded: false })); }; @@ -36,10 +36,7 @@ interface PreProcessLogOptions { timeZone: string; wrap: boolean; } -const preProcessLog = ( - log: LogRowModel, - { escape, expanded, timeZone, wrap }: PreProcessLogOptions -): ProcessedLogModel => { +const preProcessLog = (log: LogRowModel, { escape, expanded, timeZone, wrap }: PreProcessLogOptions): LogListModel => { let body = log.entry; const timestamp = dateTimeFormat(log.timeEpochMs, { timeZone, diff --git a/public/app/features/logs/components/panel/virtualization.ts b/public/app/features/logs/components/panel/virtualization.ts index 6c9bfbfbb30..201cc4c82ed 100644 --- a/public/app/features/logs/components/panel/virtualization.ts +++ b/public/app/features/logs/components/panel/virtualization.ts @@ -1,10 +1,10 @@ import { BusEventWithPayload, GrafanaTheme2 } from '@grafana/data'; -import { ProcessedLogModel } from './processing'; +import { LogListModel } from './processing'; let ctx: CanvasRenderingContext2D | null = null; let gridSize = 8; -let paddingBottom = gridSize * 0.5; +let paddingBottom = gridSize * 0.75; let lineHeight = 22; let measurementMode: 'canvas' | 'dom' = 'canvas'; @@ -16,7 +16,7 @@ export function init(theme: GrafanaTheme2) { initCanvasMeasurement(font, letterSpacing); gridSize = theme.spacing.gridSize; - paddingBottom = gridSize * 0.5; + paddingBottom = gridSize * 0.75; lineHeight = theme.typography.fontSize * theme.typography.body.lineHeight; widthMap = new Map(); @@ -144,7 +144,7 @@ interface DisplayOptions { } export function getLogLineSize( - logs: ProcessedLogModel[], + logs: LogListModel[], container: HTMLDivElement | null, { wrap, showTime }: DisplayOptions, index: number @@ -152,7 +152,8 @@ export function getLogLineSize( if (!container) { return 0; } - if (!wrap) { + // !logs[index] means the line is not yet loaded by infinite scrolling + if (!wrap || !logs[index]) { return lineHeight + paddingBottom; } const storedSize = retrieveLogLineSize(logs[index].uid, container); diff --git a/public/app/plugins/panel/logs-new/LogsPanel.tsx b/public/app/plugins/panel/logs-new/LogsPanel.tsx index c6a00fd2322..44dea0e5f54 100644 --- a/public/app/plugins/panel/logs-new/LogsPanel.tsx +++ b/public/app/plugins/panel/logs-new/LogsPanel.tsx @@ -1,13 +1,24 @@ import { css } from '@emotion/css'; -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { CoreApp, GrafanaTheme2, LogsSortOrder, PanelProps } from '@grafana/data'; +import { + AbsoluteTimeRange, + CoreApp, + DataFrame, + GrafanaTheme2, + LoadingState, + LogsSortOrder, + PanelProps, +} from '@grafana/data'; +import { config } from '@grafana/runtime'; import { usePanelContext, useStyles2 } from '@grafana/ui'; import { LogList } from 'app/features/logs/components/panel/LogList'; -import { ScrollToLogsEvent } from 'app/features/logs/components/panel/virtualization'; import { PanelDataErrorView } from 'app/features/panel/components/PanelDataErrorView'; import { dataFrameToLogsModel, dedupLogRows } from '../../../features/logs/logsModel'; +import { requestMoreLogs } from '../logs/LogsPanel'; +import { isOnNewLogsReceivedType } from '../logs/types'; +import { useDatasourcesFromTargets } from '../logs/useDatasourcesFromTargets'; import { Options } from './panelcfg.gen'; @@ -17,45 +28,69 @@ export const LogsPanel = ({ data, timeZone, fieldConfig, - options: { showTime, wrapLogMessage, sortOrder, dedupStrategy }, + options: { dedupStrategy, enableInfiniteScrolling, onNewLogsReceived, showTime, sortOrder, wrapLogMessage }, id, }: LogsPanelProps) => { - const isAscending = sortOrder === LogsSortOrder.Ascending; const style = useStyles2(getStyles); const [logsContainer, setLogsContainer] = useState(null); const [panelData, setPanelData] = useState(data); + const dataSourcesMap = useDatasourcesFromTargets(data.request?.targets); // Prevents the scroll position to change when new data from infinite scrolling is received const keepScrollPositionRef = useRef(false); + // Loading ref to prevent firing multiple requests + const loadingRef = useRef(false); const { eventBus } = usePanelContext(); const logs = useMemo(() => { const logsModel = panelData - ? dataFrameToLogsModel(panelData.series, data.request?.intervalMs, undefined, data.request?.targets) + ? dataFrameToLogsModel(panelData.series, panelData.request?.intervalMs, undefined, panelData.request?.targets) : null; return logsModel ? dedupLogRows(logsModel.rows, dedupStrategy) : []; - }, [data.request?.intervalMs, data.request?.targets, dedupStrategy, panelData]); + }, [dedupStrategy, panelData]); useEffect(() => { - setPanelData(data); + if (data.state !== LoadingState.Loading) { + setPanelData(data); + } }, [data]); - useLayoutEffect(() => { - if (keepScrollPositionRef.current) { - keepScrollPositionRef.current = false; - return; - } + const loadMoreLogs = useCallback( + async (scrollRange: AbsoluteTimeRange) => { + if (!data.request || !config.featureToggles.logsInfiniteScrolling || loadingRef.current) { + return; + } + loadingRef.current = true; + + const onNewLogsReceivedCallback = isOnNewLogsReceivedType(onNewLogsReceived) ? onNewLogsReceived : undefined; + + let newSeries: DataFrame[] = []; + try { + newSeries = await requestMoreLogs(dataSourcesMap, panelData, scrollRange, timeZone, onNewLogsReceivedCallback); + } catch (e) { + console.error(e); + } finally { + loadingRef.current = false; + } + + keepScrollPositionRef.current = true; + setPanelData({ + ...panelData, + series: newSeries, + }); + }, + [data.request, dataSourcesMap, onNewLogsReceived, panelData, timeZone] + ); + + const initialScrollPosition = useMemo(() => { /** * In dashboards, users with newest logs at the bottom have the expectation of keeping the scroll at the bottom * when new data is received. See https://github.com/grafana/grafana/pull/37634 */ if (data.request?.app === CoreApp.Dashboard || data.request?.app === CoreApp.PanelEditor) { - eventBus.publish( - new ScrollToLogsEvent({ - scrollTo: isAscending ? 'top' : 'bottom', - }) - ); + return sortOrder === LogsSortOrder.Ascending ? 'bottom' : 'top'; } - }, [data.request?.app, eventBus, isAscending, logs]); + return 'top'; + }, [data.request?.app, sortOrder]); if (!logs.length) { return ; @@ -68,9 +103,12 @@ export const LogsPanel = ({ app={CoreApp.Dashboard} containerElement={logsContainer} eventBus={eventBus} + initialScrollPosition={initialScrollPosition} logs={logs} + loadMore={enableInfiniteScrolling ? loadMoreLogs : undefined} showTime={showTime} sortOrder={sortOrder} + timeRange={data.timeRange} timeZone={timeZone} wrapLogMessage={wrapLogMessage} /> diff --git a/public/app/plugins/panel/logs-new/panelcfg.cue b/public/app/plugins/panel/logs-new/panelcfg.cue index ff5d3351ef3..e7b28449105 100644 --- a/public/app/plugins/panel/logs-new/panelcfg.cue +++ b/public/app/plugins/panel/logs-new/panelcfg.cue @@ -32,6 +32,7 @@ composableKinds: PanelCfg: { sortOrder: common.LogsSortOrder dedupStrategy: common.LogsDedupStrategy enableInfiniteScrolling?: bool + onNewLogsReceived?: _ } @cuetsy(kind="interface") } }] diff --git a/public/app/plugins/panel/logs-new/panelcfg.gen.ts b/public/app/plugins/panel/logs-new/panelcfg.gen.ts index e8d58be30ce..62523679a72 100644 --- a/public/app/plugins/panel/logs-new/panelcfg.gen.ts +++ b/public/app/plugins/panel/logs-new/panelcfg.gen.ts @@ -14,6 +14,7 @@ export interface Options { dedupStrategy: common.LogsDedupStrategy; enableInfiniteScrolling?: boolean; enableLogDetails: boolean; + onNewLogsReceived?: unknown; showTime: boolean; sortOrder: common.LogsSortOrder; wrapLogMessage: boolean; diff --git a/public/app/plugins/panel/logs-new/plugin.json b/public/app/plugins/panel/logs-new/plugin.json index 54357e1cd97..4d02ef9582d 100644 --- a/public/app/plugins/panel/logs-new/plugin.json +++ b/public/app/plugins/panel/logs-new/plugin.json @@ -1,6 +1,6 @@ { "type": "panel", - "name": "Logs (new)", + "name": "Logs new", "id": "logs-new", "state": "alpha", diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index 51ae908d9b5..a335e830aca 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -563,7 +563,7 @@ async function copyDashboardUrl(row: LogRowModel, rows: LogRowModel[], timeRange return Promise.resolve(); } -async function requestMoreLogs( +export async function requestMoreLogs( dataSourcesMap: Map, panelData: PanelData, timeRange: AbsoluteTimeRange, diff --git a/public/app/plugins/panel/logs/module.tsx b/public/app/plugins/panel/logs/module.tsx index f6d3aa142fa..45952d81f89 100644 --- a/public/app/plugins/panel/logs/module.tsx +++ b/public/app/plugins/panel/logs/module.tsx @@ -1,8 +1,8 @@ import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription } from '@grafana/data'; import { LogsPanel } from './LogsPanel'; +import { Options } from './panelcfg.gen'; import { LogsPanelSuggestionsSupplier } from './suggestions'; -import { Options } from './types'; export const plugin = new PanelPlugin(LogsPanel) .setPanelOptions((builder) => { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 27301c8765b..1872daf2358 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2012,6 +2012,10 @@ }, "logs": { "infinite-scroll": { + "end-of-range": "End of the selected time range.", + "load-more": "Scroll to load more", + "load-newer": "Loading newer logs...", + "load-older": "Loading older logs...", "older-logs": "Older logs" }, "log-details": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 59371299812..0df58885897 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2012,6 +2012,10 @@ }, "logs": { "infinite-scroll": { + "end-of-range": "Ēʼnđ őƒ ŧĥę şęľęčŧęđ ŧįmę řäʼnģę.", + "load-more": "Ŝčřőľľ ŧő ľőäđ mőřę", + "load-newer": "Ŀőäđįʼnģ ʼnęŵęř ľőģş...", + "load-older": "Ŀőäđįʼnģ őľđęř ľőģş...", "older-logs": "Øľđęř ľőģş" }, "log-details": { From 101c590f34a3d509056cce42471d5713d1932f2d Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 14 Feb 2025 13:04:19 +0100 Subject: [PATCH 607/894] Revert "Alerting: Fix loading states" (#100717) Revert "Alerting: Fix loading states (#100641)" This reverts commit ba3a90d8fd8913d95cca522f732696a89b3b3d37. --- .../features/alerting/unified/RuleViewer.tsx | 4 +-- .../alerting/unified/hooks/useCombinedRule.ts | 36 +++++++------------ .../unified/hooks/useIsRuleEditable.ts | 31 +++++----------- .../rule-editor/ExistingRuleEditor.tsx | 17 ++++----- 4 files changed, 30 insertions(+), 58 deletions(-) diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 43c38b699d0..9df84a41b18 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -36,7 +36,7 @@ const RuleViewer = (): JSX.Element => { }, [id]); // we then fetch the rule from the correct API endpoint(s) - const { loading, error, result: rule, uninitialized } = useCombinedRule({ ruleIdentifier: identifier, limitAlerts }); + const { loading, error, result: rule } = useCombinedRule({ ruleIdentifier: identifier, limitAlerts }); if (error) { return ( @@ -46,7 +46,7 @@ const RuleViewer = (): JSX.Element => { ); } - if (loading || uninitialized) { + if (loading) { return ( <> diff --git a/public/app/features/alerting/unified/hooks/useCombinedRule.ts b/public/app/features/alerting/unified/hooks/useCombinedRule.ts index 5d45c65137c..0841b794fb5 100644 --- a/public/app/features/alerting/unified/hooks/useCombinedRule.ts +++ b/public/app/features/alerting/unified/hooks/useCombinedRule.ts @@ -80,7 +80,6 @@ interface RequestState { result?: T; loading: boolean; error?: unknown; - uninitialized: boolean; } interface Props { @@ -100,7 +99,6 @@ export function useCombinedRule({ ruleIdentifier, limitAlerts }: Props): Request loading: isLoadingRuleLocation, error: ruleLocationError, result: ruleLocation, - uninitialized, } = useRuleLocation(ruleIdentifier); const { @@ -127,12 +125,7 @@ export function useCombinedRule({ ruleIdentifier, limitAlerts }: Props): Request const [ fetchRulerRuleGroup, - { - currentData: rulerRuleGroup, - isLoading: isLoadingRulerGroup, - error: rulerRuleGroupError, - isUninitialized: ruleGroupUninitialized, - }, + { currentData: rulerRuleGroup, isLoading: isLoadingRulerGroup, error: rulerRuleGroupError }, ] = alertRuleApi.endpoints.getRuleGroupForNamespace.useLazyQuery(); useEffect(() => { @@ -165,10 +158,9 @@ export function useCombinedRule({ ruleIdentifier, limitAlerts }: Props): Request }, [ruleIdentifier, ruleSourceName, promRuleNs, rulerRuleGroup, ruleSource, ruleLocation, namespaceName]); return { - loading: isLoadingDsFeatures || isLoadingPromRules || isLoadingRulerGroup || ruleGroupUninitialized, + loading: isLoadingDsFeatures || isLoadingPromRules || isLoadingRulerGroup, error: ruleLocationError ?? promRuleNsError ?? rulerRuleGroupError, result: rule, - uninitialized, }; } @@ -195,19 +187,17 @@ export function useRuleLocation(ruleIdentifier: RuleIdentifier): RequestState { @@ -306,10 +297,9 @@ export function useRuleWithLocation({ }, [ruleIdentifier, rulerRuleGroup, ruleSource, ruleLocation]); return { - loading: isLoadingRuleLocation || isLoadingDsFeatures || isLoadingRulerGroup, + loading: isLoadingRuleLocation || isLoadingDsFeatures || isLoadingRulerGroup || isUninitializedRulerGroup, error: ruleLocationError ?? rulerRuleGroupError, result: ruleWithLocation, - uninitialized, }; } diff --git a/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts b/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts index bc5d8a182a2..50ca91b8188 100644 --- a/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts +++ b/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts @@ -16,22 +16,19 @@ interface ResultBag { } export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): ResultBag { - const { currentData: dsFeatures, isLoading: loadingDataSourceFeatures } = - featureDiscoveryApi.endpoints.discoverDsFeatures.useQuery({ - uid: getDatasourceAPIUid(rulesSourceName), - }); + const { currentData: dsFeatures, isLoading } = featureDiscoveryApi.endpoints.discoverDsFeatures.useQuery({ + uid: getDatasourceAPIUid(rulesSourceName), + }); const folderUID = rule && isGrafanaRulerRule(rule) ? rule.grafana_alert.namespace_uid : undefined; - const rulePermission = getRulesPermissions(rulesSourceName); - const { folder, loading: loadingFolder } = useFolder(folderUID); + const rulePermission = getRulesPermissions(rulesSourceName); + const { folder, loading } = useFolder(folderUID); if (!rule) { return { isEditable: false, isRemovable: false, loading: false }; } - const loading = loadingFolder || loadingDataSourceFeatures; - // Grafana rules can be edited if user can edit the folder they're in // When RBAC is disabled access to a folder is the only requirement for managing rules // When RBAC is enabled the appropriate alerting permissions need to be met @@ -42,23 +39,13 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): ); } - // loading folder information - if (loadingFolder) { - return { - isRulerAvailable: true, - isEditable: false, - isRemovable: false, - loading: true, - }; - } - - // invalid folder UID if (!folder) { + // Loading or invalid folder UID return { isRulerAvailable: true, isEditable: false, isRemovable: false, - loading: false, + loading, }; } @@ -69,7 +56,7 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): isRulerAvailable: true, isEditable: canEditGrafanaRules, isRemovable: canRemoveGrafanaRules, - loading: loading, + loading: loading || isLoading, }; } @@ -82,6 +69,6 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): isRulerAvailable, isEditable: canEditCloudRules && isRulerAvailable, isRemovable: canRemoveCloudRules && isRulerAvailable, - loading: loading, + loading: isLoading, }; } diff --git a/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx index 8bc21533774..7d1b9d92fcf 100644 --- a/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx +++ b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx @@ -1,5 +1,4 @@ import { Alert, LoadingPlaceholder } from '@grafana/ui'; -import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { RuleIdentifier } from 'app/types/unified-alerting'; import { AlertWarning } from '../AlertWarning'; @@ -14,21 +13,17 @@ interface ExistingRuleEditorProps { } export function ExistingRuleEditor({ identifier }: ExistingRuleEditorProps) { - const ruleSourceName = ruleId.ruleIdentifierToRuleSourceName(identifier); - const { loading: loadingAlertRule, result: ruleWithLocation, error, - uninitialized, } = useRuleWithLocation({ ruleIdentifier: identifier }); + const ruleSourceName = ruleId.ruleIdentifierToRuleSourceName(identifier); + const { isEditable, loading: loadingEditable } = useIsRuleEditable(ruleSourceName, ruleWithLocation?.rule); - // the loading of the editable state only happens once we've got a rule with location loaded, so we set it to true by default here - const loadingEditableState = Boolean(ruleWithLocation) ? loadingEditable : true; - const loading = loadingAlertRule || loadingEditableState || uninitialized; - const ruleNotFound = !Boolean(ruleWithLocation); + const loading = loadingAlertRule || loadingEditable; if (loading) { return ; @@ -42,11 +37,11 @@ export function ExistingRuleEditor({ identifier }: ExistingRuleEditorProps) { ); } - if (ruleNotFound) { - return ; + if (!ruleWithLocation) { + return Sorry! This rule does not exist.; } - if (isEditable === false && !loadingEditable) { + if (isEditable === false) { return Sorry! You do not have permission to edit this rule.; } From af8cab92109ecfbb54625ff25a3b90a4f92dc46a Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:22:04 +0100 Subject: [PATCH 608/894] Alerting: Add Jira integration to cloud AMs (#100482) * Add Jira integration to cloud AMs * Add alertingJiraIntegration feature toggle for jira integration * Update integration name to Jira Service Management * address pr comments * gen ff * add project to the getReceiverDescription for jira * Update getReceiverDescription for jira * update text * update texts and add required option * Add conversion for fields jira integration to JSON format in the dto and viceversa * add tests * Add translation for jira receiver summary * Add placeholder for Jira duration option * move logic cheking integrtion type outside the conversion method --------- Co-authored-by: Tom Ratcliffe --- .../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 | 17 +++ .../components/contact-points/utils.ts | 12 ++ .../cloud-alertmanager-notifier-types.ts | 50 +++++++++ .../unified/utils/receiver-form.test.ts | 106 ++++++++++++++++++ .../alerting/unified/utils/receiver-form.ts | 50 ++++++++- public/app/types/alerting.ts | 3 +- public/locales/en-US/grafana.json | 3 + public/locales/pseudo-LOCALE/grafana.json | 3 + 12 files changed, 255 insertions(+), 3 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 8e955663673..9feb63a23eb 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -259,4 +259,5 @@ export interface FeatureToggles { newLogsPanel?: boolean; grafanaconThemes?: boolean; pluginsCDNSyncLoader?: boolean; + alertingJiraIntegration?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 879c3153378..f8d16934988 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1808,6 +1808,14 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaPluginsPlatformSquad, }, + { + Name: "alertingJiraIntegration", + Description: "Enables the new Jira integration for contact points in cloud alert managers.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + FrontendOnly: true, + HideFromDocs: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index a3694fa49ae..1a356254544 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -240,3 +240,4 @@ alertingAlertmanagerExtraDedupStageStopPipeline,experimental,@grafana/alerting-s newLogsPanel,experimental,@grafana/observability-logs,false,false,true grafanaconThemes,experimental,@grafana/grafana-frontend-platform,false,true,false pluginsCDNSyncLoader,experimental,@grafana/plugins-platform-backend,false,false,false +alertingJiraIntegration,experimental,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index d2f0868a839..49d0bbb3f33 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -970,4 +970,8 @@ const ( // FlagPluginsCDNSyncLoader // Load plugins from CDN synchronously FlagPluginsCDNSyncLoader = "pluginsCDNSyncLoader" + + // FlagAlertingJiraIntegration + // Enables the new Jira integration for contact points in cloud alert managers. + FlagAlertingJiraIntegration = "alertingJiraIntegration" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 0cdcc21124f..94bd2a033c3 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -274,6 +274,23 @@ "expression": "true" } }, + { + "metadata": { + "name": "alertingJiraIntegration", + "resourceVersion": "1739362088655", + "creationTimestamp": "2025-02-12T10:45:07Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-02-12 12:08:08.655259 +0000 UTC" + } + }, + "spec": { + "description": "Enables the new Jira integration for contact points in cloud alert managers.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "frontend": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "alertingListViewV2", diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts index fff055731fe..868bf032362 100644 --- a/public/app/features/alerting/unified/components/contact-points/utils.ts +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -2,6 +2,7 @@ import { difference, groupBy, take, trim, upperFirst } from 'lodash'; import { ReactNode } from 'react'; import { config } from '@grafana/runtime'; +import { t } from 'app/core/internationalization'; import { canAdminEntity, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; import { AlertManagerCortexConfig, @@ -50,6 +51,17 @@ export function getReceiverDescription(receiver: ReceiverConfigWithMetadata): Re case 'webhook': { return settings.url; } + case 'jira': { + return t( + 'alerting.contact-points.receiver-summary.jira', + `Creates a "{{issueType}}" issue in the "{{project}}" project`, + { + issueType: settings.issue_type, + project: settings.project, + url: settings.api_url, + } + ); + } case ReceiverTypes.OnCall: { return receiver[RECEIVER_PLUGIN_META_KEY]?.description; } diff --git a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts index 27b19c8d3fe..a172c9cd19d 100644 --- a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts +++ b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts @@ -1,3 +1,4 @@ +import { config } from '@grafana/runtime'; import { CloudNotifierType, NotificationChannelOption, NotifierDTO } from 'app/types'; import { option } from './notifier-types'; @@ -69,6 +70,54 @@ const httpConfigOption: NotificationChannelOption = option( } ); +const jiraNotifier: NotifierDTO = { + name: 'Jira', + description: 'Send notifications to Jira Service Management', + type: 'jira', + info: '', + heading: 'Jira settings', + options: [ + option('api_url', 'API URL', 'The host to send Jira API requests to', { required: true }), + option('project', 'Project Key', 'The project key where issues are created', { required: true }), + option('summary', 'Summary', 'Issue summary template', { placeholder: '{{ template "jira.default.summary" . }}' }), + option('description', 'Description', 'Issue description template', { + placeholder: '{{ template "jira.default.description" . }}', + }), + option('labels', 'Labels', ' Labels to be added to the issue', { element: 'string_array' }), + option('priority', 'Priority', 'Priority of the issue', { + placeholder: '{{ template "jira.default.priority" . }}', + }), + option('issue_type', 'Issue Type', 'Type of the issue (e.g. Bug)', { required: true }), + option( + 'reopen_transition', + 'Reopen transition', + 'Name of the workflow transition to reopen an issue. The target status should not have the category "done"' + ), + option( + 'resolve_transition', + 'Resolve transition', + 'Name of the workflow transition to resolve an issue. The target status must have the category "done"' + ), + option( + 'wont_fix_resolution', + "Won't fix resolution", + 'If "Reopen transition" is defined, ignore issues with that resolution' + ), + option( + 'reopen_duration', + 'Reopen duration', + 'If "Reopen transition" is defined, reopen the issue when it is not older than this value (rounded down to the nearest minute)', + { + placeholder: 'Use duration format, for example: 1.2s, 100ms', + } + ), + option('fields', 'Fields', 'Other issue and custom fields', { + element: 'key_value_map', + }), + httpConfigOption, + ], +}; + export const cloudNotifierTypes: Array> = [ { name: 'Email', @@ -266,6 +315,7 @@ export const cloudNotifierTypes: Array> = [ httpConfigOption, ], }, + ...(config.featureToggles?.alertingJiraIntegration ? [jiraNotifier] : []), { name: 'OpsGenie', description: 'Send notifications to OpsGenie', diff --git a/public/app/features/alerting/unified/utils/receiver-form.test.ts b/public/app/features/alerting/unified/utils/receiver-form.test.ts index 3ea8089a71f..e72e40a4fe1 100644 --- a/public/app/features/alerting/unified/utils/receiver-form.test.ts +++ b/public/app/features/alerting/unified/utils/receiver-form.test.ts @@ -5,6 +5,8 @@ import { grafanaAlertNotifiers, grafanaAlertNotifiersMock } from '../mockGrafana import { CloudChannelValues, GrafanaChannelValues, ReceiverFormValues } from '../types/receiver-form'; import { + convertJiraFieldToJson, + convertJsonToJiraField, formValuesToCloudReceiver, formValuesToGrafanaReceiver, grafanaReceiverToFormValues, @@ -327,3 +329,107 @@ describe('grafanaReceiverToFormValues', () => { expect(formValues.items[0].settings.url).toBeUndefined(); }); }); + +describe('convertJsonToJiraField', () => { + it('should convert nested objects to JSON strings ', () => { + const input = { + fields: { + key1: { nestedKey1: 'nestedValue1' }, + key2: { nestedKey2: 'nestedValue2' }, + }, + }; + const expectedOutput = { + fields: { + key1: '{"nestedKey1":"nestedValue1"}', + key2: '{"nestedKey2":"nestedValue2"}', + }, + }; + const result = convertJsonToJiraField(input); + expect(result).toEqual(expectedOutput); + }); + + it('should leave non-object values unchanged ', () => { + const input = { + fields: { + key1: 'value1', + key2: 123, + key3: true, + }, + }; + const result = convertJsonToJiraField(input); + expect(result).toEqual(input); + }); + + it('should handle fields object with mixed types', () => { + const input = { + fields: { + key1: 'value1', + key2: { nestedKey2: 'nestedValue2' }, + key3: 123, + key4: true, + }, + }; + const expectedOutput = { + fields: { + key1: 'value1', + key2: '{"nestedKey2":"nestedValue2"}', + key3: 123, + key4: true, + }, + }; + const result = convertJsonToJiraField(input); + expect(result).toEqual(expectedOutput); + }); +}); + +describe('convertJiraFieldToJson', () => { + it('should convert stringified objects to nested objects ', () => { + const input = { + fields: { + key1: '{"nestedKey1":{"a":2,"c":[1,2,3 ]}}', + key2: '{"nestedKey2":"nestedValue2"}', + }, + }; + const expectedOutput = { + fields: { + key1: { nestedKey1: { a: 2, c: [1, 2, 3] } }, + key2: { nestedKey2: 'nestedValue2' }, + }, + }; + const result = convertJiraFieldToJson(input); + expect(result).toEqual(expectedOutput); + }); + + it('should leave non-stringified values unchanged ', () => { + const input = { + fields: { + key1: 'value1', + key2: 123, + key3: true, + }, + }; + const result = convertJiraFieldToJson(input); + expect(result).toEqual(input); + }); + + it('should handle fields object with mixed types ', () => { + const input = { + fields: { + key1: 'value1', + key2: '{"nestedKey2":"nestedValue2"}', + key3: 123, + key4: true, + }, + }; + const expectedOutput = { + fields: { + key1: 'value1', + key2: { nestedKey2: 'nestedValue2' }, + key3: 123, + key4: true, + }, + }; + const result = convertJiraFieldToJson(input); + expect(result).toEqual(expectedOutput); + }); +}); diff --git a/public/app/features/alerting/unified/utils/receiver-form.ts b/public/app/features/alerting/unified/utils/receiver-form.ts index 283ac3e81b1..c742837ad5d 100644 --- a/public/app/features/alerting/unified/utils/receiver-form.ts +++ b/public/app/features/alerting/unified/utils/receiver-form.ts @@ -105,11 +105,14 @@ export function formValuesToCloudReceiver( name: values.name, }; values.items.forEach(({ __id, type, settings, sendResolved }) => { - const channel = omitEmptyValues({ + const channelWithOmmitedIdentifiers = omitEmptyValues({ ...omitTemporaryIdentifiers(settings), send_resolved: sendResolved ?? defaults.sendResolved, }); + const channel = + type === 'jira' ? convertJiraFieldToJson(channelWithOmmitedIdentifiers) : channelWithOmmitedIdentifiers; + if (!(`${type}_configs` in recv)) { recv[`${type}_configs`] = [channel]; } else { @@ -119,6 +122,49 @@ export function formValuesToCloudReceiver( return recv; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function convertJiraFieldToJson(object: Record) { + // Only for cloud alert manager. Jira fields option can be a nested object. We need to convert it to JSON. + + const objectCopy = structuredClone(object); + + if (typeof objectCopy.fields === 'object') { + for (const [optionName, optionValue] of Object.entries(objectCopy.fields)) { + let valueForField; + try { + // eslint-disable-next-line + valueForField = JSON.parse(optionValue as string); // is a stringified object + } catch { + valueForField = optionValue; // is not a stringified object + } + objectCopy.fields[optionName] = valueForField; + } + } + + return objectCopy; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function convertJsonToJiraField(object: Record) { + // Only for cloud alert manager. Convert JSON back to nested Jira fields option. + + const objectCopy = structuredClone(object); + + if (typeof objectCopy.fields === 'object') { + for (const [optionName, optionValue] of Object.entries(objectCopy.fields)) { + let valueForField; + if (typeof optionValue === 'object') { + valueForField = JSON.stringify(optionValue); + } else { + valueForField = optionValue; + } + objectCopy.fields[optionName] = valueForField; + } + } + + return objectCopy; +} + function cloudChannelConfigToFormChannelValues( id: string, type: CloudNotifierType, @@ -128,7 +174,7 @@ function cloudChannelConfigToFormChannelValues( __id: id, type, settings: { - ...channel, + ...(type === 'jira' ? convertJsonToJiraField(channel) : channel), }, secureFields: {}, secureSettings: {}, diff --git a/public/app/types/alerting.ts b/public/app/types/alerting.ts index ac598691607..3b32017319e 100644 --- a/public/app/types/alerting.ts +++ b/public/app/types/alerting.ts @@ -74,7 +74,8 @@ export type CloudNotifierType = | 'telegram' | 'sns' | 'discord' - | 'msteams'; + | 'msteams' + | 'jira'; export type NotifierType = GrafanaNotifierType | CloudNotifierType; export interface NotifierDTO { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 1872daf2358..58494d7d319 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -283,6 +283,9 @@ "no-delivery-attempts": "No delivery attempts", "no-integrations": "No integrations configured", "only-firing": "Delivering <1>only firing notifications", + "receiver-summary": { + "jira": "Creates a \"{{issueType}}\" issue in the \"{{project}}\" project" + }, "telegram": { "parse-mode-warning-body": "If you use a <1>parse_mode option other than <3>None, truncation may result in an invalid message, causing the notification to fail. For longer messages, we recommend using an alternative contact method.", "parse-mode-warning-title": "Telegram messages are limited to 4096 UTF-8 characters." diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 0df58885897..9338082cc96 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -283,6 +283,9 @@ "no-delivery-attempts": "Ńő đęľįvęřy äŧŧęmpŧş", "no-integrations": "Ńő įʼnŧęģřäŧįőʼnş čőʼnƒįģūřęđ", "only-firing": "Đęľįvęřįʼnģ <1>őʼnľy ƒįřįʼnģ ʼnőŧįƒįčäŧįőʼnş", + "receiver-summary": { + "jira": "Cřęäŧęş ä \"{{issueType}}\" įşşūę įʼn ŧĥę \"{{project}}\" přőĵęčŧ" + }, "telegram": { "parse-mode-warning-body": "Ĩƒ yőū ūşę ä <1>päřşę_mőđę őpŧįőʼn őŧĥęř ŧĥäʼn <3>Ńőʼnę, ŧřūʼnčäŧįőʼn mäy řęşūľŧ įʼn äʼn įʼnväľįđ męşşäģę, čäūşįʼnģ ŧĥę ʼnőŧįƒįčäŧįőʼn ŧő ƒäįľ. Főř ľőʼnģęř męşşäģęş, ŵę řęčőmmęʼnđ ūşįʼnģ äʼn äľŧęřʼnäŧįvę čőʼnŧäčŧ męŧĥőđ.", "parse-mode-warning-title": "Ŧęľęģřäm męşşäģęş äřę ľįmįŧęđ ŧő 4096 ŮŦF-8 čĥäřäčŧęřş." From 196a73ec7282467992601811e4e980efc8d0670c Mon Sep 17 00:00:00 2001 From: beejeebus Date: Fri, 14 Feb 2025 08:25:26 -0500 Subject: [PATCH 609/894] influxdb - fix nil pointer usage - fixes #100723 (#100724) When introducing errorsource over in: https://github.com/grafana/grafana/pull/99900 I introduced a bug - trying to use a http response with a non-nil error. In that case, the response is nil, so code panics. This PR removes that check. --- pkg/tsdb/influxdb/influxql/influxql.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/tsdb/influxdb/influxql/influxql.go b/pkg/tsdb/influxdb/influxql/influxql.go index 912a7abe2f4..e7de3267e6f 100644 --- a/pkg/tsdb/influxdb/influxql/influxql.go +++ b/pkg/tsdb/influxdb/influxql/influxql.go @@ -176,8 +176,7 @@ func execute(ctx context.Context, tracer trace.Tracer, dsInfo *models.Datasource res, err := dsInfo.HTTPClient.Do(request) if err != nil { return backend.DataResponse{ - Error: err, - ErrorSource: backend.ErrorSourceFromHTTPStatus(res.StatusCode), + Error: err, }, err } defer func() { From c291ec7ba969e08f2ee3b59937f8aba05da71915 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Fri, 14 Feb 2025 09:09:21 -0500 Subject: [PATCH 610/894] SQL Expressions: Include SQL Parser/Syntax error in the public message (#100725) fixes #100721 --- pkg/expr/service_sql_test.go | 17 +++++++++++++++++ pkg/expr/sql_command.go | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/expr/service_sql_test.go b/pkg/expr/service_sql_test.go index 5730ca82fd0..2ad5df4baa8 100644 --- a/pkg/expr/service_sql_test.go +++ b/pkg/expr/service_sql_test.go @@ -92,6 +92,23 @@ func TestSQLService(t *testing.T) { require.Error(t, rsp.Responses["B"].Error, "should return invalid sql error") require.ErrorContains(t, rsp.Responses["B"].Error, "blocked function load_file") }) + + t.Run("parse error should be returned", func(t *testing.T) { + s, req := newMockQueryService(resp, + newABSQLQueries(`SELECT * FROM A LIMIT sloth`), + ) + + s.features = featuremgmt.WithFeatures(featuremgmt.FlagSqlExpressions) + + pl, err := s.BuildPipeline(req) + require.NoError(t, err) + + rsp, err := s.ExecutePipeline(context.Background(), time.Now(), pl) + require.NoError(t, err) + + require.Error(t, rsp.Responses["B"].Error, "should return sql error on parsing") + require.ErrorContains(t, rsp.Responses["B"].Error, "limit expression expected to be numeric") + }) } func jsonEscape(input string) (string, error) { diff --git a/pkg/expr/sql_command.go b/pkg/expr/sql_command.go index 60d23f6e82f..069d2a39e91 100644 --- a/pkg/expr/sql_command.go +++ b/pkg/expr/sql_command.go @@ -31,7 +31,7 @@ func NewSQLCommand(refID, rawSQL string) (*SQLCommand, error) { if err != nil { logger.Warn("invalid sql query", "sql", rawSQL, "error", err) return nil, errutil.BadRequest("sql-invalid-sql", - errutil.WithPublicMessage("error reading SQL command"), + errutil.WithPublicMessage(fmt.Sprintf("invalid SQL query: %s", err)), ) } if len(tables) == 0 { From 23a657a72d422a9a6cfcb50ebb92790ff7278331 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Fri, 14 Feb 2025 15:20:07 +0100 Subject: [PATCH 611/894] scopes: adds more logging details to the scopedashboard query (#100267) Signed-off-by: bergquist --- pkg/registry/apis/scope/find_scope_dashboards.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/registry/apis/scope/find_scope_dashboards.go b/pkg/registry/apis/scope/find_scope_dashboards.go index f4cace05c19..b33f091907e 100644 --- a/pkg/registry/apis/scope/find_scope_dashboards.go +++ b/pkg/registry/apis/scope/find_scope_dashboards.go @@ -99,7 +99,7 @@ func (f *findScopeDashboardsREST) Connect(ctx context.Context, name string, opts return strings.Compare(i.Status.DashboardTitle, j.Status.DashboardTitle) }) - logger.FromContext(req.Context()).Debug("find scopedashboardbinding", "raw", len(all.Items), "filtered", len(results.Items)) + logger.FromContext(req.Context()).Debug("find scopedashboardbinding", "raw", len(all.Items), "filtered", len(results.Items), "scopeQueryParams", strings.Join(scopes, ",")) responder.Object(200, results) }), nil From 9cff383830f44ee8b982c606c824095c0379119e Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Fri, 14 Feb 2025 15:21:35 +0100 Subject: [PATCH 612/894] scopenodes: check if the query exists in the title instead of starts with the query (#100578) scopenodes: check if the string contains the search param instead of limiting to prefix Signed-off-by: bergquist --- pkg/registry/apis/scope/find.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/registry/apis/scope/find.go b/pkg/registry/apis/scope/find.go index b3d3d4c54b7..43cb057958a 100644 --- a/pkg/registry/apis/scope/find.go +++ b/pkg/registry/apis/scope/find.go @@ -104,9 +104,8 @@ func filterAndAppendItem(item scope.ScopeNode, parent string, query string, resu return // Someday this will have an index in raw storage on parentName } - // skip if query is passed and title doesn't match. - // HasPrefix is not the end goal but something that that gets us started. - if query != "" && !strings.HasPrefix(item.Spec.Title, query) { + // skip if query is passed and title doesn't contain the query. + if query != "" && !strings.Contains(item.Spec.Title, query) { return } From b9034f413ef0b4b981f97da1c3f5f107cd4c7c16 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 14 Feb 2025 14:44:47 +0000 Subject: [PATCH 613/894] Query library: Refactor to use `onSelectQuery` callback (#100360) * starting to refactor query library to use callback * replace QueryActionButton with onSelectQuery * hook up properly in explore * fix unit tests * i18n * extract types * fix refId in explore * fix unit tests * handle changing datasource to mixed * enrich queries with datasource * move out into separate function * filter out expression datasources --- .../PanelDataPane/PanelDataQueriesTab.tsx | 68 +++++----- public/app/features/explore/Explore.test.tsx | 5 + public/app/features/explore/Explore.tsx | 28 +++- .../app/features/explore/ExploreToolbar.tsx | 32 ++--- .../QueriesDrawer/QueriesDrawerDropdown.tsx | 120 ------------------ .../QueryLibrary/QueryLibraryContext.tsx | 8 +- .../features/explore/QueryLibrary/mocks.tsx | 25 ++++ .../features/explore/QueryLibrary/types.ts | 10 +- .../explore/SecondaryActions.test.tsx | 28 +++- .../app/features/explore/SecondaryActions.tsx | 65 ++++++++-- .../explore/spec/helper/interactions.ts | 2 +- public/locales/en-US/grafana.json | 5 +- public/locales/pseudo-LOCALE/grafana.json | 5 +- 13 files changed, 188 insertions(+), 213 deletions(-) delete mode 100644 public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx create mode 100644 public/app/features/explore/QueryLibrary/mocks.tsx diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index 66f83b97bd5..f669ebeeb98 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -13,7 +13,7 @@ import { } from '@grafana/scenes'; import { DataQuery } from '@grafana/schema'; import { Button, Stack, Tab } from '@grafana/ui'; -import { t, Trans } from 'app/core/internationalization'; +import { Trans } from 'app/core/internationalization'; import { addQuery } from 'app/core/utils/query'; import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard'; import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils'; @@ -25,8 +25,10 @@ import { updateQueries } from 'app/features/query/state/updateQueries'; import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard/runSharedRequest'; import { QueryGroupOptions } from 'app/types'; +import { MIXED_DATASOURCE_NAME } from '../../../../plugins/datasource/mixed/MixedDataSource'; import { useQueryLibraryContext } from '../../../explore/QueryLibrary/QueryLibraryContext'; -import { QueryActionButtonProps } from '../../../explore/QueryLibrary/types'; +import { ExpressionDatasourceUID } from '../../../expressions/types'; +import { getDatasourceSrv } from '../../../plugins/datasource_srv'; import { PanelTimeRange } from '../../scene/PanelTimeRange'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../../utils/utils'; import { getUpdatedHoverHeader } from '../getPanelFrameOptions'; @@ -315,13 +317,35 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps { - for (const query of queries) { - model.onQueriesChange(addQuery(model.getQueries(), query)); + const onSelectQueryFromLibrary = async (query: DataQuery) => { + // ensure all queries explicitly define a datasource + const enrichedQueries = queries.map((q) => + q.datasource + ? q + : { + ...q, + datasource: datasource.getRef(), + } + ); + const newQueries = addQuery(enrichedQueries, query); + model.onQueriesChange(newQueries); + if (query.datasource?.uid) { + const uniqueDatasources = new Set( + newQueries.map((q) => q.datasource?.uid).filter((uid) => uid !== ExpressionDatasourceUID) + ); + const isMixed = uniqueDatasources.size > 1; + const newDatasourceRef = { + uid: isMixed ? MIXED_DATASOURCE_NAME : query.datasource.uid, + }; + const shouldChangeDatasource = datasource.uid !== newDatasourceRef.uid; + if (shouldChangeDatasource) { + const newDatasource = getDatasourceSrv().getInstanceSettings(newDatasourceRef); + if (newDatasource) { + await model.onChangeDataSource(newDatasource); + } + } } - }); + }; return (
          @@ -358,9 +382,9 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps { - openQueryLibraryDrawer(getDatasourceNames(datasource, queries), addQueryActionButton); - }} + onClick={() => + openQueryLibraryDrawer(getDatasourceNames(datasource, queries), onSelectQueryFromLibrary) + } variant="secondary" data-testid={selectors.components.QueryTab.addQuery} > @@ -385,28 +409,6 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps void) { - return function AddQueryFromLibraryButton(props: QueryActionButtonProps) { - const label = t('dashboards.query-library.add-query-button', 'Add query'); - return ( - - ); - }; -} - function getDatasourceNames(datasource: DataSourceApi, queries: DataQuery[]): string[] { if (datasource.uid === '-- Mixed --') { // If datasource is mixed, the datasource UID is on the query. Here we map the UIDs to datasource names. diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index 92e5204d742..9fd5ba17ab8 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -103,6 +103,11 @@ const dummyProps: Props = { setSupplementaryQueryEnabled: jest.fn(), correlationEditorDetails: undefined, correlationEditorHelperData: undefined, + exploreActiveDS: { + exploreToDS: [], + dsToExplore: [], + }, + changeDatasource: jest.fn(), }; jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index de38f8019fa..28ad370648e 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -8,6 +8,7 @@ import { AbsoluteTimeRange, DataFrame, EventBus, + getNextRefId, GrafanaTheme2, hasToggleableQueryFiltersSupport, LoadingState, @@ -54,6 +55,7 @@ import { ResponseErrorContainer } from './ResponseErrorContainer'; import { SecondaryActions } from './SecondaryActions'; import TableContainer from './Table/TableContainer'; import { TraceViewContainer } from './TraceView/TraceViewContainer'; +import { changeDatasource } from './state/datasource'; import { changeSize } from './state/explorePane'; import { splitOpen } from './state/main'; import { @@ -65,7 +67,7 @@ import { setQueries, setSupplementaryQueryEnabled, } from './state/query'; -import { isSplit } from './state/selectors'; +import { isSplit, selectExploreDSMaps } from './state/selectors'; import { updateTimeRange } from './state/time'; const getStyles = (theme: GrafanaTheme2) => { @@ -605,6 +607,28 @@ export class Explore extends PureComponent { queryInspectorButtonActive={showQueryInspector} onClickAddQueryRowButton={this.onClickAddQueryRowButton} onClickQueryInspectorButton={() => setShowQueryInspector(!showQueryInspector)} + onSelectQueryFromLibrary={async (query) => { + const { changeDatasource, queries, setQueries } = this.props; + const newQueries = [ + ...queries, + { + ...query, + refId: getNextRefId(queries), + }, + ]; + setQueries(exploreId, newQueries); + if (query.datasource?.uid) { + const uniqueDatasources = new Set(newQueries.map((q) => q.datasource?.uid)); + const isMixed = uniqueDatasources.size > 1; + const newDatasourceRef = { + uid: isMixed ? MIXED_DATASOURCE_NAME : query.datasource.uid, + }; + const shouldChangeDatasource = datasourceInstance.uid !== newDatasourceRef.uid; + if (shouldChangeDatasource) { + await changeDatasource({ exploreId, datasource: newDatasourceRef }); + } + } + }} /> @@ -716,10 +740,12 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { showLogsSample, correlationEditorHelperData, correlationEditorDetails: explore.correlationEditorDetails, + exploreActiveDS: selectExploreDSMaps(state), }; } const mapDispatchToProps = { + changeDatasource, changeSize, modifyQueries, scanStart, diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 9dd6dcd5770..d35dab34118 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -28,8 +28,6 @@ import { getFiscalYearStartMonth, getTimeZone } from '../profile/state/selectors import { ExploreTimeControls } from './ExploreTimeControls'; import { LiveTailButton } from './LiveTailButton'; import { useQueriesDrawerContext } from './QueriesDrawer/QueriesDrawerContext'; -import { QueriesDrawerDropdown } from './QueriesDrawer/QueriesDrawerDropdown'; -import { useQueryLibraryContext } from './QueryLibrary/QueryLibraryContext'; import { ShortLinkButtonMenu } from './ShortLinkButtonMenu'; import { ToolbarExtensionPoint } from './extensions/ToolbarExtensionPoint'; import { changeDatasource } from './state/datasource'; @@ -93,7 +91,6 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle const isCorrelationsEditorMode = correlationDetails?.editorMode || false; const isLeftPane = useSelector(isLeftPaneSelector(exploreId)); const { drawerOpened, setDrawerOpened } = useQueriesDrawerContext(); - const { queryLibraryEnabled } = useQueryLibraryContext(); const shouldRotateSplitIcon = useMemo( () => (isLeftPane && isLargerPane) || (!isLeftPane && !isLargerPane), @@ -206,23 +203,18 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle dispatch(changeRefreshInterval({ exploreId, refreshInterval })); }; - const navBarActions = []; - - if (queryLibraryEnabled) { - navBarActions.unshift(); - } else { - navBarActions.unshift( - setDrawerOpened(!drawerOpened)} - data-testid={Components.QueryTab.queryHistoryButton} - icon="history" - > - Query history - - ); - } + const navBarActions = [ + setDrawerOpened(!drawerOpened)} + data-testid={Components.QueryTab.queryHistoryButton} + icon="history" + > + Query history + , + , + ]; return (
          diff --git a/public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx b/public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx deleted file mode 100644 index d800aaf564a..00000000000 --- a/public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { css } from '@emotion/css'; -import { ComponentProps, useState } from 'react'; - -import { Button, ButtonGroup, Dropdown, Menu, ToolbarButton } from '@grafana/ui'; -import { useStyles2 } from '@grafana/ui/'; -import { t } from 'app/core/internationalization'; - -import { createDatasourcesList } from '../../../core/utils/richHistory'; -import { useSelector } from '../../../types'; -import ExploreRunQueryButton from '../ExploreRunQueryButton'; -import { useQueryLibraryContext } from '../QueryLibrary/QueryLibraryContext'; -import { QueryActionButton } from '../QueryLibrary/types'; -import { selectExploreDSMaps } from '../state/selectors'; - -import { useQueriesDrawerContext } from './QueriesDrawerContext'; -import { i18n } from './utils'; - -// This makes TS happy as ExploreRunQueryButton has optional onClick prop while QueryActionButton doesn't -// in addition to map the rootDatasourceUid prop. -function ExploreRunQueryButtonWrapper(props: ComponentProps) { - return ; -} - -type Props = { - variant: 'compact' | 'full'; -}; - -/** - * Dropdown button that can either open a Query History drawer or a Query Library drawer. - * @param variant - * @constructor - */ -export function QueriesDrawerDropdown({ variant }: Props) { - const { drawerOpened, setDrawerOpened } = useQueriesDrawerContext(); - - const { - openDrawer: openQueryLibraryDrawer, - closeDrawer: closeQueryLibraryDrawer, - isDrawerOpen: isQueryLibraryDrawerOpen, - queryLibraryEnabled, - } = useQueryLibraryContext(); - - const [queryOption, setQueryOption] = useState<'library' | 'history'>('library'); - - const exploreActiveDS = useSelector(selectExploreDSMaps); - - const styles = useStyles2(getStyles); - - // In case query library is not enabled we show only simple button for query history in the parent. - if (!queryLibraryEnabled) { - return undefined; - } - - function toggleRichHistory() { - setQueryOption('history'); - setDrawerOpened(!drawerOpened); - } - - function toggleQueryLibrary() { - setQueryOption('library'); - if (isQueryLibraryDrawerOpen) { - closeQueryLibraryDrawer(); - } else { - // Prefill the query library filter with the dataSource. - // Get current dataSource that is open. As this is only used in Explore we get it from Explore state. - const listOfDatasources = createDatasourcesList(); - const activeDatasources = exploreActiveDS.dsToExplore - .map((eDs) => { - return listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name; - }) - .filter((name): name is string => !!name); - - openQueryLibraryDrawer(activeDatasources, ExploreRunQueryButtonWrapper); - } - } - - const menu = ( - - toggleQueryLibrary()} /> - toggleRichHistory()} /> - - ); - - const buttonLabel = queryOption === 'library' ? i18n.queryLibrary : i18n.queryHistory; - const toggle = queryOption === 'library' ? toggleQueryLibrary : toggleRichHistory; - - return ( - - toggle()} - aria-label={buttonLabel} - > - {variant === 'full' ? buttonLabel : undefined} - - - {/* Show either a drops down button so that user can select QL or QH, or show a close button if one of them is - already open.*/} - {drawerOpened || isQueryLibraryDrawerOpen ? ( - - ) : ( - - - - )} - - ); -} - -const getStyles = () => ({ - toggle: css({ width: '36px' }), - // tweaking icon position so it's nicely aligned when dropdown turns into a close button - close: css({ width: '36px', '> svg': { position: 'relative', left: 2 } }), -}); diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx index 0e9fc414375..d5b0a6d77b3 100644 --- a/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx +++ b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx @@ -2,7 +2,7 @@ import { createContext, ReactNode, useContext } from 'react'; import { DataQuery } from '@grafana/schema'; -import { QueryActionButton } from './types'; +import { OnSelectQueryType } from './types'; /** * Context with state and action to interact with Query Library. The Query Library feature consists of a drawer @@ -20,11 +20,7 @@ export type QueryLibraryContextType = { * @param options.context Used for tracking. Should identify the context this is called from, like 'explore' or * 'dashboard'. */ - openDrawer: ( - datasourceFilters: string[], - queryActionButton: QueryActionButton, - options?: { context?: string } - ) => void; + openDrawer: (datasourceFilters: string[], onSelectQuery: OnSelectQueryType, options?: { context?: string }) => void; closeDrawer: () => void; isDrawerOpen: boolean; diff --git a/public/app/features/explore/QueryLibrary/mocks.tsx b/public/app/features/explore/QueryLibrary/mocks.tsx new file mode 100644 index 00000000000..c59911e7236 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/mocks.tsx @@ -0,0 +1,25 @@ +import { PropsWithChildren } from 'react'; + +import { QueryLibraryContext } from './QueryLibraryContext'; + +type Props = { + queryLibraryAvailable?: boolean; +}; + +export function QueryLibraryContextProviderMock(props: PropsWithChildren) { + return ( + + {props.children} + + ); +} diff --git a/public/app/features/explore/QueryLibrary/types.ts b/public/app/features/explore/QueryLibrary/types.ts index b36c7280fe8..50db96feeb0 100644 --- a/public/app/features/explore/QueryLibrary/types.ts +++ b/public/app/features/explore/QueryLibrary/types.ts @@ -1,11 +1,3 @@ -import { ComponentType } from 'react'; - import { DataQuery } from '@grafana/schema'; -export type QueryActionButtonProps = { - queries: DataQuery[]; - datasourceUid?: string; - onClick: () => void; -}; - -export type QueryActionButton = ComponentType; +export type OnSelectQueryType = (query: DataQuery) => void; diff --git a/public/app/features/explore/SecondaryActions.test.tsx b/public/app/features/explore/SecondaryActions.test.tsx index 076d9883f10..64a1fd2a172 100644 --- a/public/app/features/explore/SecondaryActions.test.tsx +++ b/public/app/features/explore/SecondaryActions.test.tsx @@ -1,13 +1,34 @@ -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { noop } from 'lodash'; +import { render } from '../../../test/test-utils'; + import { QueriesDrawerContextProviderMock } from './QueriesDrawer/mocks'; +import { QueryLibraryContextProviderMock } from './QueryLibrary/mocks'; import { SecondaryActions } from './SecondaryActions'; +jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { + return { + getDataSourceSrv: () => ({ + get: () => Promise.resolve({}), + getList: () => [], + getInstanceSettings: () => {}, + }), + }; +}); + describe('SecondaryActions', () => { it('should render component with two buttons', () => { - render(); + render( + + + + ); expect(screen.getByRole('button', { name: /Add query/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Query inspector/i })).toBeInTheDocument(); @@ -21,6 +42,7 @@ describe('SecondaryActions', () => { richHistoryRowButtonHidden={true} onClickAddQueryRowButton={noop} onClickQueryInspectorButton={noop} + onSelectQueryFromLibrary={noop} /> ); @@ -35,6 +57,7 @@ describe('SecondaryActions', () => { addQueryRowButtonDisabled={true} onClickAddQueryRowButton={noop} onClickQueryInspectorButton={noop} + onSelectQueryFromLibrary={noop} /> ); @@ -54,6 +77,7 @@ describe('SecondaryActions', () => { ); diff --git a/public/app/features/explore/SecondaryActions.tsx b/public/app/features/explore/SecondaryActions.tsx index dfa98f92b77..e50bb642d86 100644 --- a/public/app/features/explore/SecondaryActions.tsx +++ b/public/app/features/explore/SecondaryActions.tsx @@ -3,6 +3,14 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { ToolbarButton, useTheme2 } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; +import { useSelector } from 'app/types'; + +import { createDatasourcesList } from '../../core/utils/richHistory'; +import { MIXED_DATASOURCE_NAME } from '../../plugins/datasource/mixed/MixedDataSource'; + +import { useQueryLibraryContext } from './QueryLibrary/QueryLibraryContext'; +import { type OnSelectQueryType } from './QueryLibrary/types'; +import { selectExploreDSMaps } from './state/selectors'; type Props = { addQueryRowButtonDisabled?: boolean; @@ -12,6 +20,7 @@ type Props = { onClickAddQueryRowButton: () => void; onClickQueryInspectorButton: () => void; + onSelectQueryFromLibrary: OnSelectQueryType; }; const getStyles = (theme: GrafanaTheme2) => { @@ -25,27 +34,57 @@ const getStyles = (theme: GrafanaTheme2) => { }; }; -export function SecondaryActions(props: Props) { +export function SecondaryActions({ + addQueryRowButtonDisabled, + addQueryRowButtonHidden, + onClickAddQueryRowButton, + onClickQueryInspectorButton, + onSelectQueryFromLibrary, + queryInspectorButtonActive, +}: Props) { const theme = useTheme2(); const styles = getStyles(theme); + const exploreActiveDS = useSelector(selectExploreDSMaps); + // Prefill the query library filter with the dataSource. + // Get current dataSource that is open. As this is only used in Explore we get it from Explore state. + const listOfDatasources = createDatasourcesList(); + const activeDatasources = exploreActiveDS.dsToExplore + .map((eDs) => { + return listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name; + }) + .filter((name): name is string => !!name && name !== MIXED_DATASOURCE_NAME); + + const { queryLibraryEnabled, openDrawer: openQueryLibraryDrawer } = useQueryLibraryContext(); return (
          - {!props.addQueryRowButtonHidden && ( - - Add query - + {!addQueryRowButtonHidden && ( + <> + + Add query + + {queryLibraryEnabled && ( + openQueryLibraryDrawer(activeDatasources, onSelectQueryFromLibrary)} + icon="plus" + > + Add query from library + + )} + )} Query inspector diff --git a/public/app/features/explore/spec/helper/interactions.ts b/public/app/features/explore/spec/helper/interactions.ts index 41b3f41ae73..73e24da2b1c 100644 --- a/public/app/features/explore/spec/helper/interactions.ts +++ b/public/app/features/explore/spec/helper/interactions.ts @@ -40,7 +40,7 @@ export const openQueryHistory = async () => { }; export const openQueryLibrary = async () => { - const button = screen.getByRole('button', { name: 'Query library' }); + const button = screen.getByRole('button', { name: 'Add query from library' }); await userEvent.click(button); await waitFor(async () => { screen.getByRole('tab', { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 58494d7d319..f0a74858ebe 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1287,9 +1287,6 @@ "panel-queries": { "add-query-from-library": "Add query from library" }, - "query-library": { - "add-query-button": "Add query" - }, "settings": { "variables": { "dependencies": { @@ -1371,7 +1368,6 @@ "close-tooltip": "Close query history", "datasource-a-z": "Data source A-Z", "datasource-z-a": "Data source Z-A", - "library-history-dropdown": "Open query library or query history", "newest-first": "Newest first", "oldest-first": "Oldest first", "query-history": "Query history", @@ -1475,6 +1471,7 @@ "switch-datasource-button": "Switch data source and run query" }, "secondary-actions": { + "add-from-query-library": "Add query from library", "query-add-button": "Add query", "query-add-button-aria-label": "Add query", "query-history-button": "Query history", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 9338082cc96..3002080e1b3 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1287,9 +1287,6 @@ "panel-queries": { "add-query-from-library": "Åđđ qūęřy ƒřőm ľįþřäřy" }, - "query-library": { - "add-query-button": "Åđđ qūęřy" - }, "settings": { "variables": { "dependencies": { @@ -1371,7 +1368,6 @@ "close-tooltip": "Cľőşę qūęřy ĥįşŧőřy", "datasource-a-z": "Đäŧä şőūřčę Å-Ż", "datasource-z-a": "Đäŧä şőūřčę Ż-Å", - "library-history-dropdown": "Øpęʼn qūęřy ľįþřäřy őř qūęřy ĥįşŧőřy", "newest-first": "Ńęŵęşŧ ƒįřşŧ", "oldest-first": "Øľđęşŧ ƒįřşŧ", "query-history": "Qūęřy ĥįşŧőřy", @@ -1475,6 +1471,7 @@ "switch-datasource-button": "Ŝŵįŧčĥ đäŧä şőūřčę äʼnđ řūʼn qūęřy" }, "secondary-actions": { + "add-from-query-library": "Åđđ qūęřy ƒřőm ľįþřäřy", "query-add-button": "Åđđ qūęřy", "query-add-button-aria-label": "Åđđ qūęřy", "query-history-button": "Qūęřy ĥįşŧőřy", From 3a8a24e662bb223df897530c60bc70fe4fa42d92 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Fri, 14 Feb 2025 08:54:36 -0600 Subject: [PATCH 614/894] Docker: Missing libresolv.so.2 from glibc (#100729) * Docker: Missing libresolv.so.2 from glibc * Misplaced && --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d1757581938..2eac9258b7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -163,7 +163,8 @@ RUN if grep -i -q alpine /etc/issue && [ `arch` = "x86_64" ]; then \ usr/glibc-compat/lib/libdl.so.2 \ usr/glibc-compat/lib/libm.so.6 \ usr/glibc-compat/lib/libpthread.so.0 \ - usr/glibc-compat/lib/librt.so.1 && \ + usr/glibc-compat/lib/librt.so.1 \ + usr/glibc-compat/lib/libresolv.so.2 && \ mkdir /lib64 && \ ln -s /usr/glibc-compat/lib/ld-linux-x86-64.so.2 /lib64; \ fi From 4d7b9a3c774be34c98ce535f8b2119b4cfc0856d Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Fri, 14 Feb 2025 10:21:08 -0500 Subject: [PATCH 615/894] SQL Expressions: Improve response to frame conversion handling (#100625) - use types.Convert to get a more normalized type returned from GMS. After syncing with GMS maintainers, the underlying type of the row could be different things (e.g. int when unit32, so this calls the Convert method from the GMS types library to normalize the interface. - Clean up fieldValFromRow more - Use IsText to capture different string types - Add more types to test, also update test to use same cmp.Diff method as others --------- Co-authored-by: Sam Jewell <2903904+samjewell@users.noreply.github.com> --- pkg/expr/sql/db_test.go | 51 +++-- pkg/expr/sql/frame_db_conv.go | 385 ++++++++-------------------------- pkg/expr/sql/frame_table.go | 37 ++++ 3 files changed, 163 insertions(+), 310 deletions(-) diff --git a/pkg/expr/sql/db_test.go b/pkg/expr/sql/db_test.go index 5d7d0332da7..57171fbca94 100644 --- a/pkg/expr/sql/db_test.go +++ b/pkg/expr/sql/db_test.go @@ -28,7 +28,7 @@ func TestQueryFrames(t *testing.T) { expected: data.NewFrame( "sqlExpressionRefId", data.NewField("n", nil, []string{"1"}), - ), + ).SetRefID("sqlExpressionRefId"), }, { name: "valid query with no input frames, one row two columns", @@ -38,7 +38,7 @@ func TestQueryFrames(t *testing.T) { "sqlExpressionRefId", data.NewField("name", nil, []string{"sam"}), data.NewField("age", nil, []int8{40}), - ), + ).SetRefID("sqlExpressionRefId"), }, { // TODO: Also ORDER BY to ensure the order is preserved @@ -54,7 +54,7 @@ func TestQueryFrames(t *testing.T) { expected: data.NewFrame( "sqlExpressionRefId", data.NewField("OSS Projects with Typos", nil, []string{"Garfana"}), - ), + ).SetRefID("sqlExpressionRefId"), }, } @@ -62,13 +62,9 @@ func TestQueryFrames(t *testing.T) { t.Run(tt.name, func(t *testing.T) { frame, err := db.QueryFrames(context.Background(), "sqlExpressionRefId", tt.query, tt.input_frames) require.NoError(t, err) - require.NotNil(t, frame.Fields) - require.Equal(t, tt.expected.Name, frame.RefID) - require.Equal(t, len(tt.expected.Fields), len(frame.Fields)) - for i := range tt.expected.Fields { - require.Equal(t, tt.expected.Fields[i].Name, frame.Fields[i].Name) - require.Equal(t, tt.expected.Fields[i].At(0), frame.Fields[i].At(0)) + if diff := cmp.Diff(tt.expected, frame, data.FrameTestCompareOptions()...); diff != "" { + require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff) } }) } @@ -79,20 +75,47 @@ func TestQueryFramesInOut(t *testing.T) { RefID: "a", Name: "a", Fields: []*data.Field{ - data.NewField("time", nil, []time.Time{time.Now(), time.Now()}), - data.NewField("time_nullable", nil, []*time.Time{p(time.Now()), nil}), + data.NewField("time", nil, []time.Time{time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC), time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)}), + data.NewField("time_nullable", nil, []*time.Time{p(time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)), nil}), data.NewField("string", nil, []string{"cat", "dog"}), data.NewField("null_nullable", nil, []*string{p("cat"), nil}), + data.NewField("bool", nil, []bool{true, false}), + data.NewField("bool_nullable", nil, []*bool{p(true), nil}), + + // Floats + data.NewField("float32", nil, []float32{1, 3}), + data.NewField("float32_nullable", nil, []*float32{p(float32(2.0)), nil}), + data.NewField("float64", nil, []float64{1, 3}), - data.NewField("float64_nullable", nil, []*float64{p(2.0), nil}), + data.NewField("float64_nullable", nil, []*float64{p(float64(2.0)), nil}), + + // Ints + data.NewField("int8", nil, []int8{1, 3}), + data.NewField("int8_nullable", nil, []*int8{p(int8(2)), nil}), + + data.NewField("int16", nil, []int16{1, 3}), + data.NewField("int16_nullable", nil, []*int16{p(int16(2)), nil}), + + data.NewField("int32", nil, []int32{1, 3}), + data.NewField("int32_nullable", nil, []*int32{p(int32(2)), nil}), data.NewField("int64", nil, []int64{1, 3}), data.NewField("int64_nullable", nil, []*int64{p(int64(2)), nil}), - data.NewField("bool", nil, []bool{true, false}), - data.NewField("bool_nullable", nil, []*bool{p(true), nil}), + // Unsigned Ints + data.NewField("uint8", nil, []uint8{1, 3}), + data.NewField("uint8_nullable", nil, []*uint8{p(uint8(2)), nil}), + + data.NewField("uint16", nil, []uint16{1, 3}), + data.NewField("uint16_nullable", nil, []*uint16{p(uint16(2)), nil}), + + data.NewField("uint32", nil, []uint32{1, 3}), + data.NewField("uint32_nullable", nil, []*uint32{p(uint32(2)), nil}), + + data.NewField("uint64", nil, []uint64{1, 3}), + data.NewField("uint64_nullable", nil, []*uint64{p(uint64(2)), nil}), }, } diff --git a/pkg/expr/sql/frame_db_conv.go b/pkg/expr/sql/frame_db_conv.go index 81e59fd9f32..302ff79d1bb 100644 --- a/pkg/expr/sql/frame_db_conv.go +++ b/pkg/expr/sql/frame_db_conv.go @@ -23,7 +23,6 @@ func convertToDataFrame(ctx *mysql.Context, iter mysql.RowIter, schema mysql.Sch if err != nil { return nil, err } - field := data.NewFieldFromFieldType(fT, 0) field.Name = col.Name f.Fields = append(f.Fields, field) @@ -40,11 +39,22 @@ func convertToDataFrame(ctx *mysql.Context, iter mysql.RowIter, schema mysql.Sch } for i, val := range row { - v, err := fieldValFromRowVal(f.Fields[i].Type(), val) + // Run val through mysql.Type.Convert to normalize underlying value + // of the interface + nV, _, err := schema[i].Type.Convert(val) + if err != nil { + return nil, err + } + + // Run the normalized value through fieldValFromRowVal to normalize + // the interface type to the dataframe value type, and make nullable + // values pointers as dataframe expects. + fV, err := fieldValFromRowVal(f.Fields[i].Type(), nV) if err != nil { return nil, fmt.Errorf("unexpected type for column %s: %w", schema[i].Name, err) } - f.Fields[i].Append(v) + + f.Fields[i].Append(fV) } } @@ -72,11 +82,10 @@ func MySQLColToFieldType(col *mysql.Column) (data.FieldType, error) { fT = data.FieldTypeInt64 case types.Uint64: fT = data.FieldTypeUint64 + case types.Float32: + fT = data.FieldTypeFloat32 case types.Float64: fT = data.FieldTypeFloat64 - // StringType represents all string types, including VARCHAR and BLOB. - case types.Text, types.LongText: - fT = data.FieldTypeString case types.Timestamp: fT = data.FieldTypeTime case types.Datetime: @@ -84,9 +93,12 @@ func MySQLColToFieldType(col *mysql.Column) (data.FieldType, error) { case types.Boolean: fT = data.FieldTypeBool default: - if types.IsDecimal(col.Type) { + switch { + case types.IsDecimal(col.Type): fT = data.FieldTypeFloat64 - } else { + case types.IsText(col.Type): + fT = data.FieldTypeString + default: return fT, fmt.Errorf("unsupported type for column %s of type %v", col.Name, col.Type) } } @@ -98,315 +110,96 @@ func MySQLColToFieldType(col *mysql.Column) (data.FieldType, error) { return fT, nil } -// Helper function to convert data.FieldType to types.Type -func convertDataType(fieldType data.FieldType) mysql.Type { - switch fieldType { - case data.FieldTypeInt8, data.FieldTypeNullableInt8: - return types.Int8 - case data.FieldTypeUint8, data.FieldTypeNullableUint8: - return types.Uint8 - case data.FieldTypeInt16, data.FieldTypeNullableInt16: - return types.Int16 - case data.FieldTypeUint16, data.FieldTypeNullableUint16: - return types.Uint16 - case data.FieldTypeInt32, data.FieldTypeNullableInt32: - return types.Int32 - case data.FieldTypeUint32, data.FieldTypeNullableUint32: - return types.Uint32 - case data.FieldTypeInt64, data.FieldTypeNullableInt64: - return types.Int64 - case data.FieldTypeUint64, data.FieldTypeNullableUint64: - return types.Uint64 - case data.FieldTypeFloat32, data.FieldTypeNullableFloat32: - return types.Float32 - case data.FieldTypeFloat64, data.FieldTypeNullableFloat64: - return types.Float64 - case data.FieldTypeString, data.FieldTypeNullableString: - return types.Text - case data.FieldTypeBool, data.FieldTypeNullableBool: - return types.Boolean - case data.FieldTypeTime, data.FieldTypeNullableTime: - return types.Timestamp - default: - fmt.Printf("------- Unsupported field type: %v", fieldType) - return types.JSON - } -} - // fieldValFromRowVal converts a go-mysql-server row value to a data.field value -// -//nolint:gocyclo func fieldValFromRowVal(fieldType data.FieldType, val interface{}) (interface{}, error) { - // the input val may be nil, it also may not be a pointer even if the fieldtype is a nullable pointer type + // if the input interface is nil, we can return an untyped nil if val == nil { return nil, nil } + nullable := fieldType.Nullable() + switch fieldType { - // ---------------------------- - // Int8 / Nullable Int8 - // ---------------------------- - case data.FieldTypeInt8: - v, ok := val.(int8) - if !ok { - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int8", val, val) - } - return v, nil + case data.FieldTypeInt8, data.FieldTypeNullableInt8: + return parseVal[int8](val, "int8", nullable) - case data.FieldTypeNullableInt8: - vP, ok := val.(*int8) - if ok { - return vP, nil - } - v, ok := val.(int8) - if ok { - return &v, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int8 or *int8", val, val) + case data.FieldTypeUint8, data.FieldTypeNullableUint8: + return parseVal[uint8](val, "uint8", nullable) - // ---------------------------- - // Uint8 / Nullable Uint8 - // ---------------------------- - case data.FieldTypeUint8: - v, ok := val.(uint8) - if !ok { - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint8", val, val) - } - return v, nil + case data.FieldTypeInt16, data.FieldTypeNullableInt16: + return parseVal[int16](val, "int16", nullable) - case data.FieldTypeNullableUint8: - vP, ok := val.(*uint8) - if ok { - return vP, nil - } - v, ok := val.(uint8) - if ok { - return &v, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint8 or *uint8", val, val) + case data.FieldTypeUint16, data.FieldTypeNullableUint16: + return parseVal[uint16](val, "uint16", nullable) - // ---------------------------- - // Int16 / Nullable Int16 - // ---------------------------- - case data.FieldTypeInt16: - v, ok := val.(int16) - if !ok { - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int16", val, val) - } - return v, nil + case data.FieldTypeInt32, data.FieldTypeNullableInt32: + return parseVal[int32](val, "int32", nullable) - case data.FieldTypeNullableInt16: - vP, ok := val.(*int16) - if ok { - return vP, nil - } - v, ok := val.(int16) - if ok { - return &v, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int16 or *int16", val, val) + case data.FieldTypeUint32, data.FieldTypeNullableUint32: + return parseVal[uint32](val, "uint32", nullable) - // ---------------------------- - // Uint16 / Nullable Uint16 - // ---------------------------- - case data.FieldTypeUint16: - v, ok := val.(uint16) - if !ok { - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint16", val, val) - } - return v, nil + case data.FieldTypeInt64, data.FieldTypeNullableInt64: + return parseVal[int64](val, "int64", nullable) - case data.FieldTypeNullableUint16: - vP, ok := val.(*uint16) - if ok { - return vP, nil - } - v, ok := val.(uint16) - if ok { - return &v, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint16 or *uint16", val, val) + case data.FieldTypeUint64, data.FieldTypeNullableUint64: + return parseVal[uint64](val, "uint64", nullable) - // ---------------------------- - // Int32 / Nullable Int32 - // ---------------------------- - case data.FieldTypeInt32: - v, ok := val.(int32) - if !ok { - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int32", val, val) - } - return v, nil + case data.FieldTypeFloat32, data.FieldTypeNullableFloat32: + return parseVal[float32](val, "float32", nullable) - case data.FieldTypeNullableInt32: - vP, ok := val.(*int32) - if ok { - return vP, nil - } - v, ok := val.(int32) - if ok { - return &v, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int32 or *int32", val, val) + case data.FieldTypeFloat64, data.FieldTypeNullableFloat64: + return parseFloat64OrDecimal(val, nullable) - // ---------------------------- - // Uint32 / Nullable Uint32 - // ---------------------------- - case data.FieldTypeUint32: - v, ok := val.(uint32) - if !ok { - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint32", val, val) - } - return v, nil + case data.FieldTypeTime, data.FieldTypeNullableTime: + return parseVal[time.Time](val, "time.Time", nullable) - case data.FieldTypeNullableUint32: - vP, ok := val.(*uint32) - if ok { - return vP, nil - } - v, ok := val.(uint32) - if ok { - return &v, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint32 or *uint32", val, val) + case data.FieldTypeString, data.FieldTypeNullableString: + return parseVal[string](val, "string", nullable) - // ---------------------------- - // Int64 / Nullable Int64 - // ---------------------------- - case data.FieldTypeInt64: - v, ok := val.(int64) - if !ok { - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int64", val, val) - } - return v, nil + case data.FieldTypeBool, data.FieldTypeNullableBool: + return parseBoolFromInt8(val, nullable) - case data.FieldTypeNullableInt64: - vP, ok := val.(*int64) - if ok { - return vP, nil - } - v, ok := val.(int64) - if ok { - return &v, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int64 or *int64", val, val) - - // ---------------------------- - // Uint64 / Nullable Uint64 - // ---------------------------- - case data.FieldTypeUint64: - v, ok := val.(uint64) - if !ok { - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint64", val, val) - } - return v, nil - - case data.FieldTypeNullableUint64: - vP, ok := val.(*uint64) - if ok { - return vP, nil - } - v, ok := val.(uint64) - if ok { - return &v, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint64 or *uint64", val, val) - - // ---------------------------- - // Float64 / Nullable Float64 - // ---------------------------- - case data.FieldTypeFloat64: - // Accept float64 or decimal.Decimal, convert decimal.Decimal -> float64 - if v, ok := val.(float64); ok { - return v, nil - } - if d, ok := val.(decimal.Decimal); ok { - return d.InexactFloat64(), nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected float64 or decimal.Decimal", val, val) - - case data.FieldTypeNullableFloat64: - // Possibly already *float64 - if vP, ok := val.(*float64); ok { - return vP, nil - } - // Possibly float64 - if v, ok := val.(float64); ok { - return &v, nil - } - // Possibly decimal.Decimal - if d, ok := val.(decimal.Decimal); ok { - f := d.InexactFloat64() - return &f, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected float64, *float64, or decimal.Decimal", val, val) - - // ---------------------------- - // Time / Nullable Time - // ---------------------------- - case data.FieldTypeTime: - v, ok := val.(time.Time) - if !ok { - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected time.Time", val, val) - } - return v, nil - - case data.FieldTypeNullableTime: - vP, ok := val.(*time.Time) - if ok { - return vP, nil - } - v, ok := val.(time.Time) - if ok { - return &v, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected time.Time or *time.Time", val, val) - - // ---------------------------- - // String / Nullable String - // ---------------------------- - case data.FieldTypeString: - v, ok := val.(string) - if !ok { - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected string", val, val) - } - return v, nil - - case data.FieldTypeNullableString: - vP, ok := val.(*string) - if ok { - return vP, nil - } - v, ok := val.(string) - if ok { - return &v, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected string or *string", val, val) - - // ---------------------------- - // Bool / Nullable Bool - // ---------------------------- - case data.FieldTypeBool: - v, ok := val.(bool) - if !ok { - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected bool", val, val) - } - return v, nil - - case data.FieldTypeNullableBool: - vP, ok := val.(*bool) - if ok { - return vP, nil - } - v, ok := val.(bool) - if ok { - return &v, nil - } - return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected bool or *bool", val, val) - - // ---------------------------- - // Fallback / Unsupported - // ---------------------------- default: return nil, fmt.Errorf("unsupported field type %s for val %v", fieldType, val) } } + +// parseVal attempts to assert `val` as type T. If successful, it returns either +// the value or a pointer, depending on `isNullable`. If not, returns an error. +func parseVal[T any](val interface{}, typeName string, isNullable bool) (interface{}, error) { + v, ok := val.(T) + if !ok { + return nil, fmt.Errorf("unexpected value type %v of type %T, expected %s", val, val, typeName) + } + return ptrIfNull(v, isNullable), nil +} + +// parseFloat64OrDecimal handles the special case where val can be float64 or decimal.Decimal. +func parseFloat64OrDecimal(val interface{}, isNullable bool) (interface{}, error) { + if fv, ok := val.(float64); ok { + return ptrIfNull(fv, isNullable), nil + } + if d, ok := val.(decimal.Decimal); ok { + return ptrIfNull(d.InexactFloat64(), isNullable), nil + } + return nil, fmt.Errorf("unexpected value type %v of type %T, expected float64 or decimal.Decimal", val, val) +} + +// parseBoolFromInt8 asserts val as an int8, converts non-zero to true. +// Returns pointer if isNullable, otherwise the bool value. +func parseBoolFromInt8(val interface{}, isNullable bool) (interface{}, error) { + v, ok := val.(int8) + if !ok { + return nil, fmt.Errorf("unexpected value type %v of type %T, expected int8 (for bool)", val, val) + } + b := (v != 0) + return ptrIfNull(b, isNullable), nil +} + +// ptrIfNull returns a pointer to val if isNullable is true; otherwise, returns val. +func ptrIfNull[T any](val T, isNullable bool) interface{} { + if isNullable { + return &val + } + return val +} diff --git a/pkg/expr/sql/frame_table.go b/pkg/expr/sql/frame_table.go index 7ddf7f1dd39..66b05cfaa60 100644 --- a/pkg/expr/sql/frame_table.go +++ b/pkg/expr/sql/frame_table.go @@ -3,10 +3,12 @@ package sql import ( + "fmt" "io" "strings" mysql "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" "github.com/grafana/grafana-plugin-sdk-go/data" ) @@ -124,3 +126,38 @@ type partition []byte func (p partition) Key() []byte { return p } + +// Helper function to convert data.FieldType to types.Type +func convertDataType(fieldType data.FieldType) mysql.Type { + switch fieldType { + case data.FieldTypeInt8, data.FieldTypeNullableInt8: + return types.Int8 + case data.FieldTypeUint8, data.FieldTypeNullableUint8: + return types.Uint8 + case data.FieldTypeInt16, data.FieldTypeNullableInt16: + return types.Int16 + case data.FieldTypeUint16, data.FieldTypeNullableUint16: + return types.Uint16 + case data.FieldTypeInt32, data.FieldTypeNullableInt32: + return types.Int32 + case data.FieldTypeUint32, data.FieldTypeNullableUint32: + return types.Uint32 + case data.FieldTypeInt64, data.FieldTypeNullableInt64: + return types.Int64 + case data.FieldTypeUint64, data.FieldTypeNullableUint64: + return types.Uint64 + case data.FieldTypeFloat32, data.FieldTypeNullableFloat32: + return types.Float32 + case data.FieldTypeFloat64, data.FieldTypeNullableFloat64: + return types.Float64 + case data.FieldTypeString, data.FieldTypeNullableString: + return types.Text + case data.FieldTypeBool, data.FieldTypeNullableBool: + return types.Boolean + case data.FieldTypeTime, data.FieldTypeNullableTime: + return types.Timestamp + default: + fmt.Printf("------- Unsupported field type: %v", fieldType) + return types.JSON + } +} From c522a5b13b1e6a93cfcdeae719bd47774f3e4636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Fri, 14 Feb 2025 16:23:25 +0100 Subject: [PATCH 616/894] fix(unified-storage): proper setup TLS in new db_engine for MySQL (#100686) --- .../unified/sql/db/dbimpl/dbEngine_test.go | 118 ------- .../db/dbimpl/{dbEngine.go => db_engine.go} | 65 +++- .../unified/sql/db/dbimpl/db_engine_test.go | 301 ++++++++++++++++++ 3 files changed, 353 insertions(+), 131 deletions(-) delete mode 100644 pkg/storage/unified/sql/db/dbimpl/dbEngine_test.go rename pkg/storage/unified/sql/db/dbimpl/{dbEngine.go => db_engine.go} (69%) create mode 100644 pkg/storage/unified/sql/db/dbimpl/db_engine_test.go diff --git a/pkg/storage/unified/sql/db/dbimpl/dbEngine_test.go b/pkg/storage/unified/sql/db/dbimpl/dbEngine_test.go deleted file mode 100644 index 583e2bcdc99..00000000000 --- a/pkg/storage/unified/sql/db/dbimpl/dbEngine_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package dbimpl - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func newValidMySQLGetter(withKeyPrefix bool) confGetter { - var prefix string - if withKeyPrefix { - prefix = "db_" - } - return newTestConfGetter(map[string]string{ - prefix + "type": dbTypeMySQL, - prefix + "host": "/var/run/mysql.socket", - prefix + "name": "grafana", - prefix + "user": "user", - prefix + "password": "password", - }, prefix) -} - -func TestGetEngineMySQLFromConfig(t *testing.T) { - t.Parallel() - - t.Run("happy path - with key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEngineMySQL(newValidMySQLGetter(true)) - assert.NotNil(t, engine) - assert.NoError(t, err) - }) - - t.Run("happy path - without key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEngineMySQL(newValidMySQLGetter(false)) - assert.NotNil(t, engine) - assert.NoError(t, err) - }) - - t.Run("invalid string", func(t *testing.T) { - t.Parallel() - - getter := newTestConfGetter(map[string]string{ - "db_type": dbTypeMySQL, - "db_host": "/var/run/mysql.socket", - "db_name": string(invalidUTF8ByteSequence), - "db_user": "user", - "db_password": "password", - }, "db_") - engine, err := getEngineMySQL(getter) - assert.Nil(t, engine) - assert.Error(t, err) - assert.ErrorIs(t, err, errInvalidUTF8Sequence) - }) -} - -func newValidPostgresGetter(withKeyPrefix bool) confGetter { - var prefix string - if withKeyPrefix { - prefix = "db_" - } - return newTestConfGetter(map[string]string{ - prefix + "type": dbTypePostgres, - prefix + "host": "localhost", - prefix + "name": "grafana", - prefix + "user": "user", - prefix + "password": "password", - }, prefix) -} - -func TestGetEnginePostgresFromConfig(t *testing.T) { - t.Parallel() - - t.Run("happy path - with key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEnginePostgres(newValidPostgresGetter(true)) - assert.NotNil(t, engine) - assert.NoError(t, err) - }) - - t.Run("happy path - without key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEnginePostgres(newValidPostgresGetter(false)) - assert.NotNil(t, engine) - assert.NoError(t, err) - }) - - t.Run("invalid string", func(t *testing.T) { - t.Parallel() - getter := newTestConfGetter(map[string]string{ - "db_type": dbTypePostgres, - "db_host": string(invalidUTF8ByteSequence), - "db_name": "grafana", - "db_user": "user", - "db_password": "password", - }, "db_") - engine, err := getEnginePostgres(getter) - - assert.Nil(t, engine) - assert.Error(t, err) - assert.ErrorIs(t, err, errInvalidUTF8Sequence) - }) - - t.Run("invalid hostport", func(t *testing.T) { - t.Parallel() - getter := newTestConfGetter(map[string]string{ - "db_type": dbTypePostgres, - "db_host": "1:1:1", - "db_name": "grafana", - "db_user": "user", - "db_password": "password", - }, "db_") - engine, err := getEnginePostgres(getter) - - assert.Nil(t, engine) - assert.Error(t, err) - }) -} diff --git a/pkg/storage/unified/sql/db/dbimpl/dbEngine.go b/pkg/storage/unified/sql/db/dbimpl/db_engine.go similarity index 69% rename from pkg/storage/unified/sql/db/dbimpl/dbEngine.go rename to pkg/storage/unified/sql/db/dbimpl/db_engine.go index ddc4aa4ca4d..529cd163eb4 100644 --- a/pkg/storage/unified/sql/db/dbimpl/dbEngine.go +++ b/pkg/storage/unified/sql/db/dbimpl/db_engine.go @@ -7,11 +7,17 @@ import ( "time" "github.com/go-sql-driver/mysql" + "github.com/grafana/dskit/crypto/tls" + "xorm.io/xorm" "github.com/grafana/grafana/pkg/storage/unified/sql/db" ) +// tlsConfigName is the name of the TLS config that we register with the MySQL +// driver. +const tlsConfigName = "db_engine_tls" + func getEngineMySQL(getter confGetter) (*xorm.Engine, error) { config := mysql.NewConfig() config.User = getter.String("user") @@ -25,29 +31,22 @@ func getEngineMySQL(getter confGetter) (*xorm.Engine, error) { // See: https://dev.mysql.com/doc/refman/en/sql-mode.html "@@SESSION.sql_mode": "ANSI", } - sslMode := getter.String("ssl_mode") - if sslMode == "true" || sslMode == "skip-verify" { - config.Params["tls"] = "preferred" - } - tls := getter.String("tls") - if tls != "" { - config.Params["tls"] = tls - } config.Collation = "utf8mb4_unicode_ci" config.Loc = time.UTC config.AllowNativePasswords = true config.ClientFoundRows = true config.ParseTime = true + // Setup TLS for the database connection if configured. + if err := configureTLS(getter, config); err != nil { + return nil, fmt.Errorf("failed to configure TLS: %w", err) + } + // allow executing multiple SQL statements in a single roundtrip, and also // enable executing the CALL statement to run stored procedures that execute // multiple SQL statements. //config.MultiStatements = true - // TODO: do we want to support these? - // config.ServerPubKey = getter.String("server_pub_key") - // config.TLSConfig = getter.String("tls_config_name") - if err := getter.Err(); err != nil { return nil, fmt.Errorf("config error: %w", err) } @@ -56,7 +55,6 @@ func getEngineMySQL(getter confGetter) (*xorm.Engine, error) { config.Net = "unix" } - // FIXME: get rid of xorm engine, err := xorm.NewEngine(db.DriverMySQL, config.FormatDSN()) if err != nil { return nil, fmt.Errorf("open database: %w", err) @@ -69,6 +67,47 @@ func getEngineMySQL(getter confGetter) (*xorm.Engine, error) { return engine, nil } +func configureTLS(getter confGetter, config *mysql.Config) error { + sslMode := getter.String("ssl_mode") + + if sslMode == "true" || sslMode == "skip-verify" { + tlsCfg := tls.ClientConfig{ + CAPath: getter.String("ca_cert_path"), + CertPath: getter.String("client_cert_path"), + KeyPath: getter.String("client_key_path"), + ServerName: getter.String("server_cert_name"), + } + + rawTLSCfg, err := tlsCfg.GetTLSConfig() + if err != nil { + return fmt.Errorf("failed to get TLS config for mysql: %w", err) + } + + if sslMode == "skip-verify" { + rawTLSCfg.InsecureSkipVerify = true + } + + if err := mysql.RegisterTLSConfig(tlsConfigName, rawTLSCfg); err != nil { + return fmt.Errorf("failed to register TLS config for mysql: %w", err) + } + + config.TLSConfig = tlsConfigName + } + + // If the TLS mode is set in the database config, we need to set it here. + if tls := getter.String("tls"); tls != "" { + // If the user has provided TLS certs, we don't want to use the tls=, as + // they would override the TLS config that we set above. They both use the same + // parameter, so we need to check for that. + if sslMode == "true" { + return fmt.Errorf("cannot provide tls certs and tls= at the same time") + } + config.Params["tls"] = tls + } + + return nil +} + func getEnginePostgres(getter confGetter) (*xorm.Engine, error) { dsnKV := map[string]string{ "user": getter.String("user"), diff --git a/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go b/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go new file mode 100644 index 00000000000..8b39100ffb3 --- /dev/null +++ b/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go @@ -0,0 +1,301 @@ +package dbimpl + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func newValidMySQLGetter(withKeyPrefix bool) confGetter { + var prefix string + if withKeyPrefix { + prefix = "db_" + } + return newTestConfGetter(map[string]string{ + prefix + "type": dbTypeMySQL, + prefix + "host": "/var/run/mysql.socket", + prefix + "name": "grafana", + prefix + "user": "user", + prefix + "password": "password", + }, prefix) +} + +func TestGetEngineMySQLFromConfig(t *testing.T) { + t.Parallel() + + t.Run("happy path - with key prefix", func(t *testing.T) { + t.Parallel() + engine, err := getEngineMySQL(newValidMySQLGetter(true)) + require.NotNil(t, engine) + require.NoError(t, err) + }) + + t.Run("happy path - without key prefix", func(t *testing.T) { + t.Parallel() + engine, err := getEngineMySQL(newValidMySQLGetter(false)) + require.NotNil(t, engine) + require.NoError(t, err) + }) + + t.Run("invalid string", func(t *testing.T) { + t.Parallel() + + getter := newTestConfGetter(map[string]string{ + "db_type": dbTypeMySQL, + "db_host": "/var/run/mysql.socket", + "db_name": string(invalidUTF8ByteSequence), + "db_user": "user", + "db_password": "password", + }, "db_") + engine, err := getEngineMySQL(getter) + require.Nil(t, engine) + require.Error(t, err) + require.ErrorIs(t, err, errInvalidUTF8Sequence) + }) +} + +func newValidPostgresGetter(withKeyPrefix bool) confGetter { + var prefix string + if withKeyPrefix { + prefix = "db_" + } + return newTestConfGetter(map[string]string{ + prefix + "type": dbTypePostgres, + prefix + "host": "localhost", + prefix + "name": "grafana", + prefix + "user": "user", + prefix + "password": "password", + }, prefix) +} + +func TestGetEnginePostgresFromConfig(t *testing.T) { + t.Parallel() + + t.Run("happy path - with key prefix", func(t *testing.T) { + t.Parallel() + engine, err := getEnginePostgres(newValidPostgresGetter(true)) + require.NotNil(t, engine) + require.NoError(t, err) + }) + + t.Run("happy path - without key prefix", func(t *testing.T) { + t.Parallel() + engine, err := getEnginePostgres(newValidPostgresGetter(false)) + require.NotNil(t, engine) + require.NoError(t, err) + }) + + t.Run("invalid string", func(t *testing.T) { + t.Parallel() + getter := newTestConfGetter(map[string]string{ + "db_type": dbTypePostgres, + "db_host": string(invalidUTF8ByteSequence), + "db_name": "grafana", + "db_user": "user", + "db_password": "password", + }, "db_") + engine, err := getEnginePostgres(getter) + + require.Nil(t, engine) + require.Error(t, err) + }) + + t.Run("invalid hostport", func(t *testing.T) { + t.Parallel() + getter := newTestConfGetter(map[string]string{ + "db_type": dbTypePostgres, + "db_host": "1:1:1", + "db_name": "grafana", + "db_user": "user", + "db_password": "password", + }, "db_") + engine, err := getEnginePostgres(getter) + + require.Nil(t, engine) + require.Error(t, err) + }) +} + +func TestGetEngineMySQLTLS(t *testing.T) { + certs := generateTestCerts(t) + + tests := []struct { + name string + config map[string]string + shouldErr bool + }{ + { + name: "with TLS disabled", + config: map[string]string{ + "type": "mysql", + "user": "user", + "pass": "pass", + "host": "localhost", + "name": "dbname", + "ssl_mode": "disable", + }, + }, + { + name: "with TLS skip-verify", + config: map[string]string{ + "type": "mysql", + "user": "user", + "pass": "pass", + "host": "localhost", + "name": "dbname", + "ssl_mode": "skip-verify", + }, + }, + { + name: "with valid TLS certificates", + config: map[string]string{ + "type": "mysql", + "user": "user", + "pass": "pass", + "host": "localhost", + "name": "dbname", + "ssl_mode": "true", + "ca_cert_path": certs.caFile, + "client_cert_path": certs.certFile, + "client_key_path": certs.keyFile, + "server_cert_name": "mysql.example.com", + }, + }, + { + name: "with invalid cert paths", + config: map[string]string{ + "type": "mysql", + "user": "user", + "pass": "pass", + "host": "localhost", + "name": "dbname", + "ssl_mode": "true", + "ca_cert_path": "nonexistent/ca.pem", + "client_cert_path": "nonexistent/client-cert.pem", + "client_key_path": "nonexistent/client-key.pem", + "server_cert_name": "mysql.example.com", + }, + shouldErr: true, + }, + { + name: "with TLS certs and tls parameter", + config: map[string]string{ + "type": "mysql", + "user": "user", + "pass": "pass", + "host": "localhost", + "name": "dbname", + "ssl_mode": "true", + "ca_cert_path": certs.caFile, + "client_cert_path": certs.certFile, + "client_key_path": certs.keyFile, + "server_cert_name": "mysql.example.com", + "tls": "preferred", + }, + shouldErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + getter := newTestConfGetter(tt.config, "") + engine, err := getEngineMySQL(getter) + + if tt.shouldErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.NotNil(t, engine) + }) + } +} + +type testCerts struct { + caFile string + certFile string + keyFile string +} + +func generateTestCerts(t *testing.T) testCerts { + t.Helper() + tempDir := t.TempDir() + + // Generate CA private key + caKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + // Generate CA certificate + ca := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Test CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + } + + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caKey.PublicKey, caKey) + require.NoError(t, err) + + clientKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + client := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{ + CommonName: "Test Client", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + SubjectKeyId: []byte{1, 2, 3, 4, 5}, + } + + clientBytes, err := x509.CreateCertificate(rand.Reader, client, ca, &clientKey.PublicKey, caKey) + require.NoError(t, err) + + // Write certificates and keys to temporary files + caFile := filepath.Join(tempDir, "ca.pem") + certFile := filepath.Join(tempDir, "cert.pem") + keyFile := filepath.Join(tempDir, "key.pem") + + writePEMFile(t, caFile, "CERTIFICATE", caBytes) + writePEMFile(t, certFile, "CERTIFICATE", clientBytes) + writePEMFile(t, keyFile, "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(clientKey)) + + return testCerts{ + caFile: caFile, + certFile: certFile, + keyFile: keyFile, + } +} + +func writePEMFile(t *testing.T, filename string, blockType string, bytes []byte) { + t.Helper() + //nolint:gosec + file, err := os.Create(filename) + require.NoError(t, err) + //nolint:errcheck + defer file.Close() + + err = pem.Encode(file, &pem.Block{ + Type: blockType, + Bytes: bytes, + }) + require.NoError(t, err) +} From 37ee1c427d10d9f19e183329e9e0e5dc8f9a75b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:33:45 +0100 Subject: [PATCH 617/894] Plugins: Upgrade grafana-plugin-sdk to 0.266.0 (#100727) --- apps/alerting/notifications/go.mod | 4 ++-- apps/alerting/notifications/go.sum | 8 ++++---- apps/investigation/go.mod | 4 ++-- apps/investigation/go.sum | 8 ++++---- apps/playlist/go.mod | 4 ++-- apps/playlist/go.sum | 8 ++++---- go.mod | 6 +++--- go.sum | 12 ++++++------ go.work.sum | 8 ++++++-- pkg/aggregator/go.mod | 6 +++--- pkg/aggregator/go.sum | 12 ++++++------ pkg/apimachinery/go.mod | 2 +- pkg/apimachinery/go.sum | 4 ++-- pkg/apiserver/go.mod | 4 ++-- pkg/apiserver/go.sum | 8 ++++---- pkg/build/go.mod | 4 ++-- pkg/build/go.sum | 8 ++++---- pkg/promlib/go.mod | 4 ++-- pkg/promlib/go.sum | 12 ++++++------ pkg/storage/unified/apistore/go.mod | 6 +++--- pkg/storage/unified/apistore/go.sum | 12 ++++++------ pkg/storage/unified/resource/go.mod | 6 +++--- pkg/storage/unified/resource/go.sum | 12 ++++++------ 23 files changed, 83 insertions(+), 79 deletions(-) diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 348d3e997ca..de67c2ef820 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -75,8 +75,8 @@ require ( golang.org/x/crypto v0.32.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.9.0 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 88f0f30100a..60a25e25bcd 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -226,8 +226,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -237,8 +237,8 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/apps/investigation/go.mod b/apps/investigation/go.mod index c931cbb36a2..3f70e0fa49f 100644 --- a/apps/investigation/go.mod +++ b/apps/investigation/go.mod @@ -63,9 +63,9 @@ require ( go.opentelemetry.io/otel/trace v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.9.0 // indirect diff --git a/apps/investigation/go.sum b/apps/investigation/go.sum index eb9c8887d1d..f6f6a0e3981 100644 --- a/apps/investigation/go.sum +++ b/apps/investigation/go.sum @@ -157,8 +157,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -167,8 +167,8 @@ golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 214e97e3054..ccc6715d0f5 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -64,9 +64,9 @@ require ( go.opentelemetry.io/otel/trace v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.9.0 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index eb9c8887d1d..f6f6a0e3981 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -157,8 +157,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -167,8 +167,8 @@ golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/go.mod b/go.mod index 9b05a5c2234..bc40220af87 100644 --- a/go.mod +++ b/go.mod @@ -87,7 +87,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.265.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.266.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 @@ -172,7 +172,7 @@ require ( golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // @grafana/alerting-backend golang.org/x/mod v0.22.0 // indirect; @grafana/grafana-backend-group golang.org/x/net v0.34.0 // @grafana/oss-big-tent @grafana/partner-datasources - golang.org/x/oauth2 v0.25.0 // @grafana/identity-access-team + golang.org/x/oauth2 v0.26.0 // @grafana/identity-access-team golang.org/x/sync v0.11.0 // @grafana/alerting-backend golang.org/x/text v0.21.0 // @grafana/grafana-backend-group golang.org/x/time v0.9.0 // @grafana/grafana-backend-group @@ -518,7 +518,7 @@ require ( go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 8f98d32599f..ec6d4334aa0 100644 --- a/go.sum +++ b/go.sum @@ -1545,8 +1545,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.265.0 h1:XshoH8R23Jm9jRreW9R3aOrIVr9vxhCWFyrMe7BFSks= -github.com/grafana/grafana-plugin-sdk-go v0.265.0/go.mod h1:nkN6xI08YcX6CGsgvRA2+19nhXA/ZPuneLMUUElOD80= +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/apps/advisor v0.0.0-20250123151950-b066a6313173 h1:uOM89HiWVVOTls0LrD4coHTckb2lA4U0sIJwCYdbhbw= github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d h1:NRVOtiG1aUwOazBj9KM7X2o2shsM6TchqisezzoH1gw= @@ -2738,8 +2738,8 @@ golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4 golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2889,8 +2889,8 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/go.work.sum b/go.work.sum index 832ae031f57..c257c4c9d6f 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1066,7 +1066,6 @@ github.com/IBM/ibm-cos-sdk-go v1.11.0/go.mod h1:FnWOym0CvrPM0nHoXvceClOEvGVXecPp github.com/IBM/sarama v1.43.1/go.mod h1:GG5q1RURtDNPz8xxJs3mgX6Ytak8Z9eLhAkJPObe2xE= github.com/IBM/sarama v1.43.2 h1:HABeEqRUh32z8yzY2hGB/j8mHSzC/HA9zlEjqFNCzSw= github.com/IBM/sarama v1.43.2/go.mod h1:Kyo4WkF24Z+1nz7xeVUFWIuKVV8RS3wM8mkvPKMdXFQ= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/KimMachineGun/automemlimit v0.6.0 h1:p/BXkH+K40Hax+PuWWPQ478hPjsp9h1CPDhLlA3Z37E= @@ -1455,7 +1454,6 @@ github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDs github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= -github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= @@ -1557,6 +1555,8 @@ github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD github.com/grafana/grafana-app-sdk v0.29.0/go.mod h1:XLt308EmK6kvqPlzjUyXxbwZKEk2vur/eiypUNDay5I= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.5/go.mod h1:i0uiuu9/sMFBJnpFbjvviH0KOZzdWkti9Q9Ck1HkFWM= github.com/grafana/grafana-plugin-sdk-go v0.262.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= +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/apps/advisor v0.0.0-20250121115006-c1eac9f9973f/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= github.com/grafana/grafana/pkg/promlib v0.0.7/go.mod h1:rnwJXCA2xRwb7F27NB35iO/JsLL/H/+eVXECk/hrEhQ= github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= @@ -2430,6 +2430,8 @@ golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= @@ -2459,6 +2461,8 @@ golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index e60db59ff74..2f254da3755 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.265.0 + github.com/grafana/grafana-plugin-sdk-go v0.266.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 @@ -138,9 +138,9 @@ require ( golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.9.0 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 05728e114af..dcacd54e2da 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -134,8 +134,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.265.0 h1:XshoH8R23Jm9jRreW9R3aOrIVr9vxhCWFyrMe7BFSks= -github.com/grafana/grafana-plugin-sdk-go v0.265.0/go.mod h1:nkN6xI08YcX6CGsgvRA2+19nhXA/ZPuneLMUUElOD80= +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/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= @@ -421,8 +421,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -445,8 +445,8 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index a9074f89305..03ac780af13 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -39,7 +39,7 @@ require ( golang.org/x/crypto v0.32.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.21.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 852b52d54c3..8e526b201f1 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -122,8 +122,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index a4d60df1efe..4676380219e 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -82,8 +82,8 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.9.0 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 8204c0082b3..b0c1e3d43de 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -254,8 +254,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -267,8 +267,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index a7192113337..8f500929041 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -30,7 +30,7 @@ require ( golang.org/x/crypto v0.32.0 // indirect; @grafana/grafana-backend-group golang.org/x/mod v0.22.0 // @grafana/grafana-backend-group golang.org/x/net v0.34.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources - golang.org/x/oauth2 v0.25.0 // @grafana/identity-access-team + golang.org/x/oauth2 v0.26.0 // @grafana/identity-access-team golang.org/x/sync v0.11.0 // indirect; @grafana/alerting-backend golang.org/x/text v0.21.0 // indirect; @grafana/grafana-backend-group golang.org/x/time v0.9.0 // indirect; @grafana/grafana-backend-group @@ -75,7 +75,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect; @grafana/grafana-backend-group google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect diff --git a/pkg/build/go.sum b/pkg/build/go.sum index 3aafd25e523..762068aef69 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -293,8 +293,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -312,8 +312,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 287d0d33ca6..3d743ca89bd 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.265.0 + github.com/grafana/grafana-plugin-sdk-go v0.266.0 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.20.5 github.com/prometheus/common v0.62.0 @@ -111,7 +111,7 @@ require ( golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.29.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 315ec4e7f57..9f9b18c82b0 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -120,8 +120,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.265.0 h1:XshoH8R23Jm9jRreW9R3aOrIVr9vxhCWFyrMe7BFSks= -github.com/grafana/grafana-plugin-sdk-go v0.265.0/go.mod h1:nkN6xI08YcX6CGsgvRA2+19nhXA/ZPuneLMUUElOD80= +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/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= @@ -344,8 +344,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -363,8 +363,8 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index b18163780f8..66c04c116bc 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -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.265.0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.266.0 // indirect github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d // indirect github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d // indirect @@ -369,9 +369,9 @@ require ( golang.org/x/crypto v0.32.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.9.0 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 6d345c4151e..60549ccc425 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -584,8 +584,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.265.0 h1:XshoH8R23Jm9jRreW9R3aOrIVr9vxhCWFyrMe7BFSks= -github.com/grafana/grafana-plugin-sdk-go v0.265.0/go.mod h1:nkN6xI08YcX6CGsgvRA2+19nhXA/ZPuneLMUUElOD80= +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/pkg/aggregator v0.0.0-20250121113133-e747350fee2d h1:aBD5kzsIAh50vjNqUkWK9mNpLGIBYAnKkWtUepGNAiQ= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d/go.mod h1:1sq0guad+G4SUTlBgx7SXfhnzy7D86K/LcVOtiQCiMA= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= @@ -1273,8 +1273,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1359,8 +1359,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 3e2c7757443..f1e7e7cc290 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-20250120145936-5f0e28e7a87c 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.265.0 + github.com/grafana/grafana-plugin-sdk-go v0.266.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 @@ -221,8 +221,8 @@ require ( golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.9.0 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 4569d6883a7..75ec44cf992 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -413,8 +413,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.265.0 h1:XshoH8R23Jm9jRreW9R3aOrIVr9vxhCWFyrMe7BFSks= -github.com/grafana/grafana-plugin-sdk-go v0.265.0/go.mod h1:nkN6xI08YcX6CGsgvRA2+19nhXA/ZPuneLMUUElOD80= +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/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= @@ -872,8 +872,8 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -922,8 +922,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= From 861686adaa7250bcf86302c5d3fd86e576135c8e Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 14 Feb 2025 15:34:44 +0000 Subject: [PATCH 618/894] Combobox: Tighten up storybook documentation (#100313) * Add JSDoc comments to more props * Remove in-dev decorator * reword MDX documentation and add migration guide --- .../src/components/Combobox/Combobox.mdx | 105 ++++++++++++------ .../components/Combobox/Combobox.story.tsx | 18 +-- .../src/components/Combobox/Combobox.tsx | 28 ++++- 3 files changed, 94 insertions(+), 57 deletions(-) diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.mdx b/packages/grafana-ui/src/components/Combobox/Combobox.mdx index 7c93072b57a..b4b5e8eba17 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.mdx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.mdx @@ -4,53 +4,66 @@ import { Combobox } from './Combobox'; -## Usage +# Combobox -**Do** +A performant and accessible combobox component that supports both synchronous and asynchronous options loading. It provides type-ahead filtering, keyboard navigation, and virtual scrolling for handling large datasets efficiently. -- Use in inline query editors -- Use when you require async calls from a select input +**Use Combobox when you need:** -**Don't** +- A searchable dropdown with keyboard navigation +- Asynchronous loading of options (e.g., API calls) +- Support for large datasets (1000+ items) +- Type-ahead filtering functionality +- Custom value creation +- Inline form usage (e.g., query editors) -- Use the async functionality, when all items are only loaded on the initial load -- Use when fewer than 4 items are needed, as a `RadioButtonGroup` may be more suitable (not for inline use cases) -- Use this component if you need custom option styling +**Consider alternatives when:** -## ComboboxOption +- You have fewer than 4 options (consider `RadioButtonGroup` instead) +- You need complex custom option styling -The `ComboboxOption` currently supports 3 properties: +## Usage & Guidelines -- `label` - The text that is visible in the menu. -- `value` (required) - The value that is selected. -- `description` - A longer description that describes the choice. +### Options -If no `label` is given, `value` will be used as a display value. +Options are supplied through the `options` prop as either: -## Sizing +- An array of options for synchronous usage +- An async function that returns a promise resolving to options for user input. -The recommended way to set the width is by sizing the container element. This is so it may reflect a similar size as other inputs in the context. +Options can be an array of objects with seperate label and values, or an array of strings which will be used as both the label and value. -If that is not possible, the width can be set directly on the component, by setting a number, which is a multiple of `8px`. +While Combobox can handle large sets of options, you should consider both the user experience of searching through many options, and the performance of loading many options from an API. -For inline usage, such as in query editors, it may be useful to size the input based on the content. Set `width="auto"` to achieve this. In this case, it is also recommended to set `maxWidth` and `minWidth`. +### Async behaviour -## Async Usage +When using Combobox with options from a remote source as the user types, you can supply the `options` prop as an function that is called on each keypress with the current input value and returns a promise resolving to an array of options matching the input. -The `options` prop can accept an async function: +Consider the following when implementing async behaviour: -- When the menu opens, the `options` function is called with `''`, to load all options. -- When the user types, the `options` function is called with the current input value. +- Consumers should return filtered options matching the input. This is bested suited for APIs that support filtering/search. +- When the menu is opened with blank input (e.g. initial click with no selected value) the function will be called with an empty string. +- Consumers should only ever load top-n options from APIs using this async function. If your API does not support filtering, consider loading options yourself and just passing the sync options array in +- Combobox does not cache calls to the async function. If you need this, implement your own caching. +- Calls to the async function are debounced, so consumers should not need to implement this themselves. -Note: The calls are debounced. Old calls are invalidated when a new call is made. +### Value -## Unit testing +The `value` prop is used to set the selected value of the combobox. A scalar value (the value of options) is preferred over a full option object. -Writing unit tests with Combobox requires mocking the `getBoundingClientRect` method because of [the virtual list library](https://github.com/TanStack/virtual/issues/29#issuecomment-657519522) +When using async options with seperate labels and values, the `value` prop can be a full option object to ensure the correct label is displayed. -This code sets up the mocking before all tests: +### Sizing -```js +Combobox defaults to filling the width of its container to match other inputs. If that's not desired, set the `width` prop to control the exact input width. + +For inline usage, such as in query editors, it may be useful to size the input based on the text content. Set width="auto" to achieve this. In this case, it is also recommended to set maxWidth and minWidth. + +### Unit tests + +The component requires mocking `getBoundingClientRect` because of virtualisation: + +```typescript beforeAll(() => { const mockGetBoundingClientRect = jest.fn(() => ({ width: 120, @@ -67,13 +80,9 @@ beforeAll(() => { }); ``` -### Selecting an option +#### Select an option by mouse -To select an option, you can use any `*ByRole` methods, as Combobox has proper roles for accessibility. - -#### Selecting option by clicking - -```js +```jsx render(); const input = screen.getByRole('combobox'); @@ -84,9 +93,9 @@ await userEvent.click(item); expect(screen.getByDisplayValue('Option 1')).toBeInTheDocument(); ``` -#### Selecting option by typing +#### Select an option by keyboard -```js +```jsx render(); const input = screen.getByRole('combobox'); @@ -96,6 +105,32 @@ await userEvent.keyboard('{ArrowDown}{Enter}'); expect(screen.getByDisplayValue('Option 3')).toBeInTheDocument(); ``` +## Migrating from Select + +Combobox's API is similar to Select, but is greatly simplified. Any workarounds you may have implemented to workaround Select's slow performance are no longer necessary. + +Some differences to note: + +- Virtualisation is built in, so no separate `VirtualizedSelect` component is needed. +- Async behaviour is built in so a seperate `AsyncSelect` component is not needed +- `isLoading: boolean` has been renamed to `loading: boolean` +- `allowCustomValue` has been renamed to `createCustomValue`. +- When specifying `width="auto"`, `minWidth` is also required. +- Groups are not supported at this time. +- Many props used to control subtle behaviour have been removed to simplify the API and improve performance. + - Custom render props, or label as ReactNode is not supported at this time. Reach out if you have a hard requirement for this and we can discuss. + +For all async behaviour, pass in a function that returns `Promise` that will be called when the menu is opened, and on keypress. + +```tsx +const loadOptions = useCallback(async (input: string) => { + const response = await fetch(`/api/options?query=${input}`); + return response.json(); +}, []); + +; +``` + ## Props diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx index 5575d21df0e..fe3c9bca3f5 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx @@ -1,9 +1,8 @@ import { action } from '@storybook/addon-actions'; import { useArgs } from '@storybook/preview-api'; import { Meta, StoryFn, StoryObj } from '@storybook/react'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; -import { Alert } from '../Alert/Alert'; import { Field } from '../Forms/Field'; import { Combobox, ComboboxProps } from './Combobox'; @@ -64,7 +63,6 @@ const meta: Meta = { ], value: 'banana', }, - decorators: [InDevDecorator], }; export default meta; @@ -257,17 +255,3 @@ export const PositioningTest: Story = { ); }, }; - -function InDevDecorator(Story: React.ElementType) { - return ( -
          - - Combobox is still in development and not able to be used externally. -
          - Within the Grafana repo, it can be used by importing it from{' '} - @grafana/ui/src/unstable -
          - -
          - ); -} diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index 62f231975e3..f69d15b3038 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -35,17 +35,32 @@ export interface ComboboxBaseProps * Allows the user to set a value which is not in the list of options. */ createCustomValue?: boolean; - options: Array> | ((inputValue: string) => Promise>>); - onChange: (option: ComboboxOption) => void; + /** - * Most consumers should pass value in as a scalar string | number. However, sometimes with Async because we don't - * have the full options loaded to match the value to, consumers may also pass in an Option with a label to display. + * An array of options, or a function that returns a promise resolving to an array of options. + * If a function, it will be called when the menu is opened and on keypress with the current search query. + */ + options: Array> | ((inputValue: string) => Promise>>); + + /** + * onChange handler is called with the newly selected option. + */ + onChange: (option: ComboboxOption) => void; + + /** + * Current selected value. Most consumers should pass a scalar value (string | number). However, sometimes with Async + * it may be better to pass in an Option with a label to display. */ value?: T | ComboboxOption | null; + /** - * Defaults to 100%. Number is a multiple of 8px. 'auto' will size the input to the content. + * Defaults to full width of container. Number is a multiple of the spacing unit. 'auto' will size the input to the content. * */ width?: number | 'auto'; + + /** + * Called when the input loses focus. + */ onBlur?: () => void; } @@ -53,6 +68,9 @@ const RECOMMENDED_ITEMS_AMOUNT = 100_000; type ClearableConditionals = | { + /** + * Allow the user to clear the selected value. `null` is emitted from the onChange handler + */ isClearable: true; /** * The onChange handler is called with `null` when clearing the Combobox. From d0394bfa7ef3b3fcffd0eb39f780c1c1ba342308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Fri, 14 Feb 2025 16:39:48 +0100 Subject: [PATCH 619/894] Extract NewSearchOptions from unified sql client setup. (#100719) * Extract NewSearchOptions from unified sql client setup. Co-authored-by: Georges Chaudy --- pkg/storage/unified/client.go | 7 +++- pkg/storage/unified/search/options.go | 54 +++++++++++++++++++++++++++ pkg/storage/unified/sql/server.go | 50 ++----------------------- pkg/storage/unified/sql/service.go | 8 +++- 4 files changed, 71 insertions(+), 48 deletions(-) create mode 100644 pkg/storage/unified/search/options.go diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index 50f144e39ab..54bf7a888b6 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified/federated" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/storage/unified/sql" ) @@ -121,7 +122,11 @@ func newClient(opts options.StorageOptions, // Use the local SQL default: - server, err := sql.NewResourceServer(ctx, db, cfg, features, docs, tracer, reg, authzc) + searchOptions, err := search.NewSearchOptions(features, cfg, tracer, docs, reg) + if err != nil { + return nil, err + } + server, err := sql.NewResourceServer(db, cfg, tracer, reg, authzc, searchOptions) if err != nil { return nil, err } diff --git a/pkg/storage/unified/search/options.go b/pkg/storage/unified/search/options.go new file mode 100644 index 00000000000..13a2612319d --- /dev/null +++ b/pkg/storage/unified/search/options.go @@ -0,0 +1,54 @@ +package search + +import ( + "log/slog" + "os" + "path/filepath" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +func NewSearchOptions(features featuremgmt.FeatureToggles, cfg *setting.Cfg, tracer tracing.Tracer, docs resource.DocumentBuilderSupplier, reg prometheus.Registerer) (resource.SearchOptions, error) { + // Setup the search server + if features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) { + root := cfg.IndexPath + if root == "" { + root = filepath.Join(cfg.DataPath, "unified-search", "bleve") + } + err := os.MkdirAll(root, 0750) + if err != nil { + return resource.SearchOptions{}, err + } + bleve, err := NewBleveBackend(BleveOptions{ + Root: root, + FileThreshold: int64(cfg.IndexFileThreshold), // fewer than X items will use a memory index + BatchSize: cfg.IndexMaxBatchSize, // This is the batch size for how many objects to add to the index at once + }, tracer, features) + + if err != nil { + return resource.SearchOptions{}, err + } + + err = reg.Register(resource.NewIndexMetrics(cfg.IndexPath, bleve)) + if err != nil { + slog.Warn("Failed to register indexer metrics", "error", err) + } + err = reg.Register(resource.NewSprinklesMetrics()) + if err != nil { + slog.Warn("Failed to register sprinkles metrics", "error", err) + } + + return resource.SearchOptions{ + Backend: bleve, + Resources: docs, + WorkerThreads: cfg.IndexWorkers, + InitMinCount: cfg.IndexMinCount, + }, nil + } + return resource.SearchOptions{}, nil +} diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index b2cade286f5..9fc287b2f73 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -1,28 +1,23 @@ package sql import ( - "context" - "log/slog" "os" - "path/filepath" "strings" "github.com/prometheus/client_golang/prometheus" "github.com/grafana/authlib/types" + infraDB "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" ) // Creates a new ResourceServer -func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg, - features featuremgmt.FeatureToggles, docs resource.DocumentBuilderSupplier, - tracer tracing.Tracer, reg prometheus.Registerer, ac types.AccessClient) (resource.ResourceServer, error) { +func NewResourceServer(db infraDB.DB, cfg *setting.Cfg, + tracer tracing.Tracer, reg prometheus.Registerer, ac types.AccessClient, searchOptions resource.SearchOptions) (resource.ResourceServer, error) { apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver") opts := resource.ResourceServerOptions{ Tracer: tracer, @@ -55,44 +50,7 @@ func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg, opts.Backend = store opts.Diagnostics = store opts.Lifecycle = store - - // Setup the search server - if features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) { - root := cfg.IndexPath - if root == "" { - root = filepath.Join(cfg.DataPath, "unified-search", "bleve") - } - err = os.MkdirAll(root, 0750) - if err != nil { - return nil, err - } - bleve, err := search.NewBleveBackend(search.BleveOptions{ - Root: root, - FileThreshold: int64(cfg.IndexFileThreshold), // fewer than X items will use a memory index - BatchSize: cfg.IndexMaxBatchSize, // This is the batch size for how many objects to add to the index at once - }, tracer, features) - - if err != nil { - return nil, err - } - - opts.Search = resource.SearchOptions{ - Backend: bleve, - Resources: docs, - WorkerThreads: cfg.IndexWorkers, - InitMinCount: cfg.IndexMinCount, - } - - // Register indexer metrics - err = reg.Register(resource.NewIndexMetrics(cfg.IndexPath, opts.Search.Backend)) - if err != nil { - slog.Warn("Failed to register indexer metrics", "error", err) - } - err = reg.Register(resource.NewSprinklesMetrics()) - if err != nil { - slog.Warn("Failed to register sprinkles metrics", "error", err) - } - } + opts.Search = searchOptions rs, err := resource.NewResourceServer(opts) if err != nil { diff --git a/pkg/storage/unified/sql/service.go b/pkg/storage/unified/sql/service.go index 6ae039d1c7f..4100ef1e1a6 100644 --- a/pkg/storage/unified/sql/service.go +++ b/pkg/storage/unified/sql/service.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resource/grpc" + "github.com/grafana/grafana/pkg/storage/unified/search" ) var ( @@ -110,7 +111,12 @@ func (s *service) start(ctx context.Context) error { return err } - server, err := NewResourceServer(ctx, s.db, s.cfg, s.features, s.docBuilders, s.tracing, s.reg, authzClient) + searchOptions, err := search.NewSearchOptions(s.features, s.cfg, s.tracing, s.docBuilders, s.reg) + if err != nil { + return err + } + + server, err := NewResourceServer(s.db, s.cfg, s.tracing, s.reg, authzClient, searchOptions) if err != nil { return err } From 6bd1041cda9b197daf699dfd519cddd5d005f513 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Fri, 14 Feb 2025 17:42:10 +0200 Subject: [PATCH 620/894] Dashboard: Fix panel edits for repeats (#100658) --- e2e/various-suite/solo-route.spec.ts | 4 +- .../scene/DashboardDatasourceBehaviour.tsx | 5 +- .../scene/DashboardSceneUrlSync.ts | 4 +- .../layout-default/RowRepeaterBehavior.ts | 9 +- .../features/dashboard-scene/utils/utils.ts | 105 +++++++++++++++++- .../datasource/dashboard/datasource.ts | 5 +- 6 files changed, 116 insertions(+), 16 deletions(-) diff --git a/e2e/various-suite/solo-route.spec.ts b/e2e/various-suite/solo-route.spec.ts index c7f90f8cd36..415257ead7c 100644 --- a/e2e/various-suite/solo-route.spec.ts +++ b/e2e/various-suite/solo-route.spec.ts @@ -22,7 +22,7 @@ describe('Solo Route', () => { cy.contains('uplot-main-div').should('not.exist'); }); - /*it('Can view solo repeated panel in scenes', () => { + it('Can view solo repeated panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' @@ -30,7 +30,7 @@ describe('Solo Route', () => { e2e.components.Panels.Panel.title('server=A').should('exist'); cy.contains('uplot-main-div').should('not.exist'); - });*/ + }); it('Can view solo in repeated row and panel in scenes', () => { // open Panel Tests - Graph NG diff --git a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx index 940b4077d8d..a35663eeaac 100644 --- a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx @@ -5,7 +5,7 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constan import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { - findVizPanelByKey, + findOriginalVizPanelByKey, getDashboardSceneFor, getLibraryPanelBehavior, getQueryRunnerFor, @@ -53,7 +53,8 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase child.state.key!.includes(rowKey)); + const index = allChildren.findIndex( + (child) => child instanceof SceneGridRow && getOriginalKey(child.state.key!) === getOriginalKey(rowKey) + ); if (index === -1) { throw new Error('RowRepeaterBehavior: Parent row not found in layout children'); @@ -286,7 +289,9 @@ function updateLayout(layout: SceneGridLayout, rows: SceneGridRow[], maxYOfRows: } function getLayoutChildrenFilterOutRepeatClones(layout: SceneGridLayout, rowKey: string) { - return layout.state.children.filter((child) => !isClonedKeyOf(child.state.key!, rowKey)); + return layout.state.children.filter( + (child) => !(child instanceof SceneGridRow) || !isClonedKeyOf(getLastKeyFromClone(child.state.key!), rowKey) + ); } function ensureUniqueKeys(item: SceneGridItemLike, ancestors: string) { diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 5e52a1d517f..60768226dfc 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -21,7 +21,7 @@ import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; -import { getOriginalKey, isClonedKey } from './clone'; +import { containsCloneKey, getLastKeyFromClone, getOriginalKey, isInCloneChain } from './clone'; export const NEW_PANEL_HEIGHT = 8; export const NEW_PANEL_WIDTH = 12; @@ -68,12 +68,63 @@ function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizP return true; } - // It might be possible to have the keys changed in the meantime from `panel-2` to `panel-2-clone-0` - // We need to check this as well - const originalObjectKey = !isClonedKey(objKey) ? getOriginalKey(objKey) : objKey; - const originalKey = !isClonedKey(key) ? getOriginalKey(key) : key; + if (!(obj instanceof VizPanel)) { + return false; + } - if (originalObjectKey === originalKey) { + return false; + }); + + if (panel) { + if (panel instanceof VizPanel) { + return panel; + } else { + throw new Error(`Found panel with key ${key} but it was not a VizPanel`); + } + } + + return null; +} + +export function findOriginalVizPanelByKey(scene: SceneObject, key: string | undefined): VizPanel | null { + if (!key) { + return null; + } + + let panel: VizPanel | null = findOriginalVizPanelInternal(scene, key); + + if (panel) { + return panel; + } + + // Also try to find by panel id + const id = parseInt(key, 10); + if (isNaN(id)) { + return null; + } + + const panelId = getVizPanelKeyForPanelId(id); + panel = findVizPanelInternal(scene, panelId); + + if (panel) { + return panel; + } + + panel = findOriginalVizPanelInternal(scene, panelId); + + return panel; +} + +function findOriginalVizPanelInternal(scene: SceneObject, key: string | undefined): VizPanel | null { + if (!key) { + return null; + } + + const panel = sceneGraph.findObject(scene, (obj) => { + const objKey = obj.state.key!; + + // Compare the original keys + if (objKey === key || (!isInCloneChain(objKey) && getOriginalKey(objKey) === getOriginalKey(key))) { return true; } @@ -95,6 +146,48 @@ function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizP return null; } +export function findEditPanel(scene: SceneObject, key: string | undefined): VizPanel | null { + if (!key) { + return null; + } + + // First we try to find the non-cloned panel + // This means it is either in not in a repeat chain or every item in the chain is not a clone + let panel: SceneObject | null = findOriginalVizPanelByKey(scene, key); + if (!panel || !panel.state.key) { + return null; + } + + // Get the actual panel key, without any of the ancestors + const panelKey = getLastKeyFromClone(panel.state.key); + + // If the panel contains clone in the key, this means it's a repeated panel, and we need to find the original panel + if (containsCloneKey(panelKey)) { + // Get the original key of the panel that we are looking for + const originalPanelKey = getOriginalKey(panelKey); + // Start the search from the parent to avoid unnecessary checks + // The parent usually is the grid item where the referenced panel is also located + panel = sceneGraph.findObject(panel.parent ?? scene, (sceneObject) => { + if (!sceneObject.state.key || isInCloneChain(sceneObject.state.key)) { + return false; + } + + const currentLastKey = getLastKeyFromClone(sceneObject.state.key); + if (containsCloneKey(currentLastKey)) { + return false; + } + + return getOriginalKey(currentLastKey) === originalPanelKey; + }); + } + + if (!(panel instanceof VizPanel)) { + return null; + } + + return panel; +} + /** * Force re-render children. This is useful in some edge case scenarios when * children deep down the scene graph needs to be re-rendered when some parent state change. diff --git a/public/app/plugins/datasource/dashboard/datasource.ts b/public/app/plugins/datasource/dashboard/datasource.ts index b80fe5594a2..16fda375e6e 100644 --- a/public/app/plugins/datasource/dashboard/datasource.ts +++ b/public/app/plugins/datasource/dashboard/datasource.ts @@ -15,7 +15,7 @@ import { import { SceneDataProvider, SceneDataTransformer, SceneObject } from '@grafana/scenes'; import { activateSceneObjectAndParentTree, - findVizPanelByKey, + findOriginalVizPanelByKey, getVizPanelKeyForPanelId, } from 'app/features/dashboard-scene/utils/utils'; @@ -109,7 +109,8 @@ export class DashboardDatasource extends DataSourceApi { } private findSourcePanel(scene: SceneObject, panelId: number) { - return findVizPanelByKey(scene, getVizPanelKeyForPanelId(panelId)); + // We're trying to find the original panel, not a cloned one, since `panelId` alone cannot resolve clones + return findOriginalVizPanelByKey(scene, getVizPanelKeyForPanelId(panelId)); } private emitFirstLoadedDataIfMixedDS( From e343cb5ac914c4596851084f83fc6e193db032cc Mon Sep 17 00:00:00 2001 From: Tito Lins Date: Fri, 14 Feb 2025 16:55:20 +0100 Subject: [PATCH 621/894] Alerting: Stop running AM integration tests on CI (#100702) --- .github/pr-commands.json | 3 ++- pkg/tests/alertmanager/alertmanager_test.go | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/pr-commands.json b/.github/pr-commands.json index 1803aa29a8a..d211b5cce3a 100644 --- a/.github/pr-commands.json +++ b/.github/pr-commands.json @@ -247,7 +247,8 @@ "/pkg/services/sqlstore/migrations/ualert/**/*", "/pkg/services/alerting/**/*", "/public/app/features/alerting/**/*", - "/pkg/tests/api/alerting/**/*" + "/pkg/tests/api/alerting/**/*", + "/pkg/tests/alertmanager/**/*" ], "action": "updateLabel", "addLabel": "area/alerting" diff --git a/pkg/tests/alertmanager/alertmanager_test.go b/pkg/tests/alertmanager/alertmanager_test.go index 269e53075e2..e3a001d7657 100644 --- a/pkg/tests/alertmanager/alertmanager_test.go +++ b/pkg/tests/alertmanager/alertmanager_test.go @@ -7,13 +7,12 @@ import ( "github.com/stretchr/testify/require" ) -func TestAlertmanagerIntegration_ExtraDedupStage(t *testing.T) { +func TestAlertmanager_ExtraDedupStage(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } t.Run("assert no flapping alerts when stopOnExtraDedup is enabled", func(t *testing.T) { - t.Skip("skipping flaky test") s, err := NewAlertmanagerScenario() require.NoError(t, err) defer s.Close() From cbae35c28bac357be4472726e37f0dd8dccef412 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 14 Feb 2025 16:56:14 +0100 Subject: [PATCH 622/894] Alerting: Delete protobuf alert rule state on alert rule deletion (#100736) --- pkg/services/ngalert/store/alert_rule.go | 7 +++ pkg/services/ngalert/store/alert_rule_test.go | 47 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 77d432ddc78..d688d6bb53d 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -68,6 +68,13 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI return err } logger.Debug("Deleted alert instances", "count", rows) + + rows, err = sess.Table("alert_rule_state").Where("org_id = ?", orgID).In("rule_uid", ruleUID).Delete(alertRule{}) + if err != nil { + return err + } + logger.Debug("Deleted alert rule state", "count", rows) + return nil }) } diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index e5d5e77f483..79b8f704681 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -715,13 +715,22 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) { sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) - b := &fakeBus{} logger := log.New("test-dbstore") - store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, &fakeBus{}) + protoInstanceStore := ProtoInstanceDBStore{ + SQLStore: sqlStore, + Logger: logger, + FeatureToggles: featuremgmt.WithFeatures(), + } gen := models.RuleGen t.Run("should emit event when rules are deleted", func(t *testing.T) { + // Create a new store to pass the custom bus to check the signal + b := &fakeBus{} + logger := log.New("test-dbstore") + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) + rule := createRule(t, store, gen) called := false b.publishFn = func(ctx context.Context, msg bus.Msg) error { @@ -737,6 +746,40 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) { require.NoError(t, err) require.True(t, called) }) + + t.Run("should delete alert rule state", func(t *testing.T) { + rule := createRule(t, store, gen) + + // Save state for the alert rule + instances := []models.AlertInstance{ + { + AlertInstanceKey: models.AlertInstanceKey{ + RuleUID: rule.UID, + RuleOrgID: rule.OrgID, + }, + }, + } + err := protoInstanceStore.SaveAlertInstancesForRule(context.Background(), rule.GetKeyWithGroup(), instances) + require.NoError(t, err) + savedInstances, err := protoInstanceStore.ListAlertInstances(context.Background(), &models.ListAlertInstancesQuery{ + RuleUID: rule.UID, + RuleOrgID: rule.OrgID, + }) + require.NoError(t, err) + require.Len(t, savedInstances, 1) + + // Delete the rule + err = store.DeleteAlertRulesByUID(context.Background(), rule.OrgID, rule.UID) + require.NoError(t, err) + + // Now there should be no alert rule state + savedInstances, err = protoInstanceStore.ListAlertInstances(context.Background(), &models.ListAlertInstancesQuery{ + RuleUID: rule.UID, + RuleOrgID: rule.OrgID, + }) + require.NoError(t, err) + require.Empty(t, savedInstances) + }) } func TestIntegration_GetNamespaceByUID(t *testing.T) { From dc5602bad91128cee18e3f50e1fda21b8130db1f Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Fri, 14 Feb 2025 17:57:28 +0200 Subject: [PATCH 623/894] SSO: Fix team_ids validation for Generic OAuth (#100732) fix team_ids validation in the API --- pkg/login/social/connectors/generic_oauth.go | 3 +- .../social/connectors/generic_oauth_test.go | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/pkg/login/social/connectors/generic_oauth.go b/pkg/login/social/connectors/generic_oauth.go index 15989c0df93..df530313c81 100644 --- a/pkg/login/social/connectors/generic_oauth.go +++ b/pkg/login/social/connectors/generic_oauth.go @@ -111,7 +111,8 @@ func (s *SocialGenericOAuth) Validate(ctx context.Context, newSettings ssoModels return err } - if info.Extra[teamIdsKey] != "" && (info.TeamIdsAttributePath == "" || info.TeamsUrl == "") { + teamIds := util.SplitString(info.Extra[teamIdsKey]) + if len(teamIds) > 0 && (info.TeamIdsAttributePath == "" || info.TeamsUrl == "") { return ssosettings.ErrInvalidOAuthConfig("If Team Ids are configured then Team Ids attribute path and Teams URL must be configured.") } diff --git a/pkg/login/social/connectors/generic_oauth_test.go b/pkg/login/social/connectors/generic_oauth_test.go index 36f9d13fd91..f61b06b0a98 100644 --- a/pkg/login/social/connectors/generic_oauth_test.go +++ b/pkg/login/social/connectors/generic_oauth_test.go @@ -1000,6 +1000,34 @@ func TestSocialGenericOAuth_Validate(t *testing.T) { }, wantErr: nil, }, + { + name: "passes when team_ids is an empty array and teams_id_attribute_path and teams_url are empty", + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "team_ids_attribute_path": "", + "teams_url": "", + "auth_url": "https://example.com/auth", + "token_url": "https://example.com/token", + "team_ids": "[]", + }, + }, + wantErr: nil, + }, + { + name: "passes when team_ids is set and teams_id_attribute_path and teams_url are not empty", + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "team_ids_attribute_path": "teams", + "teams_url": "https://example.com/teams", + "auth_url": "https://example.com/auth", + "token_url": "https://example.com/token", + "team_ids": "[\"123\"]", + }, + }, + wantErr: nil, + }, { name: "fails if settings map contains an invalid field", settings: ssoModels.SSOSettings{ @@ -1116,6 +1144,34 @@ func TestSocialGenericOAuth_Validate(t *testing.T) { }, wantErr: ssosettings.ErrBaseInvalidOAuthConfig, }, + { + name: "fails when team_ids is a valid string and teams_id_attribute_path and teams_url are empty", + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "team_ids_attribute_path": "", + "teams_url": "", + "auth_url": "https://example.com/auth", + "token_url": "https://example.com/token", + "team_ids": "123", + }, + }, + wantErr: ssosettings.ErrBaseInvalidOAuthConfig, + }, + { + name: "fails when team_ids is a valid array and teams_id_attribute_path and teams_url are empty", + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "team_ids_attribute_path": "", + "teams_url": "", + "auth_url": "https://example.com/auth", + "token_url": "https://example.com/token", + "team_ids": "[\"123\",\"456\",\"789\"]", + }, + }, + wantErr: ssosettings.ErrBaseInvalidOAuthConfig, + }, } for _, tc := range testCases { From 589340e03c4263078517772f9733fcab69f9b4d2 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 14 Feb 2025 16:07:26 +0000 Subject: [PATCH 624/894] GrafanaUI: Deprecate Select in favor of Combobox (#100294) * GrafanaUI: Deprecate Select * add deprecated decorator to stories * tweak message --- .../src/components/Select/Select.story.tsx | 19 ++++++++++++++++++- .../src/components/Select/Select.tsx | 4 ++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Select/Select.story.tsx b/packages/grafana-ui/src/components/Select/Select.story.tsx index e5fc496af71..1a6aea76ea7 100644 --- a/packages/grafana-ui/src/components/Select/Select.story.tsx +++ b/packages/grafana-ui/src/components/Select/Select.story.tsx @@ -7,6 +7,7 @@ import { useState } from 'react'; import { SelectableValue, toIconName } from '@grafana/data'; import { getAvailableIcons } from '../../types'; +import { Alert } from '../Alert/Alert'; import { Icon } from '../Icon/Icon'; import { AsyncMultiSelect, AsyncSelect, MultiSelect, Select } from './Select'; @@ -92,6 +93,7 @@ const meta: Meta = { }, }, }, + decorators: [DeprecatedDecorator], }; const loadAsyncOptions = () => { @@ -383,7 +385,7 @@ export const AutoMenuPlacement: StoryFn = (args) => { return ( <> -
          +
          - ); - } + return ( + + ); }; } diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.tsx index 3202d5e37eb..0dd2c09ff92 100644 --- a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.tsx @@ -1,59 +1,26 @@ import { useCallback, useState } from 'react'; import * as React from 'react'; -import { ValueMatcherID, RangeValueMatcherOptions, VariableOrigin } from '@grafana/data'; -import { getTemplateSrv, config as cfg } from '@grafana/runtime'; -import { InlineLabel, Input } from '@grafana/ui'; +import { ValueMatcherID, RangeValueMatcherOptions } from '@grafana/data'; +import { InlineLabel } from '@grafana/ui'; import { SuggestionsInput } from '../../suggestionsInput/SuggestionsInput'; -import { numberOrVariableValidator } from '../../utils'; +import { getVariableSuggestions, numberOrVariableValidator } from '../../utils'; import { ValueMatcherEditorConfig, ValueMatcherUIProps, ValueMatcherUIRegistryItem } from './types'; -import { convertToType } from './utils'; type PropNames = 'from' | 'to'; export function rangeMatcherEditor( config: ValueMatcherEditorConfig ): React.FC>> { - return function RangeMatcherEditor({ options, onChange, field }) { + return function RangeMatcherEditor({ options, onChange }) { const { validator } = config; const [isInvalid, setInvalid] = useState({ from: !validator(options.from), to: !validator(options.to), }); - const templateSrv = getTemplateSrv(); - const variables = templateSrv.getVariables().map((v) => { - return { value: v.name, label: v.label || v.name, origin: VariableOrigin.Template }; - }); - - const onChangeValue = useCallback( - (event: React.FormEvent, prop: PropNames) => { - setInvalid({ - ...isInvalid, - [prop]: !validator(event.currentTarget.value), - }); - }, - [setInvalid, validator, isInvalid] - ); - - const onChangeOptions = useCallback( - (event: React.FocusEvent, prop: PropNames) => { - if (isInvalid[prop]) { - return; - } - - const { value } = event.currentTarget; - - onChange({ - ...options, - [prop]: convertToType(value, field), - }); - }, - [options, onChange, isInvalid, field] - ); - const onChangeOptionsSuggestions = useCallback( (value: string, prop: PropNames) => { const invalid = !validator(value); @@ -74,45 +41,27 @@ export function rangeMatcherEditor( }, [options, onChange, isInvalid, setInvalid, validator] ); - if (cfg.featureToggles.transformationsVariableSupport) { - return ( - <> - onChangeOptionsSuggestions(val, 'from')} - suggestions={variables} - /> - and - onChangeOptionsSuggestions(val, 'to')} - /> - - ); - } + + const suggestions = getVariableSuggestions(); + return ( <> - onChangeValue(event, 'from')} - onBlur={(event) => onChangeOptions(event, 'from')} + onChange={(val) => onChangeOptionsSuggestions(val, 'from')} + suggestions={suggestions} /> and - onChangeValue(event, 'to')} - onBlur={(event) => onChangeOptions(event, 'to')} + suggestions={suggestions} + onChange={(val) => onChangeOptionsSuggestions(val, 'to')} /> ); diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx index b99f246b358..451bcf65999 100644 --- a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx @@ -1,35 +1,21 @@ import { useCallback, useState } from 'react'; import * as React from 'react'; -import { ValueMatcherID, BasicValueMatcherOptions, VariableOrigin } from '@grafana/data'; -import { config as cfg, getTemplateSrv } from '@grafana/runtime'; -import { Input } from '@grafana/ui'; +import { ValueMatcherID, BasicValueMatcherOptions } from '@grafana/data'; import { SuggestionsInput } from '../../suggestionsInput/SuggestionsInput'; +import { getVariableSuggestions } from '../../utils'; import { ValueMatcherEditorConfig, ValueMatcherUIProps, ValueMatcherUIRegistryItem } from './types'; -import { convertToType } from './utils'; export function regexMatcherEditor( config: ValueMatcherEditorConfig ): React.FC>> { - return function Render({ options, onChange, field }) { - const { validator, converter = convertToType } = config; + return function Render({ options, onChange }) { + const { validator } = config; const { value } = options; const [isInvalid, setInvalid] = useState(!validator(value)); - const templateSrv = getTemplateSrv(); - const variables = templateSrv.getVariables().map((v) => { - return { value: v.name, label: v.label || v.name, origin: VariableOrigin.Template }; - }); - - const onChangeValue = useCallback( - (event: React.FormEvent) => { - setInvalid(!validator(event.currentTarget.value)); - }, - [setInvalid, validator] - ); - const onChangeVariableValue = useCallback( (value: string) => { setInvalid(!validator(value)); @@ -41,42 +27,13 @@ export function regexMatcherEditor( [setInvalid, validator, onChange, options] ); - const onChangeOptions = useCallback( - (event: React.FocusEvent) => { - if (isInvalid) { - return; - } - - const { value } = event.currentTarget; - - onChange({ - ...options, - value: converter(value, field), - }); - }, - [options, onChange, isInvalid, field, converter] - ); - - if (cfg.featureToggles.transformationsVariableSupport) { - return ( - - ); - } - return ( - ); }; @@ -89,7 +46,6 @@ export const getRegexValueMatchersUI = (): Array true, - converter: (value) => String(value), }), }, ]; diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index 0b086eb0907..6a7a29ce69c 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -16,7 +16,6 @@ import { TimeRange, } from '@grafana/data'; import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames'; -import { config } from '@grafana/runtime'; import { ScaleDistribution, HeatmapCellLayout, @@ -52,29 +51,7 @@ export const heatmapTransformer: SynchronousDataTransformerInfo (source) => - source.pipe( - map((data) => { - if (config.featureToggles.transformationsVariableSupport) { - const optionsCopy = { - ...options, - xBuckets: { ...options.xBuckets }, - yBuckets: { ...options.yBuckets }, - }; - - if (optionsCopy.xBuckets?.value) { - optionsCopy.xBuckets.value = ctx.interpolate(optionsCopy.xBuckets.value); - } - - if (optionsCopy.yBuckets?.value) { - optionsCopy.yBuckets.value = ctx.interpolate(optionsCopy.yBuckets.value); - } - - return heatmapTransformer.transformer(optionsCopy, ctx)(data); - } else { - return heatmapTransformer.transformer(options, ctx)(data); - } - }) - ), + source.pipe(map((data) => heatmapTransformer.transformer(options, ctx)(data))), transformer: (options: HeatmapTransformerOptions) => { return (data: DataFrame[]) => { diff --git a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx index 9fb3537abd8..bc39a4e2c62 100644 --- a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx @@ -1,6 +1,6 @@ import { ChangeEvent, useEffect, useState } from 'react'; import * as React from 'react'; -import { identity, of, OperatorFunction } from 'rxjs'; +import { of, OperatorFunction } from 'rxjs'; import { map } from 'rxjs/operators'; import { @@ -81,10 +81,8 @@ export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformer }, [input, configuredOptions]); const getVariableNames = (): OperatorFunction => { - if (!cfg.featureToggles.transformationsVariableSupport) { - return identity; - } const templateSrv = getTemplateSrv(); + return (source) => source.pipe( map((input) => { diff --git a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx index 696f3458d59..a257f02b0af 100644 --- a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx +++ b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx @@ -6,19 +6,16 @@ import { TransformerRegistryItem, TransformerUIProps, TransformerCategory, - VariableOrigin, } from '@grafana/data'; import { histogramFieldInfo, HistogramTransformerInputs, } from '@grafana/data/src/transformations/transformers/histogram'; -import { getTemplateSrv, config as cfg } from '@grafana/runtime'; import { InlineField, InlineFieldRow, InlineSwitch } from '@grafana/ui'; -import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; import { getTransformationContent } from '../docs/getTransformationContent'; import { SuggestionsInput } from '../suggestionsInput/SuggestionsInput'; -import { numberOrVariableValidator } from '../utils'; +import { getVariableSuggestions, numberOrVariableValidator } from '../utils'; export const HistogramTransformerEditor = ({ input, @@ -33,36 +30,6 @@ export const HistogramTransformerEditor = ({ bucketOffset: !numberOrVariableValidator(options.bucketOffset || ''), }); - const onBucketCountChanged = useCallback( - (val?: number) => { - onChange({ - ...options, - bucketCount: val, - }); - }, - [onChange, options] - ); - - const onBucketSizeChanged = useCallback( - (val?: number) => { - onChange({ - ...options, - bucketSize: val, - }); - }, - [onChange, options] - ); - - const onBucketOffsetChanged = useCallback( - (val?: number) => { - onChange({ - ...options, - bucketOffset: val, - }); - }, - [onChange, options] - ); - const onVariableBucketCountChanged = useCallback( (value: string) => { setInvalid({ ...isInvalid, bucketCount: !numberOrVariableValidator(value) }); @@ -106,72 +73,7 @@ export const HistogramTransformerEditor = ({ }); }, [onChange, options]); - const templateSrv = getTemplateSrv(); - const variables = templateSrv.getVariables().map((v) => { - return { value: v.name, label: v.label || v.name, origin: VariableOrigin.Template }; - }); - - if (!cfg.featureToggles.transformationsVariableSupport) { - let bucketSize; - if (typeof options.bucketSize === 'string') { - bucketSize = parseInt(options.bucketSize, 10); - } else { - bucketSize = options.bucketSize; - } - - let bucketOffset; - if (typeof options.bucketOffset === 'string') { - bucketOffset = parseInt(options.bucketOffset, 10); - } else { - bucketOffset = options.bucketOffset; - } - - return ( -
          - - - - - - - - - - - - - - - - - - - - -
          - ); - } + const suggestions = getVariableSuggestions(); return (
          @@ -184,7 +86,7 @@ export const HistogramTransformerEditor = ({ tooltip={histogramFieldInfo.bucketCount.description} > ) => { const [isInvalid, setInvalid] = useState(false); - const onSetLimit = useCallback( - (value: FormEvent) => { - onChange({ - ...options, - limitField: Number(value.currentTarget.value), - }); - }, - [onChange, options] - ); - const onSetVariableLimit = useCallback( (value: string) => { setInvalid(!numberOrVariableValidator(value)); @@ -40,28 +28,6 @@ export const LimitTransformerEditor = ({ options, onChange }: TransformerUIProps [onChange, options] ); - const templateSrv = getTemplateSrv(); - const variables = templateSrv.getVariables().map((v) => { - return { value: v.name, label: v.label || v.name, origin: VariableOrigin.Template }; - }); - - if (!cfg.featureToggles.transformationsVariableSupport) { - return ( - <> - - - - - - - ); - } return ( <> @@ -71,7 +37,7 @@ export const LimitTransformerEditor = ({ options, onChange }: TransformerUIProps value={String(options.limitField)} onChange={onSetVariableLimit} placeholder="Value or variable" - suggestions={variables} + suggestions={getVariableSuggestions()} > diff --git a/public/app/features/transformers/editors/SortByTransformerEditor.tsx b/public/app/features/transformers/editors/SortByTransformerEditor.tsx index f0362a65ddb..3faa19ebf8f 100644 --- a/public/app/features/transformers/editors/SortByTransformerEditor.tsx +++ b/public/app/features/transformers/editors/SortByTransformerEditor.tsx @@ -8,7 +8,7 @@ import { TransformerCategory, } from '@grafana/data'; import { SortByField, SortByTransformerOptions } from '@grafana/data/src/transformations/transformers/sortBy'; -import { getTemplateSrv, config as cfg } from '@grafana/runtime'; +import { getTemplateSrv } from '@grafana/runtime'; import { InlineField, InlineSwitch, InlineFieldRow, Select } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; @@ -36,7 +36,7 @@ export const SortByTransformerEditor = ({ input, options, onChange }: Transforme - {config.featureToggles.sqlQuerybuilderFunctionParameters && ( - - )} + + onFromDayOfWeekChange(v)} - width={20} - /> - onTimeChange(v ? dateTimeAsMoment(v) : v, 'from')} - allowEmpty={true} - placeholder="HH:mm" - size="sm" - /> - + + {t('dashboard-settings.time-regions.advanced-description-use', 'Use ')} + + {t('dashboard-settings.time-regions.advanced-description-cron', 'Cron syntax')} + + {t( + 'dashboard-settings.time-regions.advanced-description-rest', + ' to define a recurrence schedule and duration' + )} + + } + > + ) => onModeChange(e.currentTarget.checked ? 'cron' : null)} + /> - - - {(value.fromDayOfWeek || value.toDayOfWeek) && ( - onFromDayOfWeekChange(v)} + width={20} + /> + onTimeChange(v ? dateTimeAsMoment(v) : v, 'from')} + allowEmpty={true} + placeholder="HH:mm" + size="sm" + /> + + + + + {(value.fromDayOfWeek || value.toDayOfWeek) && ( + ) => onCronExprChange(e.target.value)} + value={value.cronExpr} + placeholder="0 9 * * 1-5" + width={40} /> - )} - onTimeChange(v ? dateTimeAsMoment(v) : v, 'to')} - allowEmpty={true} - placeholder="HH:mm" - size="sm" - /> - - + + + ) => onDurationChange(e.target.value)} + value={value.duration} + placeholder="8h" + width={40} + /> + + + )} {renderTimezone()} ); diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 51dc7afb94e..02216e02f73 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -108,7 +108,7 @@ export class GrafanaDatasource extends DataSourceWithBackend { continue; } if (target.queryType === GrafanaQueryType.TimeRegions) { - const frame = doTimeRegionQuery('', target.timeRegion!, request.range, request.timezone); + const frame = doTimeRegionQuery('', target.timeRegion!, request.range); results.push( of({ data: frame ? [frame] : [], @@ -195,12 +195,7 @@ export class GrafanaDatasource extends DataSourceWithBackend { async getAnnotations(options: AnnotationQueryRequest): Promise { const query = options.annotation.target as GrafanaQuery; if (query?.queryType === GrafanaQueryType.TimeRegions) { - const frame = doTimeRegionQuery( - options.annotation.name, - query.timeRegion!, - options.range, - getDashboardSrv().getCurrent()?.timezone // Annotation queries don't include the timezone - ); + const frame = doTimeRegionQuery(options.annotation.name, query.timeRegion!, options.range); return Promise.resolve({ data: frame ? [frame] : [] }); } diff --git a/public/app/plugins/datasource/grafana/timeRegions.test.ts b/public/app/plugins/datasource/grafana/timeRegions.test.ts index f5502fec661..76c052bc195 100644 --- a/public/app/plugins/datasource/grafana/timeRegions.test.ts +++ b/public/app/plugins/datasource/grafana/timeRegions.test.ts @@ -6,7 +6,7 @@ describe('grafana data source', () => { it('supports time region query', () => { const frame = doTimeRegionQuery( 'test', - { fromDayOfWeek: 1, toDayOfWeek: 2 }, + { fromDayOfWeek: 1, toDayOfWeek: 2, timezone: 'utc' }, { from: dateTime('2023-03-01'), to: dateTime('2023-03-31'), @@ -14,8 +14,7 @@ describe('grafana data source', () => { to: '', from: '', }, - }, - 'utc' + } ); expect(toDataFrameDTO(frame!)).toMatchInlineSnapshot(` @@ -39,10 +38,10 @@ describe('grafana data source', () => { "name": "timeEnd", "type": "time", "values": [ - 1678233599000, - 1678838399000, - 1679443199000, - 1680047999000, + 1678233600000, + 1678838400000, + 1679443200000, + 1680048000000, ], }, { @@ -80,8 +79,7 @@ describe('grafana data source', () => { to: '', from: '', }, - }, - 'utc' + } ); expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(` @@ -101,7 +99,7 @@ describe('grafana data source', () => { "name": "timeEnd", "type": "time", "values": [ - 1678147199000, + 1678147200000, ], }, { @@ -133,8 +131,7 @@ describe('grafana data source', () => { to: '', from: '', }, - }, - 'utc' + } ); expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(` @@ -154,7 +151,7 @@ describe('grafana data source', () => { "name": "timeEnd", "type": "time", "values": [ - 1678165199000, + 1678165200000, ], }, { @@ -186,8 +183,7 @@ describe('grafana data source', () => { to: '', from: '', }, - }, - 'utc' + } ); expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(` @@ -207,7 +203,7 @@ describe('grafana data source', () => { "name": "timeEnd", "type": "time", "values": [ - 1678168799000, + 1678168800000, ], }, { @@ -239,8 +235,7 @@ describe('grafana data source', () => { to: '', from: '', }, - }, - 'utc' + } ); expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(` @@ -260,7 +255,7 @@ describe('grafana data source', () => { "name": "timeEnd", "type": "time", "values": [ - 1678143599000, + 1678143600000, ], }, { @@ -292,8 +287,7 @@ describe('grafana data source', () => { to: '', from: '', }, - }, - 'utc' + } ); expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(` @@ -313,7 +307,7 @@ describe('grafana data source', () => { "name": "timeEnd", "type": "time", "values": [ - 1678121999000, + 1678122000000, ], }, { @@ -345,8 +339,7 @@ describe('grafana data source', () => { to: '', from: '', }, - }, - 'Asia/Dubai' + } ); expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(` @@ -366,7 +359,7 @@ describe('grafana data source', () => { "name": "timeEnd", "type": "time", "values": [ - 1678147199000, + 1678147200000, ], }, { @@ -398,8 +391,7 @@ describe('grafana data source', () => { to: '', from: '', }, - }, - 'America/Chicago' + } ); expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(` @@ -451,8 +443,7 @@ describe('grafana data source', () => { to: '', from: '', }, - }, - 'America/Chicago' + } ); expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(` diff --git a/public/app/plugins/datasource/grafana/timeRegions.ts b/public/app/plugins/datasource/grafana/timeRegions.ts index 8c7f000ba2a..6e3fb3b0da6 100644 --- a/public/app/plugins/datasource/grafana/timeRegions.ts +++ b/public/app/plugins/datasource/grafana/timeRegions.ts @@ -1,48 +1,28 @@ -import { TimeRange, DataFrame, FieldType, getTimeZoneInfo } from '@grafana/data'; +import { TimeRange, DataFrame, FieldType } from '@grafana/data'; import { TimeRegionConfig, calculateTimesWithin } from 'app/core/utils/timeRegions'; -export function doTimeRegionQuery( - name: string, - config: TimeRegionConfig, - range: TimeRange, - tz: string -): DataFrame | undefined { - if (!config) { - return undefined; - } - const regions = calculateTimesWithin(config, range); // UTC - if (!regions.length) { - return undefined; - } +export function doTimeRegionQuery(name: string, config: TimeRegionConfig, range: TimeRange): DataFrame | undefined { + const { mode, duration, cronExpr, from, fromDayOfWeek } = config; - const times: number[] = []; - const timesEnd: number[] = []; - const texts: string[] = []; + const isValidSimple = mode == null && (fromDayOfWeek != null || from != null); + const isValidAdvanced = mode === 'cron' && cronExpr != null && duration != null; - const regionTimezone = config.timezone ?? tz; + if (isValidSimple || isValidAdvanced) { + const ranges = calculateTimesWithin(config, range); - for (const region of regions) { - let from = region.from; - let to = region.to; + if (ranges.length > 0) { + const frame: DataFrame = { + fields: [ + { name: 'time', type: FieldType.time, values: ranges.map((r) => r.from), config: {} }, + { name: 'timeEnd', type: FieldType.time, values: ranges.map((r) => r.to), config: {} }, + { name: 'text', type: FieldType.string, values: Array(ranges.length).fill(name), config: {} }, + ], + length: ranges.length, + }; - const info = getTimeZoneInfo(regionTimezone, from); - if (info) { - const offset = info.offsetInMins * 60 * 1000; - from += offset; - to += offset; + return frame; } - - times.push(from); - timesEnd.push(to); - texts.push(name); } - return { - fields: [ - { name: 'time', type: FieldType.time, values: times, config: {} }, - { name: 'timeEnd', type: FieldType.time, values: timesEnd, config: {} }, - { name: 'text', type: FieldType.string, values: texts, config: {} }, - ], - length: times.length, - }; + return undefined; } diff --git a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts deleted file mode 100644 index 960baaa4287..00000000000 --- a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { dateTime } from '@grafana/data'; - -import { TimeRegionManager, colorModes } from '../time_region_manager'; - -describe('TimeRegionManager', () => { - function plotOptionsScenario(desc: string, func: any) { - describe(desc, () => { - const ctx: any = { - panel: { - timeRegions: [], - }, - options: { - grid: { markings: [] }, - }, - panelCtrl: { - range: {}, - dashboard: {}, - }, - }; - - ctx.setup = (regions: any, from: any, to: any) => { - ctx.panel.timeRegions = regions; - ctx.panelCtrl.range.from = from; - ctx.panelCtrl.range.to = to; - const manager = new TimeRegionManager(ctx.panelCtrl); - manager.addFlotOptions(ctx.options, ctx.panel); - }; - - ctx.printScenario = () => { - console.log( - `Time range: from=${ctx.panelCtrl.range.from.format()}, to=${ctx.panelCtrl.range.to.format()}`, - ctx.panelCtrl.range.from._isUTC - ); - ctx.options.grid.markings.forEach((m: any, i: number) => { - console.log( - `Marking (${i}): from=${dateTime(m.xaxis.from).format()}, to=${dateTime(m.xaxis.to).format()}, color=${ - m.color - }` - ); - }); - }; - - func(ctx); - }); - } - - describe('When colors missing in config', () => { - plotOptionsScenario('should not throw an error when fillColor is undefined', (ctx: any) => { - const regions = [ - { fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, lineColor: '#ffffff', colorMode: 'custom' }, - ]; - const from = dateTime('2018-01-01T00:00:00+01:00'); - const to = dateTime('2018-01-01T23:59:00+01:00'); - expect(() => ctx.setup(regions, from, to)).not.toThrow(); - }); - plotOptionsScenario('should not throw an error when lineColor is undefined', (ctx: any) => { - const regions = [ - { fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, fillColor: '#ffffff', line: true, colorMode: 'custom' }, - ]; - const from = dateTime('2018-01-01T00:00:00+01:00'); - const to = dateTime('2018-01-01T23:59:00+01:00'); - expect(() => ctx.setup(regions, from, to)).not.toThrow(); - }); - }); - - describe('When creating plot markings using local time', () => { - plotOptionsScenario('for day of week region', (ctx: any) => { - const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }]; - const from = dateTime('2018-01-01T00:00:00+01:00'); - const to = dateTime('2018-01-01T23:59:00+01:00'); - ctx.setup(regions, from, to); - - it('should add 3 markings', () => { - expect(ctx.options.grid.markings.length).toBe(3); - }); - - it('should add fill', () => { - const markings = ctx.options.grid.markings; - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-01-01T01:00:00+01:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-01-02T00:59:59+01:00').format()); - expect(markings[0].color).toBe(colorModes.red.color.fill); - }); - - it('should add line before', () => { - const markings = ctx.options.grid.markings; - expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-01-01T01:00:00+01:00').format()); - expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-01-01T01:00:00+01:00').format()); - expect(markings[1].color).toBe(colorModes.red.color.line); - }); - - it('should add line after', () => { - const markings = ctx.options.grid.markings; - expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-01-02T00:59:59+01:00').format()); - expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-01-02T00:59:59+01:00').format()); - expect(markings[2].color).toBe(colorModes.red.color.line); - }); - }); - - plotOptionsScenario('for time from region', (ctx: any) => { - const regions = [{ from: '05:00', fill: true, colorMode: 'red' }]; - const from = dateTime('2018-01-01T00:00+01:00'); - const to = dateTime('2018-01-03T23:59+01:00'); - ctx.setup(regions, from, to); - - it('should add 3 markings', () => { - expect(ctx.options.grid.markings.length).toBe(3); - }); - - it('should add one fill at 05:00 each day', () => { - const markings = ctx.options.grid.markings; - - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-01-01T06:00:00+01:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-01-01T06:00:00+01:00').format()); - expect(markings[0].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-01-02T06:00:00+01:00').format()); - expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-01-02T06:00:00+01:00').format()); - expect(markings[1].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-01-03T06:00:00+01:00').format()); - expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-01-03T06:00:00+01:00').format()); - expect(markings[2].color).toBe(colorModes.red.color.fill); - }); - }); - - plotOptionsScenario('for time to region', (ctx: any) => { - const regions = [{ to: '05:00', fill: true, colorMode: 'red' }]; - const from = dateTime('2018-02-01T00:00+01:00'); - const to = dateTime('2018-02-03T23:59+01:00'); - ctx.setup(regions, from, to); - - it('should add 3 markings', () => { - expect(ctx.options.grid.markings.length).toBe(3); - }); - - it('should add one fill at 05:00 each day', () => { - const markings = ctx.options.grid.markings; - - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-02-01T06:00:00+01:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-02-01T06:00:00+01:00').format()); - expect(markings[0].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-02-02T06:00:00+01:00').format()); - expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-02-02T06:00:00+01:00').format()); - expect(markings[1].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-02-03T06:00:00+01:00').format()); - expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-02-03T06:00:00+01:00').format()); - expect(markings[2].color).toBe(colorModes.red.color.fill); - }); - }); - - plotOptionsScenario('for time from/to region', (ctx: any) => { - const regions = [{ from: '00:00', to: '05:00', fill: true, colorMode: 'red' }]; - const from = dateTime('2018-12-01T00:00+01:00'); - const to = dateTime('2018-12-03T23:59+01:00'); - ctx.setup(regions, from, to); - - it('should add 3 markings', () => { - expect(ctx.options.grid.markings.length).toBe(3); - }); - - it('should add one fill between 00:00 and 05:00 each day', () => { - const markings = ctx.options.grid.markings; - - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-12-01T01:00:00+01:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-12-01T06:00:00+01:00').format()); - expect(markings[0].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-12-02T01:00:00+01:00').format()); - expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-12-02T06:00:00+01:00').format()); - expect(markings[1].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-12-03T01:00:00+01:00').format()); - expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-12-03T06:00:00+01:00').format()); - expect(markings[2].color).toBe(colorModes.red.color.fill); - }); - }); - - plotOptionsScenario('for time from/to region crossing midnight', (ctx: any) => { - const regions = [{ from: '22:00', to: '00:30', fill: true, colorMode: 'red' }]; - const from = dateTime('2018-12-01T12:00+01:00'); - const to = dateTime('2018-12-04T08:00+01:00'); - ctx.setup(regions, from, to); - - it('should add 3 markings', () => { - expect(ctx.options.grid.markings.length).toBe(3); - }); - - it('should add one fill between 22:00 and 00:30 each day', () => { - const markings = ctx.options.grid.markings; - - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-12-01T23:00:00+01:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-12-02T01:30:00+01:00').format()); - expect(markings[0].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-12-02T23:00:00+01:00').format()); - expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-12-03T01:30:00+01:00').format()); - expect(markings[1].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-12-03T23:00:00+01:00').format()); - expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-12-04T01:30:00+01:00').format()); - expect(markings[2].color).toBe(colorModes.red.color.fill); - }); - }); - - plotOptionsScenario('for day of week from/to region', (ctx: any) => { - const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }]; - const from = dateTime('2018-01-01T18:45:05+01:00'); - const to = dateTime('2018-01-22T08:27:00+01:00'); - ctx.setup(regions, from, to); - - it('should add 3 markings', () => { - expect(ctx.options.grid.markings.length).toBe(3); - }); - - it('should add one fill at each sunday', () => { - const markings = ctx.options.grid.markings; - - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-01-07T01:00:00+01:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-01-08T00:59:59+01:00').format()); - expect(markings[0].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-01-14T01:00:00+01:00').format()); - expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-01-15T00:59:59+01:00').format()); - expect(markings[1].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-01-21T01:00:00+01:00').format()); - expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-01-22T00:59:59+01:00').format()); - expect(markings[2].color).toBe(colorModes.red.color.fill); - }); - }); - - plotOptionsScenario('for day of week from region', (ctx: any) => { - const regions = [{ fromDayOfWeek: 7, fill: true, colorMode: 'red' }]; - const from = dateTime('2018-01-01T18:45:05+01:00'); - const to = dateTime('2018-01-22T08:27:00+01:00'); - ctx.setup(regions, from, to); - - it('should add 3 markings', () => { - expect(ctx.options.grid.markings.length).toBe(3); - }); - - it('should add one fill at each sunday', () => { - const markings = ctx.options.grid.markings; - - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-01-07T01:00:00+01:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-01-08T00:59:59+01:00').format()); - expect(markings[0].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-01-14T01:00:00+01:00').format()); - expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-01-15T00:59:59+01:00').format()); - expect(markings[1].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-01-21T01:00:00+01:00').format()); - expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-01-22T00:59:59+01:00').format()); - expect(markings[2].color).toBe(colorModes.red.color.fill); - }); - }); - - plotOptionsScenario('for day of week to region', (ctx: any) => { - const regions = [{ toDayOfWeek: 7, fill: true, colorMode: 'red' }]; - const from = dateTime('2018-01-01T18:45:05+01:00'); - const to = dateTime('2018-01-22T08:27:00+01:00'); - ctx.setup(regions, from, to); - - it('should add 3 markings', () => { - expect(ctx.options.grid.markings.length).toBe(3); - }); - - it('should add one fill at each sunday', () => { - const markings = ctx.options.grid.markings; - - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-01-07T01:00:00+01:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-01-08T00:59:59+01:00').format()); - expect(markings[0].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-01-14T01:00:00+01:00').format()); - expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-01-15T00:59:59+01:00').format()); - expect(markings[1].color).toBe(colorModes.red.color.fill); - - expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-01-21T01:00:00+01:00').format()); - expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-01-22T00:59:59+01:00').format()); - expect(markings[2].color).toBe(colorModes.red.color.fill); - }); - }); - - plotOptionsScenario('for day of week from/to time region', (ctx: any) => { - const regions = [{ fromDayOfWeek: 7, from: '23:00', toDayOfWeek: 1, to: '01:40', fill: true, colorMode: 'red' }]; - const from = dateTime('2018-12-07T12:51:19+01:00'); - const to = dateTime('2018-12-10T13:51:29+01:00'); - ctx.setup(regions, from, to); - - it('should add 1 marking', () => { - expect(ctx.options.grid.markings.length).toBe(1); - }); - - it('should add one fill between sunday 23:00 and monday 01:40', () => { - const markings = ctx.options.grid.markings; - - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-12-10T00:00:00+01:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-12-10T02:40:00+01:00').format()); - }); - }); - - plotOptionsScenario('for day of week from/to time region', (ctx: any) => { - const regions = [{ fromDayOfWeek: 6, from: '03:00', toDayOfWeek: 7, to: '02:00', fill: true, colorMode: 'red' }]; - const from = dateTime('2018-12-07T12:51:19+01:00'); - const to = dateTime('2018-12-10T13:51:29+01:00'); - ctx.setup(regions, from, to); - - it('should add 1 marking', () => { - expect(ctx.options.grid.markings.length).toBe(1); - }); - - it('should add one fill between saturday 03:00 and sunday 02:00', () => { - const markings = ctx.options.grid.markings; - - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-12-08T04:00:00+01:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-12-09T03:00:00+01:00').format()); - }); - }); - - plotOptionsScenario('for day of week from/to time region with daylight saving time', (ctx: any) => { - const regions = [{ fromDayOfWeek: 7, from: '20:00', toDayOfWeek: 7, to: '23:00', fill: true, colorMode: 'red' }]; - const from = dateTime('2018-03-17T06:00:00+01:00'); - const to = dateTime('2018-04-03T06:00:00+02:00'); - ctx.setup(regions, from, to); - - it('should add 3 markings', () => { - expect(ctx.options.grid.markings.length).toBe(3); - }); - - it('should add one fill at each sunday between 20:00 and 23:00', () => { - const markings = ctx.options.grid.markings; - - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-03-18T21:00:00+01:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-03-19T00:00:00+01:00').format()); - - expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-03-25T22:00:00+02:00').format()); - expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-03-26T01:00:00+02:00').format()); - - expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-04-01T22:00:00+02:00').format()); - expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-04-02T01:00:00+02:00').format()); - }); - }); - - plotOptionsScenario('for each day of week with winter time', (ctx: any) => { - const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }]; - const from = dateTime('2018-10-20T14:50:11+02:00'); - const to = dateTime('2018-11-07T12:56:23+01:00'); - ctx.setup(regions, from, to); - - it('should add 3 markings', () => { - expect(ctx.options.grid.markings.length).toBe(3); - }); - - it('should add one fill at each sunday', () => { - const markings = ctx.options.grid.markings; - - expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-10-21T02:00:00+02:00').format()); - expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-10-22T01:59:59+02:00').format()); - - expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-10-28T02:00:00+02:00').format()); - expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-10-29T00:59:59+01:00').format()); - - expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-11-04T01:00:00+01:00').format()); - expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-11-05T00:59:59+01:00').format()); - }); - }); - }); -}); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index d7cba3f8990..9773ba5c185 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1337,6 +1337,12 @@ "time-zone-label": "Time zone", "week-start-label": "Week start" }, + "time-regions": { + "advanced-description-cron": "Cron syntax", + "advanced-description-rest": " to define a recurrence schedule and duration", + "advanced-description-use": "Use ", + "advanced-label": "Advanced" + }, "variables": { "title": "Variables" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 2dc0f928c44..b442e198f22 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1337,6 +1337,12 @@ "time-zone-label": "Ŧįmę žőʼnę", "week-start-label": "Ŵęęĸ şŧäřŧ" }, + "time-regions": { + "advanced-description-cron": "Cřőʼn şyʼnŧäχ", + "advanced-description-rest": " ŧő đęƒįʼnę ä řęčūřřęʼnčę şčĥęđūľę äʼnđ đūřäŧįőʼn", + "advanced-description-use": "Ůşę ", + "advanced-label": "Åđväʼnčęđ" + }, "variables": { "title": "Väřįäþľęş" }, diff --git a/yarn.lock b/yarn.lock index c54b1d4815a..105c54482f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13935,6 +13935,13 @@ __metadata: languageName: node linkType: hard +"croner@npm:^9.0.0": + version: 9.0.0 + resolution: "croner@npm:9.0.0" + checksum: 10/b3cea758eedfe92e35c4ebae46c9db615565348ad898b5938fd94ea77abc5ff68d86539db248a1666b007b0726da642c9046879e8fd598220afcc87c8135e656 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -18265,6 +18272,7 @@ __metadata: copy-webpack-plugin: "npm:12.0.2" core-js: "npm:3.40.0" crashme: "npm:0.0.15" + croner: "npm:^9.0.0" css-loader: "npm:7.1.2" css-minimizer-webpack-plugin: "npm:7.0.0" cypress: "npm:13.10.0" From 18e54a99742b43eedbf2f7ce9c5b3fa5764a31ad Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:53:23 -0500 Subject: [PATCH 772/894] Docs: Add cron option for time regions (#101021) --- .../build-dashboards/annotate-visualizations/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md b/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md index 13220b2f56a..610f74585d1 100644 --- a/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md +++ b/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md @@ -209,3 +209,7 @@ When adding or editing an annotation, you can define a repeating time region by The above configuration produces the following result in the Time series panel: {{< figure src="/media/docs/grafana/screenshot-grafana-10-0-timeseries-time-regions.png" max-width="600px" alt="Time series visualization with time regions business hours" >}} + +Toggle the **Advanced** switch and use [Cron syntax](https://crontab.run/) to set more granular time region controls. The following example sets a time region of 9:00 AM, Monday to Friday: + +{{< figure src="/media/docs/grafana/dashboards/screenshot-annotations-cron-option-v11.6.png" max-width="600px" alt="Time region query with cron syntax" >}} From a8c5252a832a2cafdeaac983a7ecb77708a13eea Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Thu, 20 Feb 2025 22:11:39 +0100 Subject: [PATCH 773/894] featureflags: reaplces explore with drilldown in descriptions (#101101) Signed-off-by: bergquist --- pkg/services/featuremgmt/registry.go | 8 +++--- pkg/services/featuremgmt/toggles_gen.go | 8 +++--- pkg/services/featuremgmt/toggles_gen.json | 33 ++++++++++++++--------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 6ef1d2155fc..832fcdba105 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -877,7 +877,7 @@ var ( }, { Name: "exploreMetrics", - Description: "Enables the new Explore Metrics core app", + Description: "Enables the new Grafana Metrics Drilldown core app", Stage: FeatureStageGeneralAvailability, Expression: "true", // enabled by default FrontendOnly: true, @@ -1346,21 +1346,21 @@ var ( }, { Name: "exploreLogsShardSplitting", - Description: "Used in Explore Logs to split queries into multiple queries based on the number of shards", + Description: "Used in Logs Drilldown to split queries into multiple queries based on the number of shards", Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, }, { Name: "exploreLogsAggregatedMetrics", - Description: "Used in Explore Logs to query by aggregated metrics", + Description: "Used in Logs Drilldown to query by aggregated metrics", Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, }, { Name: "exploreLogsLimitedTimeRange", - Description: "Used in Explore Logs to limit the time range", + Description: "Used in Logs Drilldown to limit the time range", Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 9aa900797ce..e9060522259 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -476,7 +476,7 @@ const ( FlagLogsInfiniteScrolling = "logsInfiniteScrolling" // FlagExploreMetrics - // Enables the new Explore Metrics core app + // Enables the new Grafana Metrics Drilldown core app FlagExploreMetrics = "exploreMetrics" // FlagAlertingSimplifiedRouting @@ -716,15 +716,15 @@ const ( FlagAlertingPrometheusRulesPrimary = "alertingPrometheusRulesPrimary" // FlagExploreLogsShardSplitting - // Used in Explore Logs to split queries into multiple queries based on the number of shards + // Used in Logs Drilldown to split queries into multiple queries based on the number of shards FlagExploreLogsShardSplitting = "exploreLogsShardSplitting" // FlagExploreLogsAggregatedMetrics - // Used in Explore Logs to query by aggregated metrics + // Used in Logs Drilldown to query by aggregated metrics FlagExploreLogsAggregatedMetrics = "exploreLogsAggregatedMetrics" // FlagExploreLogsLimitedTimeRange - // Used in Explore Logs to limit the time range + // Used in Logs Drilldown to limit the time range FlagExploreLogsLimitedTimeRange = "exploreLogsLimitedTimeRange" // FlagHomeSetupGuide diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 983c65e8298..2c3efd65bd6 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1544,11 +1544,14 @@ { "metadata": { "name": "exploreLogsAggregatedMetrics", - "resourceVersion": "1724938092041", - "creationTimestamp": "2024-08-29T13:55:59Z" + "resourceVersion": "1740084492165", + "creationTimestamp": "2024-08-29T13:55:59Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-02-20 20:48:12.165306 +0000 UTC" + } }, "spec": { - "description": "Used in Explore Logs to query by aggregated metrics", + "description": "Used in Logs Drilldown to query by aggregated metrics", "stage": "experimental", "codeowner": "@grafana/observability-logs", "frontend": true @@ -1557,11 +1560,14 @@ { "metadata": { "name": "exploreLogsLimitedTimeRange", - "resourceVersion": "1724938092041", - "creationTimestamp": "2024-08-29T13:55:59Z" + "resourceVersion": "1740084492165", + "creationTimestamp": "2024-08-29T13:55:59Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-02-20 20:48:12.165306 +0000 UTC" + } }, "spec": { - "description": "Used in Explore Logs to limit the time range", + "description": "Used in Logs Drilldown to limit the time range", "stage": "experimental", "codeowner": "@grafana/observability-logs", "frontend": true @@ -1570,11 +1576,14 @@ { "metadata": { "name": "exploreLogsShardSplitting", - "resourceVersion": "1724938092041", - "creationTimestamp": "2024-08-29T13:55:59Z" + "resourceVersion": "1740084492165", + "creationTimestamp": "2024-08-29T13:55:59Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-02-20 20:48:12.165306 +0000 UTC" + } }, "spec": { - "description": "Used in Explore Logs to split queries into multiple queries based on the number of shards", + "description": "Used in Logs Drilldown to split queries into multiple queries based on the number of shards", "stage": "experimental", "codeowner": "@grafana/observability-logs", "frontend": true @@ -1583,14 +1592,14 @@ { "metadata": { "name": "exploreMetrics", - "resourceVersion": "1737658563230", + "resourceVersion": "1740084233934", "creationTimestamp": "2024-04-09T18:15:18Z", "annotations": { - "grafana.app/updatedTimestamp": "2025-01-23 18:56:03.23086 +0000 UTC" + "grafana.app/updatedTimestamp": "2025-02-20 20:43:53.934892 +0000 UTC" } }, "spec": { - "description": "Enables the new Explore Metrics core app", + "description": "Enables the new Grafana Metrics Drilldown core app", "stage": "GA", "codeowner": "@grafana/observability-metrics", "frontend": true, From 0209d719484fdde2d7019699cc6697401023283a Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 02:30:29 +0200 Subject: [PATCH 774/894] I18n: Download translations from Crowdin (#101109) 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 | 15 +++++++++++++++ public/locales/es-ES/grafana.json | 15 +++++++++++++++ public/locales/fr-FR/grafana.json | 15 +++++++++++++++ public/locales/pt-BR/grafana.json | 15 +++++++++++++++ public/locales/zh-Hans/grafana.json | 15 +++++++++++++++ 5 files changed, 75 insertions(+) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index ec82fb1aab8..1c26fc857cc 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -1337,6 +1337,12 @@ "time-zone-label": "Zeitzone", "week-start-label": "Wochenbeginn" }, + "time-regions": { + "advanced-description-cron": "", + "advanced-description-rest": "", + "advanced-description-use": "", + "advanced-label": "" + }, "variables": { "title": "Variable" }, @@ -1419,6 +1425,11 @@ }, "explore": { "add-to-dashboard": "Zum Dashboard hinzufügen", + "drilldownInfo": { + "action": "", + "description": "", + "title": "" + }, "logs": { "logs-volume": { "add-filters": "", @@ -2499,6 +2510,9 @@ "detect": { "title": "Erkennen" }, + "drilldown": { + "title": "" + }, "explore": { "title": "Entdecken" }, @@ -2714,6 +2728,7 @@ "close": "Menü schließen", "dock": "Menü andocken", "list-label": "Navigation", + "new": "", "open": "", "undock": "Menü abdocken" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 47b9c17242a..aead0c584e5 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -1337,6 +1337,12 @@ "time-zone-label": "Huso horario", "week-start-label": "Inicio de la semana" }, + "time-regions": { + "advanced-description-cron": "", + "advanced-description-rest": "", + "advanced-description-use": "", + "advanced-label": "" + }, "variables": { "title": "Variables" }, @@ -1419,6 +1425,11 @@ }, "explore": { "add-to-dashboard": "Añadir al tablero", + "drilldownInfo": { + "action": "", + "description": "", + "title": "" + }, "logs": { "logs-volume": { "add-filters": "", @@ -2499,6 +2510,9 @@ "detect": { "title": "Detectar" }, + "drilldown": { + "title": "" + }, "explore": { "title": "Explorar" }, @@ -2714,6 +2728,7 @@ "close": "Cerrar menú", "dock": "Anclar el menú", "list-label": "Navegación", + "new": "", "open": "", "undock": "Desanclar el menú" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index bdd9bbee422..d7bc91e81db 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -1337,6 +1337,12 @@ "time-zone-label": "Fuseau horaire", "week-start-label": "Début de la semaine" }, + "time-regions": { + "advanced-description-cron": "", + "advanced-description-rest": "", + "advanced-description-use": "", + "advanced-label": "" + }, "variables": { "title": "Variables" }, @@ -1419,6 +1425,11 @@ }, "explore": { "add-to-dashboard": "Ajouter au tableau de bord", + "drilldownInfo": { + "action": "", + "description": "", + "title": "" + }, "logs": { "logs-volume": { "add-filters": "", @@ -2499,6 +2510,9 @@ "detect": { "title": "Détecter" }, + "drilldown": { + "title": "" + }, "explore": { "title": "Explorer" }, @@ -2714,6 +2728,7 @@ "close": "Fermer le menu", "dock": "Ancrer le menu", "list-label": "Navigation", + "new": "", "open": "", "undock": "Ancrer le menu" }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 2226b4a5d49..d9df30cae3b 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -1337,6 +1337,12 @@ "time-zone-label": "Fuso horário", "week-start-label": "Início da semana" }, + "time-regions": { + "advanced-description-cron": "", + "advanced-description-rest": "", + "advanced-description-use": "", + "advanced-label": "" + }, "variables": { "title": "Variáveis" }, @@ -1419,6 +1425,11 @@ }, "explore": { "add-to-dashboard": "Adicionar ao painel de controle", + "drilldownInfo": { + "action": "", + "description": "", + "title": "" + }, "logs": { "logs-volume": { "add-filters": "", @@ -2499,6 +2510,9 @@ "detect": { "title": "Detectar" }, + "drilldown": { + "title": "" + }, "explore": { "title": "Explorar" }, @@ -2714,6 +2728,7 @@ "close": "Fechar menu", "dock": "Menu da dock", "list-label": "Navegação", + "new": "", "open": "", "undock": "Desacoplar menu" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index d849cb5e829..441b75cca69 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -1328,6 +1328,12 @@ "time-zone-label": "时区", "week-start-label": "每周开始日" }, + "time-regions": { + "advanced-description-cron": "", + "advanced-description-rest": "", + "advanced-description-use": "", + "advanced-label": "" + }, "variables": { "title": "变量" }, @@ -1410,6 +1416,11 @@ }, "explore": { "add-to-dashboard": "添加到仪表板", + "drilldownInfo": { + "action": "", + "description": "", + "title": "" + }, "logs": { "logs-volume": { "add-filters": "", @@ -2489,6 +2500,9 @@ "detect": { "title": "检测" }, + "drilldown": { + "title": "" + }, "explore": { "title": "探索" }, @@ -2704,6 +2718,7 @@ "close": "关闭菜单", "dock": "停靠菜单", "list-label": "导航", + "new": "", "open": "", "undock": "取消停靠菜单" }, From 33eca9e6fb4ce72c6ac0f7bda06945b8a3719697 Mon Sep 17 00:00:00 2001 From: Charandas <542168+charandas@users.noreply.github.com> Date: Thu, 20 Feb 2025 18:29:40 -0800 Subject: [PATCH 775/894] aggregation: fix config.ini reading of the new bool (#101099) --- pkg/services/apiserver/aggregator/aggregator.go | 15 +++++++++++---- pkg/services/apiserver/aggregator/config.go | 9 ++++++++- pkg/services/apiserver/config.go | 1 + pkg/services/apiserver/options/extra.go | 1 + pkg/services/apiserver/options/kube-aggregator.go | 2 +- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/pkg/services/apiserver/aggregator/aggregator.go b/pkg/services/apiserver/aggregator/aggregator.go index 0945ac83139..7338cbab7bb 100644 --- a/pkg/services/apiserver/aggregator/aggregator.go +++ b/pkg/services/apiserver/aggregator/aggregator.go @@ -144,6 +144,11 @@ func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig generi aggregatorConfig.ExtraConfig.ProxyClientKeyFile = commandOptions.KubeAggregatorOptions.ProxyClientKeyFile } + customExtraConfig := &CustomExtraConfig{ + DiscoveryOnlyProxyClientCertFile: commandOptions.KubeAggregatorOptions.ProxyClientCertFile, + DiscoveryOnlyProxyClientKeyFile: commandOptions.KubeAggregatorOptions.ProxyClientKeyFile, + } + if err := commandOptions.KubeAggregatorOptions.ApplyTo(aggregatorConfig, commandOptions.RecommendedOptions.Etcd); err != nil { return nil, err } @@ -156,7 +161,7 @@ func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig generi // Exit early, if no remote services file is configured if commandOptions.KubeAggregatorOptions.RemoteServicesFile == "" { - return NewConfig(aggregatorConfig, sharedInformerFactory, []builder.APIGroupBuilder{serviceAPIBuilder}, nil), nil + return NewConfig(aggregatorConfig, customExtraConfig, sharedInformerFactory, []builder.APIGroupBuilder{serviceAPIBuilder}, nil), nil } remoteServices, err := ReadRemoteServices(commandOptions.KubeAggregatorOptions.RemoteServicesFile) @@ -176,9 +181,11 @@ func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig generi serviceClientSet: serviceClient, } - return NewConfig(aggregatorConfig, sharedInformerFactory, []builder.APIGroupBuilder{serviceAPIBuilder}, remoteServicesConfig), nil + return NewConfig(aggregatorConfig, customExtraConfig, sharedInformerFactory, []builder.APIGroupBuilder{serviceAPIBuilder}, remoteServicesConfig), nil } +// CreateAggregatorServer creates an aggregated server to layer into the existing apiserver +// TODO: passing options temporarily as that allows us to pass in cert/key for client into AvailableController but skip it in the aggregator lib func CreateAggregatorServer(config *Config, delegateAPIServer genericapiserver.DelegationTarget, reg prometheus.Registerer) (*aggregatorapiserver.APIAggregator, error) { aggregatorConfig := config.KubeAggregatorConfig sharedInformerFactory := config.Informers @@ -257,8 +264,8 @@ func CreateAggregatorServer(config *Config, delegateAPIServer genericapiserver.D proxyCurrentCertKeyContentFunc := func() ([]byte, []byte) { return nil, nil } - if len(config.KubeAggregatorConfig.ExtraConfig.ProxyClientCertFile) > 0 && len(config.KubeAggregatorConfig.ExtraConfig.ProxyClientKeyFile) > 0 { - aggregatorProxyCerts, err := dynamiccertificates.NewDynamicServingContentFromFiles("aggregator-proxy-cert", config.KubeAggregatorConfig.ExtraConfig.ProxyClientCertFile, config.KubeAggregatorConfig.ExtraConfig.ProxyClientKeyFile) + if len(config.CustomExtraConfig.DiscoveryOnlyProxyClientCertFile) > 0 && len(config.CustomExtraConfig.DiscoveryOnlyProxyClientKeyFile) > 0 { + aggregatorProxyCerts, err := dynamiccertificates.NewDynamicServingContentFromFiles("aggregator-proxy-cert", config.CustomExtraConfig.DiscoveryOnlyProxyClientCertFile, config.CustomExtraConfig.DiscoveryOnlyProxyClientKeyFile) if err != nil { return nil, err } diff --git a/pkg/services/apiserver/aggregator/config.go b/pkg/services/apiserver/aggregator/config.go index 45d434d5a1a..19017f5ad1a 100644 --- a/pkg/services/apiserver/aggregator/config.go +++ b/pkg/services/apiserver/aggregator/config.go @@ -28,8 +28,14 @@ type RemoteServicesConfig struct { serviceClientSet *serviceclientset.Clientset } +type CustomExtraConfig struct { + DiscoveryOnlyProxyClientCertFile string + DiscoveryOnlyProxyClientKeyFile string +} + type Config struct { KubeAggregatorConfig *aggregatorapiserver.Config + CustomExtraConfig *CustomExtraConfig // this is temporary and will be removed once we have moved across newer auth rollout in cloud Informers informersv0alpha1.SharedInformerFactory RemoteServicesConfig *RemoteServicesConfig // Builders contain prerequisite api groups for aggregator to function correctly e.g. ExternalName @@ -40,7 +46,7 @@ type Config struct { } // remoteServices may be nil when not using aggregation -func NewConfig(aggregator *aggregatorapiserver.Config, informers informersv0alpha1.SharedInformerFactory, builders []builder.APIGroupBuilder, remoteServices *RemoteServicesConfig) *Config { +func NewConfig(aggregator *aggregatorapiserver.Config, customExtraConfig *CustomExtraConfig, informers informersv0alpha1.SharedInformerFactory, builders []builder.APIGroupBuilder, remoteServices *RemoteServicesConfig) *Config { getMergedOpenAPIDefinitions := func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { aggregatorAPIs := aggregatoropenapi.GetOpenAPIDefinitions(ref) builderAPIs := builder.GetOpenAPIDefinitions(builders)(ref) @@ -61,6 +67,7 @@ func NewConfig(aggregator *aggregatorapiserver.Config, informers informersv0alph return &Config{ aggregator, + customExtraConfig, informers, remoteServices, builders, diff --git a/pkg/services/apiserver/config.go b/pkg/services/apiserver/config.go index e8f41b685a8..82d99a227de 100644 --- a/pkg/services/apiserver/config.go +++ b/pkg/services/apiserver/config.go @@ -43,6 +43,7 @@ func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o o.KubeAggregatorOptions.ProxyClientCertFile = apiserverCfg.Key("proxy_client_cert_file").MustString("") o.KubeAggregatorOptions.ProxyClientKeyFile = apiserverCfg.Key("proxy_client_key_file").MustString("") + o.KubeAggregatorOptions.LegacyClientCertAuth = apiserverCfg.Key("legacy_client_cert_auth").MustBool(true) o.KubeAggregatorOptions.APIServiceCABundleFile = apiserverCfg.Key("apiservice_ca_bundle_file").MustString("") o.KubeAggregatorOptions.RemoteServicesFile = apiserverCfg.Key("remote_services_file").MustString("") diff --git a/pkg/services/apiserver/options/extra.go b/pkg/services/apiserver/options/extra.go index 715c319a62f..cdbceb6ff03 100644 --- a/pkg/services/apiserver/options/extra.go +++ b/pkg/services/apiserver/options/extra.go @@ -48,6 +48,7 @@ func (o *ExtraOptions) ApplyTo(c *genericapiserver.RecommendedConfig) error { }); err != nil { return err } + // TODO: klog isn't working as expected, investigate - it logs some of the time klog.SetSlogLogger(logger) if _, err := logs.GlogSetter(strconv.Itoa(o.Verbosity)); err != nil { logger.Error("failed to set log level", "error", err) diff --git a/pkg/services/apiserver/options/kube-aggregator.go b/pkg/services/apiserver/options/kube-aggregator.go index 20e48c7a7ed..480011091b3 100644 --- a/pkg/services/apiserver/options/kube-aggregator.go +++ b/pkg/services/apiserver/options/kube-aggregator.go @@ -48,7 +48,7 @@ func (o *KubeAggregatorOptions) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&o.ProxyClientKeyFile, "proxy-client-key-file", o.ProxyClientKeyFile, "path to proxy client key file") - fs.BoolVar(&o.LegacyClientCertAuth, "legacy_client_cert_auth", true, + fs.BoolVar(&o.LegacyClientCertAuth, "legacy-client-cert-auth", true, "whether to use legacy client cert auth") } From 0290da6aaaa120c8360f96f0e961c5540f4257a1 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Fri, 21 Feb 2025 08:23:04 +0100 Subject: [PATCH 776/894] AccessControl: Allow plugin roles to include `plugins:write` (#101089) --- .../accesscontrol/pluginutils/utils.go | 1 + .../accesscontrol/pluginutils/utils_test.go | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/pkg/services/accesscontrol/pluginutils/utils.go b/pkg/services/accesscontrol/pluginutils/utils.go index 0fe7a46b199..8b7500a97cd 100644 --- a/pkg/services/accesscontrol/pluginutils/utils.go +++ b/pkg/services/accesscontrol/pluginutils/utils.go @@ -12,6 +12,7 @@ import ( var ( allowedCoreActions = map[string]string{ + "plugins:write": "plugins:id:", "plugins.app:access": "plugins:id:", "folders:create": "folders:uid:", "folders:read": "folders:uid:", diff --git a/pkg/services/accesscontrol/pluginutils/utils_test.go b/pkg/services/accesscontrol/pluginutils/utils_test.go index 80289ec77e7..746d92289f4 100644 --- a/pkg/services/accesscontrol/pluginutils/utils_test.go +++ b/pkg/services/accesscontrol/pluginutils/utils_test.go @@ -172,6 +172,29 @@ func TestValidatePluginRole(t *testing.T) { }, wantErr: &ac.ErrorInvalidRole{}, }, + { + name: "valid core plugin permission targets plugin", + pluginID: "test-app", + role: ac.RoleDTO{ + Name: "plugins:test-app:reader", + DisplayName: "Plugin Configurator", + Permissions: []ac.Permission{ + {Action: "plugins:write", Scope: "plugins:id:test-app"}, + }, + }, + }, + { + name: "invalid core plugin permission targets other plugin", + pluginID: "test-app", + role: ac.RoleDTO{ + Name: "plugins:test-app:reader", + DisplayName: "Plugin Configurator", + Permissions: []ac.Permission{ + {Action: "plugins:write", Scope: "plugins:id:other-app"}, + }, + }, + wantErr: &ac.ErrorInvalidRole{}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 79c0e5e3ece5342854e2192f476db42b2d800eec Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Fri, 21 Feb 2025 08:51:34 +0100 Subject: [PATCH 777/894] scopes: moves scopes to enterprise (#100746) Signed-off-by: bergquist --- go.work.sum | 5 + pkg/apimachinery/apis/common/v0alpha1/doc.go | 1 + .../apis/common/v0alpha1/types.go | 42 ++ .../common/v0alpha1/zz_generated.deepcopy.go | 99 +++ .../common/v0alpha1/zz_generated.openapi.go | 141 ++++ ...enerated.openapi_violation_exceptions.list | 1 + pkg/apis/scope/v0alpha1/doc.go | 6 - pkg/apis/scope/v0alpha1/register.go | 139 ---- pkg/apis/scope/v0alpha1/types.go | 167 ----- .../scope/v0alpha1/zz_generated.deepcopy.go | 370 ---------- .../scope/v0alpha1/zz_generated.defaults.go | 19 - .../scope/v0alpha1/zz_generated.openapi.go | 661 ------------------ ...enerated.openapi_violation_exceptions.list | 4 - pkg/registry/apis/apis.go | 2 - pkg/registry/apis/scope/find.go | 113 --- .../apis/scope/find_scope_dashboards.go | 106 --- pkg/registry/apis/scope/find_test.go | 71 -- pkg/registry/apis/scope/register.go | 224 ------ pkg/registry/apis/scope/storage.go | 142 ---- pkg/registry/apis/wireset.go | 2 - .../apis/scopes/scope_nodes_example_test.go | 84 --- pkg/tests/apis/scopes/scopes_test.go | 221 ------ .../example-scope-dashboard-binding-abc.yaml | 10 - .../example-scope-dashboard-binding-xyz.yaml | 10 - .../apis/scopes/testdata/example-scope.yaml | 14 - .../apis/scopes/testdata/example-scope2.yaml | 14 - .../apis/scopes/testdata/example-scope3.yaml | 14 - .../scopeNodesExample/scopeNodes.json | 179 ----- .../testdata/scopeNodesExample/scopes.json | 95 --- 29 files changed, 289 insertions(+), 2667 deletions(-) create mode 100644 pkg/apimachinery/apis/common/v0alpha1/zz_generated.deepcopy.go delete mode 100644 pkg/apis/scope/v0alpha1/doc.go delete mode 100644 pkg/apis/scope/v0alpha1/register.go delete mode 100644 pkg/apis/scope/v0alpha1/types.go delete mode 100644 pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go delete mode 100644 pkg/apis/scope/v0alpha1/zz_generated.defaults.go delete mode 100644 pkg/apis/scope/v0alpha1/zz_generated.openapi.go delete mode 100644 pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list delete mode 100644 pkg/registry/apis/scope/find.go delete mode 100644 pkg/registry/apis/scope/find_scope_dashboards.go delete mode 100644 pkg/registry/apis/scope/find_test.go delete mode 100644 pkg/registry/apis/scope/register.go delete mode 100644 pkg/registry/apis/scope/storage.go delete mode 100644 pkg/tests/apis/scopes/scope_nodes_example_test.go delete mode 100644 pkg/tests/apis/scopes/scopes_test.go delete mode 100644 pkg/tests/apis/scopes/testdata/example-scope-dashboard-binding-abc.yaml delete mode 100644 pkg/tests/apis/scopes/testdata/example-scope-dashboard-binding-xyz.yaml delete mode 100644 pkg/tests/apis/scopes/testdata/example-scope.yaml delete mode 100644 pkg/tests/apis/scopes/testdata/example-scope2.yaml delete mode 100644 pkg/tests/apis/scopes/testdata/example-scope3.yaml delete mode 100644 pkg/tests/apis/scopes/testdata/scopeNodesExample/scopeNodes.json delete mode 100644 pkg/tests/apis/scopes/testdata/scopeNodesExample/scopes.json diff --git a/go.work.sum b/go.work.sum index 76d7190d1b2..5ef1c177e66 100644 --- a/go.work.sum +++ b/go.work.sum @@ -617,6 +617,11 @@ github.com/grafana/cog v0.0.23/go.mod h1:jrS9indvWuDs60RHEZpLaAkmZdgyoLKMOEUT0ji github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak= github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90= github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= +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/go.mod h1:dHhFF484qs1cmdIShKCB3kl+tMJyc4yuwgTQ3Afz37o= +github.com/grafana/grafana/apps/investigations v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:ygFcJP2McdSeMJVj/3YrKafZMc/lZBsp54HO51MtJYw= +github.com/grafana/grafana/apps/playlist v0.0.0-20250220164708-c8d4ff28a450/go.mod h1:KKIsWpbv88Lwwcvdjon73zFL7vNJvuXLtsSoUjJErTw= +github.com/grafana/grafana/pkg/build v0.0.0-20250220114259-be81314e2118/go.mod h1:STVpVboMYeBAfyn6Zw6XHhTHqUxzMy7pzRiVgk1l0W0= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P2qggOAHTj/GCZfoLBle3OvNSYh1VkRBU= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= diff --git a/pkg/apimachinery/apis/common/v0alpha1/doc.go b/pkg/apimachinery/apis/common/v0alpha1/doc.go index 8dbffd26f90..695a6afa690 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/doc.go +++ b/pkg/apimachinery/apis/common/v0alpha1/doc.go @@ -1,3 +1,4 @@ +// +k8s:deepcopy-gen=package // +k8s:openapi-gen=true // +k8s:defaulter-gen=TypeMeta // +groupName=common.grafana.app diff --git a/pkg/apimachinery/apis/common/v0alpha1/types.go b/pkg/apimachinery/apis/common/v0alpha1/types.go index 63ea94a8a96..8ccc08c62a0 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/types.go +++ b/pkg/apimachinery/apis/common/v0alpha1/types.go @@ -1,5 +1,9 @@ package v0alpha1 +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + // Similar to // https://dev-k8sref-io.web.app/docs/common-definitions/objectreference-/ // ObjectReference contains enough information to let you inspect or modify the referred object. @@ -15,3 +19,41 @@ type ObjectReference struct { // APIVersion is the version of the API group that contains the referred object. APIVersion string `json:"apiVersion,omitempty"` } + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type Scope struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ScopeSpec `json:"spec,omitempty"` +} + +type ScopeSpec struct { + Title string `json:"title"` + Description string `json:"description"` + + // +listType=atomic + Filters []ScopeFilter `json:"filters"` +} + +type ScopeFilter struct { + Key string `json:"key"` + Value string `json:"value"` + // Values is used for operators that require multiple values (e.g. one-of and not-one-of). + Values []string `json:"values,omitempty"` + Operator FilterOperator `json:"operator"` +} + +// Type of the filter operator. +// +enum +type FilterOperator string + +// Defines values for FilterOperator. +const ( + FilterOperatorEquals FilterOperator = "equals" + FilterOperatorNotEquals FilterOperator = "not-equals" + FilterOperatorRegexMatch FilterOperator = "regex-match" + FilterOperatorRegexNotMatch FilterOperator = "regex-not-match" + FilterOperatorOneOf FilterOperator = "one-of" + FilterOperatorNotOneOf FilterOperator = "not-one-of" +) diff --git a/pkg/apimachinery/apis/common/v0alpha1/zz_generated.deepcopy.go b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..8a3334e3634 --- /dev/null +++ b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.deepcopy.go @@ -0,0 +1,99 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectReference) DeepCopyInto(out *ObjectReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectReference. +func (in *ObjectReference) DeepCopy() *ObjectReference { + if in == nil { + return nil + } + out := new(ObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Scope) DeepCopyInto(out *Scope) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Scope. +func (in *Scope) DeepCopy() *Scope { + if in == nil { + return nil + } + out := new(Scope) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Scope) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScopeFilter) DeepCopyInto(out *ScopeFilter) { + *out = *in + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeFilter. +func (in *ScopeFilter) DeepCopy() *ScopeFilter { + if in == nil { + return nil + } + out := new(ScopeFilter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScopeSpec) DeepCopyInto(out *ScopeSpec) { + *out = *in + if in.Filters != nil { + in, out := &in.Filters, &out.Filters + *out = make([]ScopeFilter, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeSpec. +func (in *ScopeSpec) DeepCopy() *ScopeSpec { + if in == nil { + return nil + } + out := new(ScopeSpec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go index 2c5933d1790..8ddb9f8aecf 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go +++ b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go @@ -16,6 +16,9 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ObjectReference": schema_apimachinery_apis_common_v0alpha1_ObjectReference(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Scope": schema_apimachinery_apis_common_v0alpha1_Scope(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeFilter": schema_apimachinery_apis_common_v0alpha1_ScopeFilter(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeSpec": schema_apimachinery_apis_common_v0alpha1_ScopeSpec(ref), "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured": Unstructured{}.OpenAPIDefinition(), "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref), "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref), @@ -118,6 +121,144 @@ func schema_apimachinery_apis_common_v0alpha1_ObjectReference(ref common.Referen } } +func schema_apimachinery_apis_common_v0alpha1_Scope(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeSpec"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_apimachinery_apis_common_v0alpha1_ScopeFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "key": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "values": { + SchemaProps: spec.SchemaProps{ + Description: "Values is used for operators that require multiple values (e.g. one-of and not-one-of).", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "operator": { + SchemaProps: spec.SchemaProps{ + Description: "Possible enum values:\n - `\"equals\"`\n - `\"not-equals\"`\n - `\"not-one-of\"`\n - `\"one-of\"`\n - `\"regex-match\"`\n - `\"regex-not-match\"`", + Default: "", + Type: []string{"string"}, + Format: "", + Enum: []interface{}{"equals", "not-equals", "not-one-of", "one-of", "regex-match", "regex-not-match"}, + }, + }, + }, + Required: []string{"key", "value", "operator"}, + }, + }, + } +} + +func schema_apimachinery_apis_common_v0alpha1_ScopeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "filters": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeFilter"), + }, + }, + }, + }, + }, + }, + Required: []string{"title", "description", "filters"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeFilter"}, + } +} + func schema_pkg_apis_meta_v1_APIGroup(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi_violation_exceptions.list index caca0df1147..e4491e5ef86 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,3 +1,4 @@ +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1,ScopeFilter,Values API rule violation: names_match,github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1,Unstructured,Object API rule violation: names_match,k8s.io/apimachinery/pkg/apis/meta/v1,APIResourceList,APIResources API rule violation: names_match,k8s.io/apimachinery/pkg/apis/meta/v1,Duration,Duration diff --git a/pkg/apis/scope/v0alpha1/doc.go b/pkg/apis/scope/v0alpha1/doc.go deleted file mode 100644 index f9f43a0ea38..00000000000 --- a/pkg/apis/scope/v0alpha1/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// +k8s:deepcopy-gen=package -// +k8s:openapi-gen=true -// +k8s:defaulter-gen=TypeMeta -// +groupName=scope.grafana.app - -package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/scope/v0alpha1" diff --git a/pkg/apis/scope/v0alpha1/register.go b/pkg/apis/scope/v0alpha1/register.go deleted file mode 100644 index e4c514c062d..00000000000 --- a/pkg/apis/scope/v0alpha1/register.go +++ /dev/null @@ -1,139 +0,0 @@ -package v0alpha1 - -import ( - "fmt" - "time" - - "github.com/grafana/grafana/pkg/apimachinery/utils" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -const ( - GROUP = "scope.grafana.app" - VERSION = "v0alpha1" - APIVERSION = GROUP + "/" + VERSION -) - -var ScopeResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "scopes", "scope", "Scope", - func() runtime.Object { return &Scope{} }, - func() runtime.Object { return &ScopeList{} }, - utils.TableColumns{ - Definition: []metav1.TableColumnDefinition{ - {Name: "Name", Type: "string", Format: "name"}, - {Name: "Created At", Type: "date"}, - {Name: "Title", Type: "string"}, - {Name: "Filters", Type: "array"}, - }, - Reader: func(obj any) ([]interface{}, error) { - m, ok := obj.(*Scope) - if !ok { - return nil, fmt.Errorf("expected scope") - } - return []interface{}{ - m.Name, - m.CreationTimestamp.UTC().Format(time.RFC3339), - m.Spec.Title, - m.Spec.Filters, - }, nil - }, - }, // default table converter -) - -var ScopeDashboardBindingResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "scopedashboardbindings", "scopedashboardbinding", "ScopeDashboardBinding", - func() runtime.Object { return &ScopeDashboardBinding{} }, - func() runtime.Object { return &ScopeDashboardBindingList{} }, - utils.TableColumns{ - Definition: []metav1.TableColumnDefinition{ - {Name: "Name", Type: "string", Format: "name"}, - {Name: "Created At", Type: "date"}, - {Name: "Dashboard", Type: "string"}, - {Name: "Scope", Type: "string"}, - }, - Reader: func(obj any) ([]interface{}, error) { - m, ok := obj.(*ScopeDashboardBinding) - if !ok { - return nil, fmt.Errorf("expected scope dashboard binding") - } - return []interface{}{ - m.Name, - m.CreationTimestamp.UTC().Format(time.RFC3339), - m.Spec.Dashboard, - m.Spec.Scope, - }, nil - }, - }, -) - -var ScopeNodeResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "scopenodes", "scopenode", "ScopeNode", - func() runtime.Object { return &ScopeNode{} }, - func() runtime.Object { return &ScopeNodeList{} }, - utils.TableColumns{ - Definition: []metav1.TableColumnDefinition{ - {Name: "Name", Type: "string", Format: "name"}, - {Name: "Created At", Type: "date"}, - {Name: "Title", Type: "string"}, - {Name: "Parent Name", Type: "string"}, - {Name: "Node Type", Type: "string"}, - {Name: "Link Type", Type: "string"}, - {Name: "Link ID", Type: "string"}, - }, - Reader: func(obj any) ([]interface{}, error) { - m, ok := obj.(*ScopeNode) - if !ok { - return nil, fmt.Errorf("expected scope node") - } - return []interface{}{ - m.Name, - m.CreationTimestamp.UTC().Format(time.RFC3339), - m.Spec.Title, - m.Spec.ParentName, - m.Spec.NodeType, - m.Spec.LinkType, - m.Spec.LinkID, - }, nil - }, - }, // default table converter -) - -var ( - // SchemeGroupVersion is group version used to register these objects - SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} - InternalGroupVersion = schema.GroupVersion{Group: GROUP, Version: runtime.APIVersionInternal} - - // SchemaBuilder is used by standard codegen - SchemeBuilder runtime.SchemeBuilder - localSchemeBuilder = &SchemeBuilder - AddToScheme = localSchemeBuilder.AddToScheme -) - -func init() { - localSchemeBuilder.Register(func(s *runtime.Scheme) error { - return AddKnownTypes(SchemeGroupVersion, s) - }) -} - -// Adds the list of known types to the given scheme. -func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error { - scheme.AddKnownTypes(gv, - &Scope{}, - &ScopeList{}, - &ScopeDashboardBinding{}, - &ScopeDashboardBindingList{}, - &ScopeNode{}, - &ScopeNodeList{}, - &FindScopeNodeChildrenResults{}, - &FindScopeDashboardBindingsResults{}, - ) - //metav1.AddToGroupVersion(scheme, gv) - return nil -} - -// Resource takes an unqualified resource and returns a Group qualified GroupResource -func Resource(resource string) schema.GroupResource { - return SchemeGroupVersion.WithResource(resource).GroupResource() -} diff --git a/pkg/apis/scope/v0alpha1/types.go b/pkg/apis/scope/v0alpha1/types.go deleted file mode 100644 index c7ed0657ab5..00000000000 --- a/pkg/apis/scope/v0alpha1/types.go +++ /dev/null @@ -1,167 +0,0 @@ -package v0alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -/* -Please keep pkg/promlib/models/query.go and pkg/promlib/models/scope.go in sync -with this file until this package is out of the grafana/grafana module. -*/ - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type Scope struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ScopeSpec `json:"spec,omitempty"` -} - -type ScopeSpec struct { - Title string `json:"title"` - Description string `json:"description"` - - // +listType=atomic - Filters []ScopeFilter `json:"filters"` -} - -type ScopeFilter struct { - Key string `json:"key"` - Value string `json:"value"` - // Values is used for operators that require multiple values (e.g. one-of and not-one-of). - Values []string `json:"values,omitempty"` - Operator FilterOperator `json:"operator"` -} - -// Type of the filter operator. -// +enum -type FilterOperator string - -// Defines values for FilterOperator. -const ( - FilterOperatorEquals FilterOperator = "equals" - FilterOperatorNotEquals FilterOperator = "not-equals" - FilterOperatorRegexMatch FilterOperator = "regex-match" - FilterOperatorRegexNotMatch FilterOperator = "regex-not-match" - FilterOperatorOneOf FilterOperator = "one-of" - FilterOperatorNotOneOf FilterOperator = "not-one-of" -) - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ScopeList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []Scope `json:"items,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ScopeDashboardBinding struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ScopeDashboardBindingSpec `json:"spec,omitempty"` - Status ScopeDashboardBindingStatus `json:"status,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ScopeDashboardBindingList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []ScopeDashboardBinding `json:"items,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type FindScopeDashboardBindingsResults struct { - metav1.TypeMeta `json:",inline"` - - Items []ScopeDashboardBinding `json:"items,omitempty"` - Message string `json:"message,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ScopeNode struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ScopeNodeSpec `json:"spec,omitempty"` -} - -type ScopeDashboardBindingSpec struct { - Dashboard string `json:"dashboard"` - Scope string `json:"scope"` -} - -// Type of the item. -// +enum -// ScopeDashboardBindingStatus contains derived information about a ScopeDashboardBinding. -type ScopeDashboardBindingStatus struct { - // DashboardTitle should be populated and update from the dashboard - DashboardTitle string `json:"dashboardTitle"` - - // Groups is used for the grouping of dashboards that are suggested based - // on a scope. The source of truth for this information has not been - // determined yet. - Groups []string `json:"groups,omitempty"` - - // DashboardTitleConditions is a list of conditions that are used to determine if the dashboard title is valid. - // +optional - // +listType=map - // +listMapKey=type - DashboardTitleConditions []metav1.Condition `json:"dashboardTitleConditions,omitempty"` - - // DashboardTitleConditions is a list of conditions that are used to determine if the list of groups is valid. - // +optional - // +listType=map - // +listMapKey=type - GroupsConditions []metav1.Condition `json:"groupsConditions,omitempty"` -} - -type NodeType string - -// Defines values for ItemType. -const ( - NodeTypeContainer NodeType = "container" - NodeTypeLeaf NodeType = "leaf" -) - -// Type of the item. -// +enum -type LinkType string - -// Defines values for ItemType. -const ( - LinkTypeScope LinkType = "scope" -) - -type ScopeNodeSpec struct { - //+optional - ParentName string `json:"parentName,omitempty"` - - NodeType NodeType `json:"nodeType"` // container | leaf - - Title string `json:"title"` - Description string `json:"description,omitempty"` - DisableMultiSelect bool `json:"disableMultiSelect"` - - LinkType LinkType `json:"linkType,omitempty"` // scope (later more things) - LinkID string `json:"linkId,omitempty"` // the k8s name - // ?? should this be a slice of links -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ScopeNodeList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []ScopeNode `json:"items,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type FindScopeNodeChildrenResults struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []ScopeNode `json:"items,omitempty"` -} diff --git a/pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go deleted file mode 100644 index 73a9723640e..00000000000 --- a/pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go +++ /dev/null @@ -1,370 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by deepcopy-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FindScopeDashboardBindingsResults) DeepCopyInto(out *FindScopeDashboardBindingsResults) { - *out = *in - out.TypeMeta = in.TypeMeta - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ScopeDashboardBinding, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FindScopeDashboardBindingsResults. -func (in *FindScopeDashboardBindingsResults) DeepCopy() *FindScopeDashboardBindingsResults { - if in == nil { - return nil - } - out := new(FindScopeDashboardBindingsResults) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FindScopeDashboardBindingsResults) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FindScopeNodeChildrenResults) DeepCopyInto(out *FindScopeNodeChildrenResults) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ScopeNode, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FindScopeNodeChildrenResults. -func (in *FindScopeNodeChildrenResults) DeepCopy() *FindScopeNodeChildrenResults { - if in == nil { - return nil - } - out := new(FindScopeNodeChildrenResults) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FindScopeNodeChildrenResults) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Scope) DeepCopyInto(out *Scope) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Scope. -func (in *Scope) DeepCopy() *Scope { - if in == nil { - return nil - } - out := new(Scope) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Scope) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ScopeDashboardBinding) DeepCopyInto(out *ScopeDashboardBinding) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - in.Status.DeepCopyInto(&out.Status) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDashboardBinding. -func (in *ScopeDashboardBinding) DeepCopy() *ScopeDashboardBinding { - if in == nil { - return nil - } - out := new(ScopeDashboardBinding) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ScopeDashboardBinding) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ScopeDashboardBindingList) DeepCopyInto(out *ScopeDashboardBindingList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ScopeDashboardBinding, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDashboardBindingList. -func (in *ScopeDashboardBindingList) DeepCopy() *ScopeDashboardBindingList { - if in == nil { - return nil - } - out := new(ScopeDashboardBindingList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ScopeDashboardBindingList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ScopeDashboardBindingSpec) DeepCopyInto(out *ScopeDashboardBindingSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDashboardBindingSpec. -func (in *ScopeDashboardBindingSpec) DeepCopy() *ScopeDashboardBindingSpec { - if in == nil { - return nil - } - out := new(ScopeDashboardBindingSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ScopeDashboardBindingStatus) DeepCopyInto(out *ScopeDashboardBindingStatus) { - *out = *in - if in.Groups != nil { - in, out := &in.Groups, &out.Groups - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.DashboardTitleConditions != nil { - in, out := &in.DashboardTitleConditions, &out.DashboardTitleConditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.GroupsConditions != nil { - in, out := &in.GroupsConditions, &out.GroupsConditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDashboardBindingStatus. -func (in *ScopeDashboardBindingStatus) DeepCopy() *ScopeDashboardBindingStatus { - if in == nil { - return nil - } - out := new(ScopeDashboardBindingStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ScopeFilter) DeepCopyInto(out *ScopeFilter) { - *out = *in - if in.Values != nil { - in, out := &in.Values, &out.Values - *out = make([]string, len(*in)) - copy(*out, *in) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeFilter. -func (in *ScopeFilter) DeepCopy() *ScopeFilter { - if in == nil { - return nil - } - out := new(ScopeFilter) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ScopeList) DeepCopyInto(out *ScopeList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Scope, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeList. -func (in *ScopeList) DeepCopy() *ScopeList { - if in == nil { - return nil - } - out := new(ScopeList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ScopeList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ScopeNode) DeepCopyInto(out *ScopeNode) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeNode. -func (in *ScopeNode) DeepCopy() *ScopeNode { - if in == nil { - return nil - } - out := new(ScopeNode) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ScopeNode) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ScopeNodeList) DeepCopyInto(out *ScopeNodeList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ScopeNode, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeNodeList. -func (in *ScopeNodeList) DeepCopy() *ScopeNodeList { - if in == nil { - return nil - } - out := new(ScopeNodeList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ScopeNodeList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ScopeNodeSpec) DeepCopyInto(out *ScopeNodeSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeNodeSpec. -func (in *ScopeNodeSpec) DeepCopy() *ScopeNodeSpec { - if in == nil { - return nil - } - out := new(ScopeNodeSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ScopeSpec) DeepCopyInto(out *ScopeSpec) { - *out = *in - if in.Filters != nil { - in, out := &in.Filters, &out.Filters - *out = make([]ScopeFilter, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeSpec. -func (in *ScopeSpec) DeepCopy() *ScopeSpec { - if in == nil { - return nil - } - out := new(ScopeSpec) - in.DeepCopyInto(out) - return out -} diff --git a/pkg/apis/scope/v0alpha1/zz_generated.defaults.go b/pkg/apis/scope/v0alpha1/zz_generated.defaults.go deleted file mode 100644 index 238fc2f4edc..00000000000 --- a/pkg/apis/scope/v0alpha1/zz_generated.defaults.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by defaulter-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// RegisterDefaults adds defaulters functions to the given scheme. -// Public to allow building arbitrary schemes. -// All generated defaulters are covering - they call all nested defaulters. -func RegisterDefaults(scheme *runtime.Scheme) error { - return nil -} diff --git a/pkg/apis/scope/v0alpha1/zz_generated.openapi.go b/pkg/apis/scope/v0alpha1/zz_generated.openapi.go deleted file mode 100644 index 4018d2cf024..00000000000 --- a/pkg/apis/scope/v0alpha1/zz_generated.openapi.go +++ /dev/null @@ -1,661 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by openapi-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - common "k8s.io/kube-openapi/pkg/common" - spec "k8s.io/kube-openapi/pkg/validation/spec" -) - -func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { - return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.FindScopeDashboardBindingsResults": schema_pkg_apis_scope_v0alpha1_FindScopeDashboardBindingsResults(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.FindScopeNodeChildrenResults": schema_pkg_apis_scope_v0alpha1_FindScopeNodeChildrenResults(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.Scope": schema_pkg_apis_scope_v0alpha1_Scope(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBinding": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBinding(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingList": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingList(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingSpec(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingStatus": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingStatus(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeFilter": schema_pkg_apis_scope_v0alpha1_ScopeFilter(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeList": schema_pkg_apis_scope_v0alpha1_ScopeList(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeNode": schema_pkg_apis_scope_v0alpha1_ScopeNode(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeNodeList": schema_pkg_apis_scope_v0alpha1_ScopeNodeList(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeNodeSpec": schema_pkg_apis_scope_v0alpha1_ScopeNodeSpec(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeSpec": schema_pkg_apis_scope_v0alpha1_ScopeSpec(ref), - } -} - -func schema_pkg_apis_scope_v0alpha1_FindScopeDashboardBindingsResults(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBinding"), - }, - }, - }, - }, - }, - "message": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBinding"}, - } -} - -func schema_pkg_apis_scope_v0alpha1_FindScopeNodeChildrenResults(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeNode"), - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeNode", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_scope_v0alpha1_Scope(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeSpec"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBinding(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec"), - }, - }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingStatus"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec", "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBinding"), - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBinding", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "dashboard": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "scope": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"dashboard", "scope"}, - }, - }, - } -} - -func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Type of the item. ScopeDashboardBindingStatus contains derived information about a ScopeDashboardBinding.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "dashboardTitle": { - SchemaProps: spec.SchemaProps{ - Description: "DashboardTitle should be populated and update from the dashboard", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "groups": { - SchemaProps: spec.SchemaProps{ - Description: "Groups is used for the grouping of dashboards that are suggested based on a scope. The source of truth for this information has not been determined yet.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "dashboardTitleConditions": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-map-keys": []interface{}{ - "type", - }, - "x-kubernetes-list-type": "map", - }, - }, - SchemaProps: spec.SchemaProps{ - Description: "DashboardTitleConditions is a list of conditions that are used to determine if the dashboard title is valid.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"), - }, - }, - }, - }, - }, - "groupsConditions": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-map-keys": []interface{}{ - "type", - }, - "x-kubernetes-list-type": "map", - }, - }, - SchemaProps: spec.SchemaProps{ - Description: "DashboardTitleConditions is a list of conditions that are used to determine if the list of groups is valid.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"), - }, - }, - }, - }, - }, - }, - Required: []string{"dashboardTitle"}, - }, - }, - Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Condition"}, - } -} - -func schema_pkg_apis_scope_v0alpha1_ScopeFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "key": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "value": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "values": { - SchemaProps: spec.SchemaProps{ - Description: "Values is used for operators that require multiple values (e.g. one-of and not-one-of).", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "operator": { - SchemaProps: spec.SchemaProps{ - Description: "Possible enum values:\n - `\"equals\"`\n - `\"not-equals\"`\n - `\"not-one-of\"`\n - `\"one-of\"`\n - `\"regex-match\"`\n - `\"regex-not-match\"`", - Default: "", - Type: []string{"string"}, - Format: "", - Enum: []interface{}{"equals", "not-equals", "not-one-of", "one-of", "regex-match", "regex-not-match"}, - }, - }, - }, - Required: []string{"key", "value", "operator"}, - }, - }, - } -} - -func schema_pkg_apis_scope_v0alpha1_ScopeList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.Scope"), - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.Scope", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_scope_v0alpha1_ScopeNode(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeNodeSpec"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeNodeSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_scope_v0alpha1_ScopeNodeList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeNode"), - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeNode", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_scope_v0alpha1_ScopeNodeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "parentName": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "nodeType": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "title": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "description": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "disableMultiSelect": { - SchemaProps: spec.SchemaProps{ - Default: false, - Type: []string{"boolean"}, - Format: "", - }, - }, - "linkType": { - SchemaProps: spec.SchemaProps{ - Description: "Possible enum values:\n - `\"scope\"`", - Type: []string{"string"}, - Format: "", - Enum: []interface{}{"scope"}, - }, - }, - "linkId": { - SchemaProps: spec.SchemaProps{ - Description: "scope (later more things)", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"nodeType", "title", "disableMultiSelect"}, - }, - }, - } -} - -func schema_pkg_apis_scope_v0alpha1_ScopeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "title": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "description": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "filters": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-type": "atomic", - }, - }, - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeFilter"), - }, - }, - }, - }, - }, - }, - Required: []string{"title", "description", "filters"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeFilter"}, - } -} diff --git a/pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list deleted file mode 100644 index b473e1d66f0..00000000000 --- a/pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ /dev/null @@ -1,4 +0,0 @@ -API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,FindScopeDashboardBindingsResults,Items -API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboardBindingStatus,Groups -API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeFilter,Values -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeNodeSpec,LinkID diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 0d3327725ab..5636df66eb3 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam" "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/query" - "github.com/grafana/grafana/pkg/registry/apis/scope" "github.com/grafana/grafana/pkg/registry/apis/userstorage" ) @@ -31,7 +30,6 @@ func ProvideRegistryServiceSink( _ *datasource.DataSourceAPIBuilder, _ *folders.FolderAPIBuilder, _ *iam.IdentityAccessManagementAPIBuilder, - _ *scope.ScopeAPIBuilder, _ *query.QueryAPIBuilder, _ *notifications.NotificationsAPIBuilder, _ *userstorage.UserStorageAPIBuilder, diff --git a/pkg/registry/apis/scope/find.go b/pkg/registry/apis/scope/find.go deleted file mode 100644 index 43cb057958a..00000000000 --- a/pkg/registry/apis/scope/find.go +++ /dev/null @@ -1,113 +0,0 @@ -package scope - -import ( - "context" - "fmt" - "net/http" - "strings" - - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/internalversion" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/rest" - - scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1" - "github.com/grafana/grafana/pkg/infra/log" -) - -var logger = log.New("find-scopenode") - -type findREST struct { - scopeNodeStorage *storage -} - -var ( - _ rest.Storage = (*findREST)(nil) - _ rest.SingularNameProvider = (*findREST)(nil) - _ rest.Connecter = (*findREST)(nil) - _ rest.Scoper = (*findREST)(nil) - _ rest.StorageMetadata = (*findREST)(nil) -) - -func (r *findREST) New() runtime.Object { - // This is added as the "ResponseType" regarless what ProducesObject() says :) - return &scope.FindScopeNodeChildrenResults{} -} - -func (r *findREST) Destroy() {} - -func (r *findREST) NamespaceScoped() bool { - return true -} - -func (r *findREST) GetSingularName() string { - return "FindScopeNodeChildrenResults" // Used for the -} - -func (r *findREST) ProducesMIMETypes(verb string) []string { - return []string{"application/json"} // and parquet! -} - -func (r *findREST) ProducesObject(verb string) interface{} { - return &scope.FindScopeNodeChildrenResults{} -} - -func (r *findREST) ConnectMethods() []string { - return []string{"GET"} -} - -func (r *findREST) NewConnectOptions() (runtime.Object, bool, string) { - return nil, false, "" // true means you can use the trailing path as a variable -} - -func (r *findREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - // See: /pkg/services/apiserver/builder/helper.go#L34 - // The name is set with a rewriter hack - if name != "name" { - return nil, errors.NewNotFound(schema.GroupResource{}, name) - } - - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - parent := req.URL.Query().Get("parent") - query := req.URL.Query().Get("query") - results := &scope.FindScopeNodeChildrenResults{} - - raw, err := r.scopeNodeStorage.List(ctx, &internalversion.ListOptions{ - Limit: 10000, - }) - - if err != nil { - responder.Error(err) - return - } - - all, ok := raw.(*scope.ScopeNodeList) - - if !ok { - responder.Error(fmt.Errorf("expected ScopeNodeList")) - return - } - - for _, item := range all.Items { - filterAndAppendItem(item, parent, query, results) - } - - logger.FromContext(req.Context()).Debug("find scopenode", "raw", len(all.Items), "filtered", len(results.Items)) - - responder.Object(200, results) - }), nil -} - -func filterAndAppendItem(item scope.ScopeNode, parent string, query string, results *scope.FindScopeNodeChildrenResults) { - if parent != item.Spec.ParentName { - return // Someday this will have an index in raw storage on parentName - } - - // skip if query is passed and title doesn't contain the query. - if query != "" && !strings.Contains(item.Spec.Title, query) { - return - } - - results.Items = append(results.Items, item) -} diff --git a/pkg/registry/apis/scope/find_scope_dashboards.go b/pkg/registry/apis/scope/find_scope_dashboards.go deleted file mode 100644 index b33f091907e..00000000000 --- a/pkg/registry/apis/scope/find_scope_dashboards.go +++ /dev/null @@ -1,106 +0,0 @@ -package scope - -import ( - "context" - "fmt" - "net/http" - "slices" - "strings" - - scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/internalversion" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/rest" -) - -type findScopeDashboardsREST struct { - scopeDashboardStorage *storage -} - -var ( - _ rest.Storage = (*findScopeDashboardsREST)(nil) - _ rest.SingularNameProvider = (*findScopeDashboardsREST)(nil) - _ rest.Connecter = (*findScopeDashboardsREST)(nil) - _ rest.Scoper = (*findScopeDashboardsREST)(nil) - _ rest.StorageMetadata = (*findScopeDashboardsREST)(nil) -) - -func (f *findScopeDashboardsREST) New() runtime.Object { - return &scope.FindScopeDashboardBindingsResults{} -} - -func (f *findScopeDashboardsREST) Destroy() {} - -func (f *findScopeDashboardsREST) NamespaceScoped() bool { - return true -} - -func (f *findScopeDashboardsREST) GetSingularName() string { - return "FindScopeDashboardsResult" // not sure if this is actually used, but it is required to exist -} - -func (f *findScopeDashboardsREST) ProducesMIMETypes(verb string) []string { - return []string{"application/json"} // and parquet! -} - -func (f *findScopeDashboardsREST) ProducesObject(verb string) interface{} { - return &scope.FindScopeDashboardBindingsResults{} -} - -func (f *findScopeDashboardsREST) ConnectMethods() []string { - return []string{"GET"} -} - -func (f *findScopeDashboardsREST) NewConnectOptions() (runtime.Object, bool, string) { - return nil, false, "" // true means you can use the trailing path as a variable -} - -func (f *findScopeDashboardsREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - // See: /pkg/services/apiserver/builder/helper.go#L34 - // The name is set with a rewriter hack - if name != "name" { - return nil, errors.NewNotFound(schema.GroupResource{}, name) - } - - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - raw, err := f.scopeDashboardStorage.List(ctx, &internalversion.ListOptions{ - Limit: 10000, - }) - if err != nil { - w.WriteHeader(500) - return - } - all, ok := raw.(*scope.ScopeDashboardBindingList) - if !ok { - w.WriteHeader(500) - return - } - - scopes := req.URL.Query()["scope"] - results := &scope.FindScopeDashboardBindingsResults{ - Message: fmt.Sprintf("Find: %s", scopes), - Items: make([]scope.ScopeDashboardBinding, 0), - } - - // we can improve the performance by calling .List once per scope if they are index by labels. - // The API stays the same thou. - for _, item := range all.Items { - for _, s := range scopes { - if item.Spec.Scope == s { - results.Items = append(results.Items, item) - } - } - } - - // sort the dashboard lists based on dashboard title. - slices.SortFunc(results.Items, func(i, j scope.ScopeDashboardBinding) int { - return strings.Compare(i.Status.DashboardTitle, j.Status.DashboardTitle) - }) - - logger.FromContext(req.Context()).Debug("find scopedashboardbinding", "raw", len(all.Items), "filtered", len(results.Items), "scopeQueryParams", strings.Join(scopes, ",")) - - responder.Object(200, results) - }), nil -} diff --git a/pkg/registry/apis/scope/find_test.go b/pkg/registry/apis/scope/find_test.go deleted file mode 100644 index 0f9ce99b6e4..00000000000 --- a/pkg/registry/apis/scope/find_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package scope - -import ( - "testing" - - scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1" - "github.com/stretchr/testify/require" -) - -func TestFilterAndAppendItem(t *testing.T) { - tcs := []struct { - Description string - - ParentName string - Title string - - QueryParam string - ParentParam string - - ExpectedMatches int - }{ - { - Description: "Matching parent without query param", - ParentName: "ParentNumberOne", - Title: "item", - QueryParam: "", - ParentParam: "ParentNumberOne", - ExpectedMatches: 1, - }, - { - Description: "Not matching parent", - ParentName: "ParentNumberOne", - Title: "itemOne", - QueryParam: "itemTwo", - ParentParam: "ParentNumberTwo", - ExpectedMatches: 0, - }, - { - Description: "Matching parent and query param", - ParentName: "ParentNumberOne", - Title: "itemOne", - QueryParam: "itemOne", - ParentParam: "ParentNumberOne", - ExpectedMatches: 1, - }, - { - Description: "matching parent but not matching query param", - ParentName: "ParentNumberOne", - Title: "itemOne", - QueryParam: "itemTwo", - ParentParam: "ParentNumberOne", - ExpectedMatches: 0, - }, - } - - for _, tc := range tcs { - results := &scope.FindScopeNodeChildrenResults{} - item := scope.ScopeNode{ - Spec: scope.ScopeNodeSpec{ - ParentName: tc.ParentName, - Title: tc.Title, - Description: "item description", - NodeType: "item type", - LinkType: "item link type", - LinkID: "item link ID", - }, - } - filterAndAppendItem(item, tc.ParentParam, tc.QueryParam, results) - require.Equal(t, len(results.Items), tc.ExpectedMatches, tc.Description) - } -} diff --git a/pkg/registry/apis/scope/register.go b/pkg/registry/apis/scope/register.go deleted file mode 100644 index c1144f519a8..00000000000 --- a/pkg/registry/apis/scope/register.go +++ /dev/null @@ -1,224 +0,0 @@ -package scope - -import ( - "fmt" - - "github.com/prometheus/client_golang/prometheus" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/rest" - genericapiserver "k8s.io/apiserver/pkg/server" - "k8s.io/kube-openapi/pkg/common" - "k8s.io/kube-openapi/pkg/spec3" - "k8s.io/kube-openapi/pkg/validation/spec" - - scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1" - "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/services/featuremgmt" -) - -var _ builder.APIGroupBuilder = (*ScopeAPIBuilder)(nil) - -// This is used just so wire has something unique to return -type ScopeAPIBuilder struct{} - -func NewScopeAPIBuilder() *ScopeAPIBuilder { - return &ScopeAPIBuilder{} -} - -func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, reg prometheus.Registerer) *ScopeAPIBuilder { - if !featuremgmt.AnyEnabled(features, - featuremgmt.FlagScopeApi, - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - return nil // skip registration unless opting into experimental apis - } - builder := NewScopeAPIBuilder() - apiregistration.RegisterAPI(builder) - return builder -} - -func (b *ScopeAPIBuilder) GetAuthorizer() authorizer.Authorizer { - return nil // default authorizer is fine -} - -func (b *ScopeAPIBuilder) GetGroupVersion() schema.GroupVersion { - return scope.SchemeGroupVersion -} - -func (b *ScopeAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { - err := scope.AddToScheme(scheme) - if err != nil { - return err - } - - err = scheme.AddFieldLabelConversionFunc( - scope.ScopeResourceInfo.GroupVersionKind(), - func(label, value string) (string, string, error) { - fieldSet := SelectableScopeFields(&scope.Scope{}) - for key := range fieldSet { - if label == key { - return label, value, nil - } - } - return "", "", fmt.Errorf("field label not supported for %s: %s", scope.ScopeResourceInfo.GroupVersionKind(), label) - }, - ) - if err != nil { - return err - } - - err = scheme.AddFieldLabelConversionFunc( - scope.ScopeDashboardBindingResourceInfo.GroupVersionKind(), - func(label, value string) (string, string, error) { - fieldSet := SelectableScopeDashboardBindingFields(&scope.ScopeDashboardBinding{}) - for key := range fieldSet { - if label == key { - return label, value, nil - } - } - return "", "", fmt.Errorf("field label not supported for %s: %s", scope.ScopeDashboardBindingResourceInfo.GroupVersionKind(), label) - }, - ) - if err != nil { - return err - } - - err = scheme.AddFieldLabelConversionFunc( - scope.ScopeNodeResourceInfo.GroupVersionKind(), - func(label, value string) (string, string, error) { - fieldSet := SelectableScopeNodeFields(&scope.ScopeNode{}) - for key := range fieldSet { - if label == key { - return label, value, nil - } - } - return "", "", fmt.Errorf("field label not supported for %s: %s", scope.ScopeNodeResourceInfo.GroupVersionKind(), label) - }, - ) - if err != nil { - return err - } - - // This is required for --server-side apply - err = scope.AddKnownTypes(scope.InternalGroupVersion, scheme) - if err != nil { - return err - } - - // Only one version right now - return scheme.SetVersionPriority(scope.SchemeGroupVersion) -} - -func (b *ScopeAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { - scheme := opts.Scheme - optsGetter := opts.OptsGetter - - scopeResourceInfo := scope.ScopeResourceInfo - scopeDashboardResourceInfo := scope.ScopeDashboardBindingResourceInfo - scopeNodeResourceInfo := scope.ScopeNodeResourceInfo - - storage := map[string]rest.Storage{} - - scopeStorage, err := newScopeStorage(scheme, optsGetter) - if err != nil { - return err - } - storage[scopeResourceInfo.StoragePath()] = scopeStorage - - scopeDashboardStorage, scopedDashboardStatusStorage, err := newScopeDashboardBindingStorage(scheme, optsGetter) - if err != nil { - return err - } - storage[scopeDashboardResourceInfo.StoragePath()] = scopeDashboardStorage - storage[scopeDashboardResourceInfo.StoragePath()+"/status"] = scopedDashboardStatusStorage - - scopeNodeStorage, err := newScopeNodeStorage(scheme, optsGetter) - if err != nil { - return err - } - storage[scopeNodeResourceInfo.StoragePath()] = scopeNodeStorage - - // Adds a rest.Connector - // NOTE! the server has a hardcoded rewrite filter that fills in a name - // so the standard k8s plumbing continues to work - storage["scope_node_children"] = &findREST{scopeNodeStorage: scopeNodeStorage} - - // Adds a rest.Connector - // NOTE! the server has a hardcoded rewrite filter that fills in a name - // so the standard k8s plumbing continues to work - storage["scope_dashboard_bindings"] = &findScopeDashboardsREST{scopeDashboardStorage: scopeDashboardStorage} - - apiGroupInfo.VersionedResourcesStorageMap[scope.VERSION] = storage - return nil -} - -func (b *ScopeAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return scope.GetOpenAPIDefinitions -} - -func (b *ScopeAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { - // The plugin description - oas.Info.Description = "Grafana scopes" - - // The root api URL - root := "/apis/" + b.GetGroupVersion().String() + "/" - - // Add query parameters to the rest.Connector - sub := oas.Paths.Paths[root+"namespaces/{namespace}/scope_node_children/{name}"] - if sub != nil && sub.Get != nil { - sub.Parameters = []*spec3.Parameter{ - { - ParameterProps: spec3.ParameterProps{ - Name: "namespace", - In: "path", - Description: "object name and auth scope, such as for teams and projects", - Example: "default", - Required: true, - Schema: spec.StringProperty().UniqueValues(), - }, - }, - } - sub.Get.Description = "Navigate the scopes tree" - sub.Get.Parameters = []*spec3.Parameter{ - { - ParameterProps: spec3.ParameterProps{ - Name: "parent", - In: "query", - Description: "The parent scope node", - }, - }, - } - delete(oas.Paths.Paths, root+"namespaces/{namespace}/scope_node_children/{name}") - oas.Paths.Paths[root+"namespaces/{namespace}/find/scope_node_children"] = sub - } - - findDashboardPath := oas.Paths.Paths[root+"namespaces/{namespace}/scope_dashboard_bindings/{name}"] - if findDashboardPath != nil && sub.Get != nil { - sub.Parameters = []*spec3.Parameter{ - { - ParameterProps: spec3.ParameterProps{ - Name: "namespace", - In: "path", - Description: "object name and auth scope, such as for teams and projects", - Example: "default", - Required: true, - }, - }, - } - findDashboardPath.Get.Description = "find scope dashboard bindings that match any of the given scopes." - findDashboardPath.Get.Parameters = []*spec3.Parameter{ - { - ParameterProps: spec3.ParameterProps{ - Name: "scope", - In: "query", - Description: "A scope name (id) to match against, this parameter may be repeated", - }, - }, - } - delete(oas.Paths.Paths, root+"namespaces/{namespace}/scope_dashboard_bindings/{name}") - oas.Paths.Paths[root+"namespaces/{namespace}/find/scope_dashboard_bindings"] = findDashboardPath - } - - return oas, nil -} diff --git a/pkg/registry/apis/scope/storage.go b/pkg/registry/apis/scope/storage.go deleted file mode 100644 index 23b4d4c21f9..00000000000 --- a/pkg/registry/apis/scope/storage.go +++ /dev/null @@ -1,142 +0,0 @@ -package scope - -import ( - "fmt" - - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/generic" - genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" - apistore "k8s.io/apiserver/pkg/storage" - - scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" -) - -var _ grafanarest.Storage = (*storage)(nil) - -type storage struct { - *genericregistry.Store -} - -func newScopeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) { - resourceInfo := scope.ScopeResourceInfo - strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) - store := &genericregistry.Store{ - NewFunc: resourceInfo.NewFunc, - NewListFunc: resourceInfo.NewListFunc, - KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()), - KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()), - PredicateFunc: Matcher, - DefaultQualifiedResource: resourceInfo.GroupResource(), - SingularQualifiedResource: resourceInfo.SingularGroupResource(), - TableConvertor: resourceInfo.TableConverter(), - CreateStrategy: strategy, - UpdateStrategy: strategy, - DeleteStrategy: strategy, - } - options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs} - if err := store.CompleteWithOptions(options); err != nil { - return nil, err - } - return &storage{Store: store}, nil -} - -func newScopeDashboardBindingStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, *grafanaregistry.StatusREST, error) { - resourceInfo := scope.ScopeDashboardBindingResourceInfo - strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) - - store := &genericregistry.Store{ - NewFunc: resourceInfo.NewFunc, - NewListFunc: resourceInfo.NewListFunc, - KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()), - KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()), - PredicateFunc: Matcher, - DefaultQualifiedResource: resourceInfo.GroupResource(), - SingularQualifiedResource: resourceInfo.SingularGroupResource(), - TableConvertor: resourceInfo.TableConverter(), - CreateStrategy: strategy, - UpdateStrategy: strategy, - DeleteStrategy: strategy, - } - options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs} - if err := store.CompleteWithOptions(options); err != nil { - return nil, nil, err - } - - statusStrategy := grafanaregistry.NewStatusStrategy(scheme, resourceInfo.GroupVersion()) - statusREST := grafanaregistry.NewStatusREST(store, statusStrategy) - return &storage{Store: store}, statusREST, nil -} - -func newScopeNodeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) { - resourceInfo := scope.ScopeNodeResourceInfo - strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) - - store := &genericregistry.Store{ - NewFunc: resourceInfo.NewFunc, - NewListFunc: resourceInfo.NewListFunc, - KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()), - KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()), - PredicateFunc: Matcher, - DefaultQualifiedResource: resourceInfo.GroupResource(), - SingularQualifiedResource: resourceInfo.SingularGroupResource(), - TableConvertor: resourceInfo.TableConverter(), - CreateStrategy: strategy, - UpdateStrategy: strategy, - DeleteStrategy: strategy, - } - options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs} - if err := store.CompleteWithOptions(options); err != nil { - return nil, err - } - return &storage{Store: store}, nil -} - -func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { - if s, ok := obj.(*scope.Scope); ok { - return labels.Set(s.Labels), SelectableScopeFields(s), nil - } - if s, ok := obj.(*scope.ScopeDashboardBinding); ok { - return labels.Set(s.Labels), SelectableScopeDashboardBindingFields(s), nil - } - if s, ok := obj.(*scope.ScopeNode); ok { - return labels.Set(s.Labels), SelectableScopeNodeFields(s), nil - } - return nil, nil, fmt.Errorf("not a scope or ScopeDashboardBinding object") -} - -// Matcher returns a generic.SelectionPredicate that matches on label and field selectors. -func Matcher(label labels.Selector, field fields.Selector) apistore.SelectionPredicate { - return apistore.SelectionPredicate{ - Label: label, - Field: field, - GetAttrs: GetAttrs, - } -} - -func SelectableScopeFields(obj *scope.Scope) fields.Set { - return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{ - "spec.title": obj.Spec.Title, - }) -} - -func SelectableScopeDashboardBindingFields(obj *scope.ScopeDashboardBinding) fields.Set { - return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{ - "spec.scope": obj.Spec.Scope, - }) -} - -func SelectableScopeNodeFields(obj *scope.ScopeNode) fields.Set { - parentName := "" - - if obj != nil { - parentName = obj.Spec.ParentName - } - - return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{ - "spec.parentName": parentName, - }) -} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 22734abadbb..c98c4da67be 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam" "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/query" - "github.com/grafana/grafana/pkg/registry/apis/scope" "github.com/grafana/grafana/pkg/registry/apis/service" "github.com/grafana/grafana/pkg/registry/apis/userstorage" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" @@ -42,7 +41,6 @@ var WireSet = wire.NewSet( provisioning.RegisterAPIService, service.RegisterAPIService, query.RegisterAPIService, - scope.RegisterAPIService, notifications.RegisterAPIService, userstorage.RegisterAPIService, ) diff --git a/pkg/tests/apis/scopes/scope_nodes_example_test.go b/pkg/tests/apis/scopes/scope_nodes_example_test.go deleted file mode 100644 index 71846d207df..00000000000 --- a/pkg/tests/apis/scopes/scope_nodes_example_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package scopes - -import ( - "context" - "encoding/json" - "os" - "testing" - - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/tests/apis" - "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -func TestIntegrationScopeNodesExample(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode.") - } - - ctx := context.Background() - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, // required for experimental APIs - EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service - }, - }) - - scopeClient := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - Namespace: "default", // actually org1 - GVR: schema.GroupVersionResource{ - Group: "scope.grafana.app", Version: "v0alpha1", Resource: "scopes", - }, - }) - - scopeNodesClient := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - Namespace: "default", // actually org1 - GVR: schema.GroupVersionResource{ - Group: "scope.grafana.app", Version: "v0alpha1", Resource: "scopenodes", - }, - }) - - createOptions := metav1.CreateOptions{FieldValidation: "Strict"} - - t.Run("Create scopes", func(t *testing.T) { - ul := jsonListToUnstructuredList(t, "testdata/scopeNodesExample/scopes.json") - - for _, item := range ul.Items { - _, err := scopeClient.Resource.Create(ctx, &item, createOptions) - require.NoError(t, err) - } - - found, err := scopeClient.Resource.List(ctx, metav1.ListOptions{}) - require.NoError(t, err) - require.Len(t, found.Items, 5) - }) - - t.Run("Create scopeNodes", func(t *testing.T) { - ul := jsonListToUnstructuredList(t, "testdata/scopeNodesExample/scopeNodes.json") - - for _, item := range ul.Items { - _, err := scopeNodesClient.Resource.Create(ctx, &item, createOptions) - require.NoError(t, err) - } - - found, err := scopeNodesClient.Resource.List(ctx, metav1.ListOptions{}) - require.NoError(t, err) - require.Len(t, found.Items, 12) - }) -} - -func jsonListToUnstructuredList(t *testing.T, fname string) (ul unstructured.UnstructuredList) { - // nolint:gosec - f, err := os.ReadFile(fname) - require.NoError(t, err) - - err = json.Unmarshal(f, &ul) - require.NoError(t, err) - return -} diff --git a/pkg/tests/apis/scopes/scopes_test.go b/pkg/tests/apis/scopes/scopes_test.go deleted file mode 100644 index bf8fd293286..00000000000 --- a/pkg/tests/apis/scopes/scopes_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package scopes - -import ( - "context" - "encoding/json" - "strings" - "testing" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/tests/apis" - "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/grafana/grafana/pkg/tests/testsuite" -) - -func TestMain(m *testing.M) { - testsuite.Run(m) -} - -func TestIntegrationScopes(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := context.Background() - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - EnableFeatureToggles: []string{ - featuremgmt.FlagScopeApi, // Required to register the API - }, - }) - - t.Run("Check discovery client", func(t *testing.T) { - disco := helper.NewDiscoveryClient() - resources, err := disco.ServerResourcesForGroupVersion("scope.grafana.app/v0alpha1") - require.NoError(t, err) - - v1Disco, err := json.MarshalIndent(resources, "", " ") - require.NoError(t, err) - //fmt.Printf("%s", string(v1Disco)) - - require.JSONEq(t, `{ - "kind": "APIResourceList", - "apiVersion": "v1", - "groupVersion": "scope.grafana.app/v0alpha1", - "resources": [ - { - "name": "scope_dashboard_bindings", - "singularName": "FindScopeDashboardsResult", - "namespaced": true, - "kind": "FindScopeDashboardBindingsResults", - "verbs": [ - "get" - ] - }, - { - "name": "scope_node_children", - "singularName": "FindScopeNodeChildrenResults", - "namespaced": true, - "kind": "FindScopeNodeChildrenResults", - "verbs": [ - "get" - ] - }, - { - "name": "scopedashboardbindings", - "singularName": "scopedashboardbinding", - "namespaced": true, - "kind": "ScopeDashboardBinding", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ] - }, - { - "name": "scopedashboardbindings/status", - "singularName": "", - "namespaced": true, - "kind": "ScopeDashboardBinding", - "verbs": [ - "get", - "patch", - "update" - ] - }, - { - "name": "scopenodes", - "singularName": "scopenode", - "namespaced": true, - "kind": "ScopeNode", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ] - }, - { - "name": "scopes", - "singularName": "scope", - "namespaced": true, - "kind": "Scope", - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update", - "watch" - ] - } - ] - }`, string(v1Disco)) - }) - - t.Run("Check create and list", func(t *testing.T) { - // Scope create+get - scopeClient := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - Namespace: "default", // actually org1 - GVR: schema.GroupVersionResource{ - Group: "scope.grafana.app", Version: "v0alpha1", Resource: "scopes", - }, - }) - createOptions := metav1.CreateOptions{FieldValidation: "Strict"} - - s0, err := scopeClient.Resource.Create(ctx, - helper.LoadYAMLOrJSONFile("testdata/example-scope.yaml"), - createOptions, - ) - - require.NoError(t, err) - require.Equal(t, "example", s0.GetName()) - s1, err := scopeClient.Resource.Get(ctx, "example", metav1.GetOptions{}) - require.NoError(t, err) - require.Equal(t, - mustNestedString(s0.Object, "spec", "title"), - mustNestedString(s1.Object, "spec", "title"), - ) - - _, err = scopeClient.Resource.Create(ctx, - helper.LoadYAMLOrJSONFile("testdata/example-scope2.yaml"), - createOptions, - ) - require.NoError(t, err) - - // Name length test - scope3 := helper.LoadYAMLOrJSONFile("testdata/example-scope3.yaml") - - // Name too long (>253) - scope3.SetName(strings.Repeat("0", 254)) - _, err = scopeClient.Resource.Create(ctx, - scope3, - createOptions, - ) - require.Error(t, err) - - // Maximum allowed length for name (253) - scope3.SetName(strings.Repeat("0", 253)) - _, err = scopeClient.Resource.Create(ctx, - scope3, - createOptions, - ) - require.NoError(t, err) - - // Field Selector test - found, err := scopeClient.Resource.List(ctx, metav1.ListOptions{ - FieldSelector: "spec.title=foo-scope", - }) - require.NoError(t, err) - require.Len(t, found.Items, 1) - require.Equal(t, - "example2", - mustNestedString(found.Items[0].Object, "metadata", "name"), - ) - - // Create bindings - scopeDashboardBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - Namespace: "default", // actually org1 - GVR: schema.GroupVersionResource{ - Group: "scope.grafana.app", Version: "v0alpha1", Resource: "scopedashboardbindings", - }, - }) - _, err = scopeDashboardBindingClient.Resource.Create(ctx, - helper.LoadYAMLOrJSONFile("testdata/example-scope-dashboard-binding-abc.yaml"), - createOptions, - ) - require.NoError(t, err) - _, err = scopeDashboardBindingClient.Resource.Create(ctx, - helper.LoadYAMLOrJSONFile("testdata/example-scope-dashboard-binding-xyz.yaml"), - createOptions, - ) - require.NoError(t, err) - - found, err = scopeDashboardBindingClient.Resource.List(ctx, metav1.ListOptions{}) - require.NoError(t, err) - require.Len(t, found.Items, 2) - }) -} - -func mustNestedString(obj map[string]interface{}, fields ...string) string { - v, _, _ := unstructured.NestedString(obj, fields...) - return v -} diff --git a/pkg/tests/apis/scopes/testdata/example-scope-dashboard-binding-abc.yaml b/pkg/tests/apis/scopes/testdata/example-scope-dashboard-binding-abc.yaml deleted file mode 100644 index 190d006cca6..00000000000 --- a/pkg/tests/apis/scopes/testdata/example-scope-dashboard-binding-abc.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: scope.grafana.app/v0alpha1 -kind: ScopeDashboardBinding -metadata: - name: example_abc -spec: - scope: example - dashboard: abc -status: - dashboardTitle: "Example Dashboard ABC" - groups: ["group1", "group2"] \ No newline at end of file diff --git a/pkg/tests/apis/scopes/testdata/example-scope-dashboard-binding-xyz.yaml b/pkg/tests/apis/scopes/testdata/example-scope-dashboard-binding-xyz.yaml deleted file mode 100644 index 4d289e8d8c6..00000000000 --- a/pkg/tests/apis/scopes/testdata/example-scope-dashboard-binding-xyz.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: scope.grafana.app/v0alpha1 -kind: ScopeDashboardBinding -metadata: - name: example_xyz -spec: - scope: example - dashboard: xyz -status: - dashboardTitle: "Example Dashboard XYZ" - groups: ["group2", "group3"] \ No newline at end of file diff --git a/pkg/tests/apis/scopes/testdata/example-scope.yaml b/pkg/tests/apis/scopes/testdata/example-scope.yaml deleted file mode 100644 index b5a7eccf009..00000000000 --- a/pkg/tests/apis/scopes/testdata/example-scope.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: scope.grafana.app/v0alpha1 -kind: Scope -metadata: - name: example -spec: - title: bar-scope - description: Longer description for a scope - filters: - - key: aaa - operator: equals - value: bbb - - key: ccc - operator: not-equals - value: ddd diff --git a/pkg/tests/apis/scopes/testdata/example-scope2.yaml b/pkg/tests/apis/scopes/testdata/example-scope2.yaml deleted file mode 100644 index aa6e38513b3..00000000000 --- a/pkg/tests/apis/scopes/testdata/example-scope2.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: scope.grafana.app/v0alpha1 -kind: Scope -metadata: - name: example2 -spec: - title: foo-scope - description: Longer description for a scope - filters: - - key: aaa - operator: equals - value: zzz - - key: ccc - operator: not-equals - value: yyy diff --git a/pkg/tests/apis/scopes/testdata/example-scope3.yaml b/pkg/tests/apis/scopes/testdata/example-scope3.yaml deleted file mode 100644 index e1f36c39d87..00000000000 --- a/pkg/tests/apis/scopes/testdata/example-scope3.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: scope.grafana.app/v0alpha1 -kind: Scope -metadata: - name: example-long -spec: - title: baz-scope - description: Longer description for a scope - filters: - - key: aaa - operator: equals - value: eee - - key: ccc - operator: not-equals - value: fff diff --git a/pkg/tests/apis/scopes/testdata/scopeNodesExample/scopeNodes.json b/pkg/tests/apis/scopes/testdata/scopeNodesExample/scopeNodes.json deleted file mode 100644 index b5163d5b5c6..00000000000 --- a/pkg/tests/apis/scopes/testdata/scopeNodesExample/scopeNodes.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "kind": "List", - "items": [ - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "applications" - }, - "spec": { - "description": "Application Scopes", - "title": "Applications", - "nodeType": "container" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "applications-clusters" - }, - "spec": { - "description": "Application/Clusters Scopes", - "title": "Clusters", - "nodeType": "container", - "parentName": "applications", - "linkId": "indexHelperCluster", - "linkType": "scope" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "clusters" - }, - "spec": { - "description": "Cluster Scopes", - "title": "Clusters", - "nodeType": "container", - "linkId": "indexHelperCluster", - "linkType": "scope" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "clusters-application" - }, - "spec": { - "description": "Clusters/Application Scopes", - "title": "Applications", - "nodeType": "container", - "parentName": "clusters" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "applications-slothPictureFactory" - }, - "spec": { - "description": "slothPictureFactory", - "title": "slothPictureFactory", - "nodeType": "leaf", - "parentName": "applications", - "linkId": "slothPictureFactory", - "linkType": "scope" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "applications-slothVoteTracker" - }, - "spec": { - "description": "slothVoteTracker", - "title": "slothVoteTracker", - "nodeType": "leaf", - "parentName": "applications", - "linkId": "slothVoteTracker", - "linkType": "scope" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "applications-clusters-slothClusterNorth" - }, - "spec": { - "description": "slothClusterNorth", - "title": "slothClusterNorth", - "nodeType": "leaf", - "parentName": "applications-clusters", - "linkId": "slothClusterNorth", - "linkType": "scope" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "applications-clusters-slothClusterSouth" - }, - "spec": { - "description": "slothClusterSouth", - "title": "slothClusterSouth", - "nodeType": "leaf", - "parentName": "applications-clusters", - "linkId": "slothClusterSouth", - "linkType": "scope" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "clusters-slothClusterNorth" - }, - "spec": { - "description": "slothClusterNorth", - "title": "slothClusterNorth", - "nodeType": "leaf", - "parentName": "clusters", - "linkId": "slothClusterNorth", - "linkType": "scope" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "clusters-slothClusterSouth" - }, - "spec": { - "description": "slothClusterSouth", - "title": "slothClusterSouth", - "nodeType": "leaf", - "parentName": "clusters", - "linkId": "slothClusterSouth", - "linkType": "scope" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "clusters-applications-slothPictureFactory" - }, - "spec": { - "description": "slothPictureFactory", - "title": "slothPictureFactory", - "nodeType": "leaf", - "parentName": "clusters-applications", - "linkId": "slothPictureFactory", - "linkType": "scope" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "ScopeNode", - "metadata": { - "name": "clusters-applications-slothVoteTracker" - }, - "spec": { - "description": "slothVoteTracker", - "title": "slothVoteTracker", - "nodeType": "leaf", - "parentName": "clusters-applications", - "linkId": "slothVoteTracker", - "linkType": "scope" - } - } - ] -} \ No newline at end of file diff --git a/pkg/tests/apis/scopes/testdata/scopeNodesExample/scopes.json b/pkg/tests/apis/scopes/testdata/scopeNodesExample/scopes.json deleted file mode 100644 index 13b3d2bb4f8..00000000000 --- a/pkg/tests/apis/scopes/testdata/scopeNodesExample/scopes.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "kind": "List", - "items": [ - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "Scope", - "metadata": { - "name": "slothClusterNorth" - }, - "spec": { - "description": "slothClusterNorth", - "filters": [ - { - "key": "cluster", - "operator": "equals", - "value": "slothClusterNorth" - } - ], - "title": "slothClusterNorth" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "Scope", - "metadata": { - "name": "slothClusterSouth" - }, - "spec": { - "description": "slothClusterSouth", - "filters": [ - { - "key": "cluster", - "operator": "equals", - "value": "slothClusterSouth" - } - ], - "title": "slothClusterSouth" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "Scope", - "metadata": { - "name": "slothPictureFactory" - }, - "spec": { - "description": "slothPictureFactory", - "filters": [ - { - "key": "app", - "operator": "equals", - "value": "slothPictureFactory" - } - ], - "title": "slothPictureFactory" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "Scope", - "metadata": { - "name": "slothVoteTracker" - }, - "spec": { - "description": "slothVoteTracker", - "filters": [ - { - "key": "app", - "operator": "equals", - "value": "slothVoteTracker" - } - ], - "title": "slothVoteTracker" - } - }, - { - "apiVersion": "scope.grafana.app/v0alpha1", - "kind": "Scope", - "metadata": { - "name": "indexHelperCluster" - }, - "spec": { - "description": "redundant label filter but makes queries faster", - "filters": [ - { - "key": "indexHelper", - "operator": "equals", - "value": "cluster" - } - ], - "title": "Cluster Index Helper" - } - } - ] -} \ No newline at end of file From 5974c197cb39af6f143f18d8000ee2a079cd2085 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Fri, 21 Feb 2025 08:57:01 +0000 Subject: [PATCH 778/894] Settings: Updating docs for removal of settings (#100956) * wip * linting * update word --- .../roles-and-permissions/_index.md | 25 +--------------- .../index.md | 4 +-- .../manage-dashboard-permissions/index.md | 22 +------------- .../grant-editor-admin-permissions/index.md | 30 ------------------- docs/sources/developers/http_api/team.md | 2 -- .../explore/get-started-with-explore.md | 6 +--- .../setup-grafana/configure-grafana/_index.md | 8 +++++ 7 files changed, 13 insertions(+), 84 deletions(-) delete mode 100644 docs/sources/administration/user-management/server-user-management/grant-editor-admin-permissions/index.md diff --git a/docs/sources/administration/roles-and-permissions/_index.md b/docs/sources/administration/roles-and-permissions/_index.md index 134482723b4..87457d1d0fa 100644 --- a/docs/sources/administration/roles-and-permissions/_index.md +++ b/docs/sources/administration/roles-and-permissions/_index.md @@ -121,29 +121,6 @@ For more information about assigning dashboard folder permissions, refer to [Gra For more information about assigning dashboard permissions, refer to [Grant dashboard permissions]({{< relref "../user-management/manage-dashboard-permissions/#grant-dashboard-permissions" >}}). -## Editors with administrator permissions - -If you have access to the Grafana server, you can modify the default editor role so that editors can use administrator permissions to manage dashboard folders, dashboards, and teams that they create. - -{{% admonition type="note" %}} -This permission does not allow editors to manage folders, dashboards, and teams that they do not create. -{{% /admonition %}} - -This setting can be used to enable self-organizing teams to administer their own dashboards. - -For more information about assigning administrator permissions to editors, refer to [Grant editors administrator permissions]({{< relref "../user-management/server-user-management/grant-editor-admin-permissions/" >}}). - -## Viewers with dashboard preview and Explore permissions - -If you have access to the Grafana server, you can modify the default viewer role so that viewers can: - -- Edit and preview dashboards, but cannot save their changes or create new dashboards. -- Access and use [Explore]({{< relref "../../explore" >}}). - -Extending the viewer role is useful for public Grafana installations where you want anonymous users to be able to edit panels and queries, but not be able to save or create new dashboards. - -For more information about assigning dashboard preview permissions to viewers, refer to [Enable viewers to preview dashboards and use Explore]({{< relref "../user-management/manage-dashboard-permissions/#enable-viewers-to-edit-but-not-save-dashboards-and-use-explore" >}}). - ## Teams and permissions A team is a group of users within an organization that have common dashboard and data source permission needs. For example, instead of assigning five users access to the same dashboard, you can create a team that consists of those users and assign dashboard permissions to the team. A user can belong to multiple teams. @@ -153,7 +130,7 @@ You can assign a team member one of the following permissions: - **Member**: Includes the user as a member of the team. Members do not have team administrator privileges. - **Admin**: Administrators have permission to manage various aspects of the team, including team membership, permissions, and settings. -Because teams exist inside an organization, the organization administrator can manage all teams. When the `editors_can_admin` setting is enabled, editors can create teams and manage teams that they create. For more information about the `editors_can_admin` setting, refer to [Grant editors administrator permissions]({{< relref "../user-management/server-user-management/grant-editor-admin-permissions/" >}}). +Because teams exist inside an organization, the organization administrator can manage all teams. For details on managing teams, see [Team management]({{< relref "../team-management/" >}}). diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index f651becaa4e..2c328c749b8 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -58,8 +58,8 @@ The following tables list permissions associated with basic and fixed roles. | ------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | Grafana Admin | `basic_grafana_admin` | `fixed:roles:reader`
          `fixed:roles:writer`
          `fixed:users:reader`
          `fixed:users:writer`
          `fixed:org.users:reader`
          `fixed:org.users:writer`
          `fixed:ldap:reader`
          `fixed:ldap:writer`
          `fixed:stats:reader`
          `fixed:settings:reader`
          `fixed:settings:writer`
          `fixed:provisioning:writer`
          `fixed:organization:reader`
          `fixed:organization:maintainer`
          `fixed:licensing:reader`
          `fixed:licensing:writer`
          `fixed:datasources.caching:reader`
          `fixed:datasources.caching:writer`
          `fixed:dashboards.insights:reader`
          `fixed:datasources.insights:reader`
          `fixed:plugins:maintainer`
          `fixed:authentication.config:writer`
          `fixed:library.panels:creator`
          `fixed:library.panels:reader`
          `fixed:library.panels:general.reader`
          `fixed:library.panels:writer`
          `fixed:library.panels:general.writer`
          `fixed:groupsync:writer`
          `fixed:migrationassistant:migrator` | Default [Grafana server administrator](/docs/grafana//administration/roles-and-permissions/#grafana-server-administrators) assignments. | | Admin | `basic_admin` | `fixed:reports:reader`
          `fixed:reports:writer`
          `fixed:datasources:reader`
          `fixed:datasources:writer`
          `fixed:organization:writer`
          `fixed:datasources.permissions:reader`
          `fixed:datasources.permissions:writer`
          `fixed:teams:writer`
          `fixed:dashboards:reader`
          `fixed:dashboards:writer`
          `fixed:dashboards.permissions:reader`
          `fixed:dashboards.permissions:writer`
          `fixed:dashboards.public:writer`
          `fixed:folders:reader`
          `fixed:folders:writer`
          `fixed:folders.permissions:reader`
          `fixed:folders.permissions:writer`
          `fixed:alerting:writer`
          `fixed:apikeys:reader`
          `fixed:apikeys:writer`
          `fixed:alerting.provisioning.secrets:reader`
          `fixed:alerting.provisioning:writer`
          `fixed:datasources.caching:reader`
          `fixed:datasources.caching:writer`
          `fixed:dashboards.insights:reader`
          `fixed:datasources.insights:reader`
          `fixed:plugins:writer`
          `fixed:library.panels:creator`
          `fixed:library.panels:reader`
          `fixed:library.panels:general.reader`
          `fixed:library.panels:writer`
          `fixed:library.panels:general.writer`
          `fixed:alerting.provisioning.status:writer`
          `fixed:groupsync:writer` | Default [Grafana organization administrator](ref:rbac-basic-roles) assignments. | -| Editor | `basic_editor` | `fixed:datasources:explorer`
          `fixed:dashboards:creator`
          `fixed:folders:creator`
          `fixed:annotations:writer`
          `fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled
          `fixed:alerting:writer`
          `fixed:dashboards.insights:reader`
          `fixed:datasources.insights:reader`
          `fixed:library.panels:creator`
          `fixed:library.panels:general.reader`
          `fixed:library.panels:general.writer`
          `fixed:alerting.provisioning.status:writer` | Default [Editor](ref:rbac-basic-roles) assignments. | -| Viewer | `basic_viewer` | `fixed:datasources.id:reader`
          `fixed:organization:reader`
          `fixed:annotations:reader`
          `fixed:annotations.dashboard:writer`
          `fixed:alerting:reader`
          `fixed:plugins.app:reader`
          `fixed:dashboards.insights:reader`
          `fixed:datasources.insights:reader`
          `fixed:library.panels:general.reader`
          `fixed:datasources:explorer` if the `viewers_can_edit` configuration flag is enabled | Default [Viewer](ref:rbac-basic-roles) assignments. | +| Editor | `basic_editor` | `fixed:datasources:explorer`
          `fixed:dashboards:creator`
          `fixed:folders:creator`
          `fixed:annotations:writer`
          `fixed:alerting:writer`
          `fixed:dashboards.insights:reader`
          `fixed:datasources.insights:reader`
          `fixed:library.panels:creator`
          `fixed:library.panels:general.reader`
          `fixed:library.panels:general.writer`
          `fixed:alerting.provisioning.status:writer` | Default [Editor](ref:rbac-basic-roles) assignments. | +| Viewer | `basic_viewer` | `fixed:datasources.id:reader`
          `fixed:organization:reader`
          `fixed:annotations:reader`
          `fixed:annotations.dashboard:writer`
          `fixed:alerting:reader`
          `fixed:plugins.app:reader`
          `fixed:dashboards.insights:reader`
          `fixed:datasources.insights:reader`
          `fixed:library.panels:general.reader` | Default [Viewer](ref:rbac-basic-roles) assignments. | | No Basic Role | n/a | | Default [No Basic Role](ref:rbac-basic-roles) | ## Fixed role definitions diff --git a/docs/sources/administration/user-management/manage-dashboard-permissions/index.md b/docs/sources/administration/user-management/manage-dashboard-permissions/index.md index e87d0193fcc..b75f675cc5e 100644 --- a/docs/sources/administration/user-management/manage-dashboard-permissions/index.md +++ b/docs/sources/administration/user-management/manage-dashboard-permissions/index.md @@ -63,27 +63,7 @@ Grant dashboard permissions when you want to restrict or enhance dashboard acces 1. Select the user, service account, team, or role. 1. Select the permission and click **Save**. -## Enable viewers to edit (but not save) dashboards and use Explore - -By default, the viewer organization role does not allow viewers to create dashboards or use the Explore feature. However, by modifying a configuration setting, you can allow viewers to edit a panel and make changes to a dashboard but not save those changes. This setting also enables viewers to use the Explore feature. - -This modification is useful for public Grafana installations where you want anonymous users to be able to edit panels and queries but not save or create new dashboards. - -### Before you begin - -- Ensure that you have access to the Grafana server - -**To enable viewers to preview dashboards and use Explore**: - -1. Open the Grafana configuration file. - - For more information about the Grafana configuration file and its location, refer to [Configuration]({{< relref "../../../setup-grafana/configure-grafana/" >}}). - -1. Locate the `viewers_can_edit` parameter. -1. Set the `viewers_can_edit` value to `true`. -1. Save your changes and restart Grafana. - -## Edit dashboard permissions +# Edit dashboard permissions Edit dashboard permissions when you are want to enhance or restrict a user's access to a dashboard. For more information about dashboard permissions, refer to [Dashboard permissions]({{< relref "../../roles-and-permissions/#dashboard-permissions" >}}). diff --git a/docs/sources/administration/user-management/server-user-management/grant-editor-admin-permissions/index.md b/docs/sources/administration/user-management/server-user-management/grant-editor-admin-permissions/index.md deleted file mode 100644 index cfefd775975..00000000000 --- a/docs/sources/administration/user-management/server-user-management/grant-editor-admin-permissions/index.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -aliases: - - ../../manage-users-and-permissions/manage-server-users/grant-editor-admin-permissions/ -labels: - products: - - enterprise - - oss -title: Grant editors team creator permissions -weight: 60 ---- - -# Grant editors team creator permissions - -By default, the editor organization role does not allow editors to creator and manage teams. You can allow them to do so using the `editors_can_admin` configuration option. - -When `editors_can_admin` is enabled, users with the Editor role in an organization can create teams, and they are Administrators of the teams they create. To learn more about team permissions, refer to [Team management]({{< relref "../../../team-management/" >}}). - -## Before you begin - -- Ensure that you have access to the Grafana server - -**To enable editors with team creator permissions**: - -1. Log in to the Grafana server and open the Grafana configuration file. - - For more information about the Grafana configuration file and its location, refer to [Configuration]({{< relref "../../../../setup-grafana/configure-grafana/" >}}). - -1. Locate the `editors_can_admin` parameter. -1. Set the `editors_can_admin` value to `true`. -1. Save your changes and restart the Grafana server. diff --git a/docs/sources/developers/http_api/team.md b/docs/sources/developers/http_api/team.md index e2c4cc654b4..e94d80be650 100644 --- a/docs/sources/developers/http_api/team.md +++ b/docs/sources/developers/http_api/team.md @@ -26,8 +26,6 @@ Access to these API endpoints is restricted as follows: - All authenticated users are able to view details of teams they are a member of. - Organization Admins are able to manage all teams and team members. -- If you enable `editors_can_admin` configuration flag, then Organization Editors can create teams and manage teams where they are Admin. - - If you enable `editors_can_admin` configuration flag, Editors can find out whether a team that they are not members of exists by trying to create a team with the same name. > If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "/docs/grafana/latest/administration/roles-and-permissions/access-control/custom-role-actions-scopes" >}}) for more information. diff --git a/docs/sources/explore/get-started-with-explore.md b/docs/sources/explore/get-started-with-explore.md index 7e78e4e80c3..b25f855bac9 100644 --- a/docs/sources/explore/get-started-with-explore.md +++ b/docs/sources/explore/get-started-with-explore.md @@ -38,14 +38,10 @@ Watch the following video to get started using Explore: ## Before you begin -In order to access Explore, you must have either the `editor` or `administrator` role, unless the [`viewers_can_edit` option](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#viewers_can_edit) is enabled. Refer to [Role and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions/) for more information on what each role can access. +In order to access Explore, you must have either the `editor` or `administrator` basic role or the `data sources explore` role. Refer to [Role and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions/) for more information on what each role can access. Refer to [Role-based access control (RBAC)](https://grafana.com/docs/grafana//administration/roles-and-permissions/access-control/) in Grafana Enterprise to understand how you can manage Explore with role-based permissions. -{{< admonition type="note" >}} -If you are using Grafana Cloud, open a [support ticket in the Cloud Portal](https://grafana.com/auth/sign-in) to enable the `viewers_can_edit` option. -{{< /admonition >}} - ## Explore elements Explore consists of a toolbar, outline, query editor, the ability to add multiple queries, a query history and a query inspector. diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 527fc60d7a5..babe3619b35 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -950,10 +950,18 @@ If you manage users externally you can replace the user invite button for organi #### `viewers_can_edit` +{{< admonition type="note" >}} +This option is deprecated - assign your viewers as editors, if you are using RBAC assign the data sources explorer role to your users. +{{< /admonition >}} + Viewers can access and use [Explore]({{< relref "../../explore" >}}) and perform temporary edits on panels in dashboards they have access to. They cannot save their changes. Default is `false`. #### `editors_can_admin` +{{< admonition type="note" >}} +This option is deprecated - assign your editors as admins, if you are using RBAC assign the team creator role to your users. +{{< /admonition >}} + Editors can administrate dashboards, folders and teams they create. Default is `false`. From f0f8bb890c54bea74b52c4497172e1ef71a5d694 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:03:25 +0200 Subject: [PATCH 779/894] Remove `menuShouldBlockScroll` react-select flag (#100950) remove select flag - menuShouldBlockScroll --- packages/grafana-ui/src/components/Select/SelectBase.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 6a173554360..75c2b10681a 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -255,7 +255,6 @@ export function SelectBase({ menuIsOpen: isOpen, menuPlacement: menuPlacement === 'auto' && closeToBottom ? 'top' : menuPlacement, menuPosition, - menuShouldBlockScroll: true, menuPortalTarget: menuShouldPortal && typeof document !== 'undefined' ? document.body : undefined, menuShouldScrollIntoView: false, onBlur, From 6eca5c09dfb3e45a84bbb7e9a25ad3e4f8d79c26 Mon Sep 17 00:00:00 2001 From: Edward Qian Date: Fri, 21 Feb 2025 05:33:12 -0500 Subject: [PATCH 780/894] Prometheus: Remove query assistant and related components (#100669) * remove query assistant related components * remove export statement * remove grafana/llm from prometheus packages * remove extra package * revert unintended change * incorrect handling of managedPluginsInstall merge deletion * update yarn.lock * linting fix * linting fix --- .../feature-toggles/index.md | 1 - .../various-suite/prometheus-editor.spec.ts | 13 - e2e/various-suite/prometheus-editor.spec.ts | 13 - .../src/types/featureToggles.gen.ts | 1 - packages/grafana-prometheus/package.json | 1 - packages/grafana-prometheus/src/index.ts | 1 - .../components/PromQueryBuilder.test.tsx | 24 +- .../components/PromQueryBuilder.tsx | 40 +- .../components/promQail/PromQail.test.tsx | 148 ----- .../components/promQail/PromQail.tsx | 616 ------------------ .../promQail/QueryAssistantButton.test.tsx | 51 -- .../promQail/QueryAssistantButton.tsx | 86 --- .../promQail/QuerySuggestionContainer.tsx | 102 --- .../promQail/QuerySuggestionItem.tsx | 322 --------- .../querybuilder/components/promQail/index.ts | 1 - .../components/promQail/prompts.ts | 115 ---- .../promQail/resources/AI_Logo_bw.svg | 4 - .../promQail/resources/AI_Logo_color.svg | 11 - .../components/promQail/state/helpers.test.ts | 73 --- .../components/promQail/state/helpers.ts | 415 ------------ .../components/promQail/state/state.ts | 44 -- .../components/promQail/state/templates.ts | 342 ---------- .../querybuilder/components/promQail/types.ts | 18 - pkg/services/featuremgmt/registry.go | 7 - pkg/services/featuremgmt/toggles-gitlog.csv | 1 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 16 - yarn.lock | 1 - 29 files changed, 2 insertions(+), 2470 deletions(-) delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.test.tsx delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.tsx delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/QueryAssistantButton.test.tsx delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/QueryAssistantButton.tsx delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/QuerySuggestionContainer.tsx delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/QuerySuggestionItem.tsx delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/index.ts delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/prompts.ts delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/resources/AI_Logo_bw.svg delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/resources/AI_Logo_color.svg delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/state/helpers.test.ts delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/state/helpers.ts delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/state/state.ts delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/state/templates.ts delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/promQail/types.ts 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 716f2ace6f9..c1fcf849447 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -174,7 +174,6 @@ Experimental features might be changed or removed without prior notice. | `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | | `queryServiceFromUI` | Routes requests to the new query service | | `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | -| `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | | `prometheusCodeModeMetricNamesSearch` | Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names | | `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | | `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | diff --git a/e2e/old-arch/various-suite/prometheus-editor.spec.ts b/e2e/old-arch/various-suite/prometheus-editor.spec.ts index d04d502a706..64993979a9a 100644 --- a/e2e/old-arch/various-suite/prometheus-editor.spec.ts +++ b/e2e/old-arch/various-suite/prometheus-editor.spec.ts @@ -159,19 +159,6 @@ describe('Prometheus query editor', () => { e2e.components.DataSource.Prometheus.queryEditor.builder.metricsExplorer().should('exist'); }); - - // NEED TO COMPLETE QUEY ADVISOR WORK OR FIGURE OUT HOW TO ENABLE EXPERIMENTAL FEATURE TOGGLES - // it('should have a query advisor when enabled with feature toggle', () => { - // cy.window().then((win) => { - // win.localStorage.setItem('grafana.featureToggles', 'prometheusPromQAIL=0'); - - // navigateToEditor('Builder', 'prometheusBuilder'); - - // getResources(); - - // e2e.components.DataSource.Prometheus.queryEditor.builder.queryAdvisor().should('exist'); - // }); - // }); }); }); diff --git a/e2e/various-suite/prometheus-editor.spec.ts b/e2e/various-suite/prometheus-editor.spec.ts index 09ae51e4f7f..e1746edf6b7 100644 --- a/e2e/various-suite/prometheus-editor.spec.ts +++ b/e2e/various-suite/prometheus-editor.spec.ts @@ -159,19 +159,6 @@ describe.skip('Prometheus query editor', () => { e2e.components.DataSource.Prometheus.queryEditor.builder.metricsExplorer().should('exist'); }); - - // NEED TO COMPLETE QUEY ADVISOR WORK OR FIGURE OUT HOW TO ENABLE EXPERIMENTAL FEATURE TOGGLES - // it('should have a query advisor when enabled with feature toggle', () => { - // cy.window().then((win) => { - // win.localStorage.setItem('grafana.featureToggles', 'prometheusPromQAIL=0'); - - // navigateToEditor('Builder', 'prometheusBuilder'); - - // getResources(); - - // e2e.components.DataSource.Prometheus.queryEditor.builder.queryAdvisor().should('exist'); - // }); - // }); }); }); diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 2b2f4befd4f..84091bf6c14 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -118,7 +118,6 @@ export interface FeatureToggles { recoveryThreshold?: boolean; lokiStructuredMetadata?: boolean; cachingOptimizeSerializationMemoryUsage?: boolean; - prometheusPromQAIL?: boolean; prometheusCodeModeMetricNamesSearch?: boolean; addFieldFromCalculationStatFunctions?: boolean; alertmanagerRemoteSecondary?: boolean; diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 17a88423cb2..4232a385393 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -40,7 +40,6 @@ "@floating-ui/react": "0.27.3", "@grafana/data": "11.6.0-pre", "@grafana/e2e-selectors": "11.6.0-pre", - "@grafana/llm": "0.12.0", "@grafana/plugin-ui": "0.10.1", "@grafana/runtime": "11.6.0-pre", "@grafana/schema": "11.6.0-pre", diff --git a/packages/grafana-prometheus/src/index.ts b/packages/grafana-prometheus/src/index.ts index 2e7bc495150..80fa0e5130d 100644 --- a/packages/grafana-prometheus/src/index.ts +++ b/packages/grafana-prometheus/src/index.ts @@ -55,7 +55,6 @@ export { PromQueryEditorSelector } from './querybuilder/components/PromQueryEdit export { PromQueryLegendEditor } from './querybuilder/components/PromQueryLegendEditor'; export { QueryPreview } from './querybuilder/components/QueryPreview'; export { MetricsModal } from './querybuilder/components/metrics-modal/MetricsModal'; -export { PromQail } from './querybuilder/components/promQail/PromQail'; // SRC/ // Main export diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx index db74ff98655..8e58f495963 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx @@ -11,7 +11,7 @@ import { QueryHint, TimeRange, } from '@grafana/data'; -import { config, TemplateSrv } from '@grafana/runtime'; +import { TemplateSrv } from '@grafana/runtime'; import { PrometheusDatasource } from '../../datasource'; import PromQlLanguageProvider from '../../language_provider'; @@ -108,28 +108,6 @@ describe('PromQueryBuilder', () => { await waitFor(() => expect(datasource.getVariables).toBeCalled()); }); - it('checks if the LLM plugin is enabled when the `prometheusPromQAIL` feature is enabled', async () => { - jest.replaceProperty(config, 'featureToggles', { - prometheusPromQAIL: true, - }); - const mockIsLLMPluginEnabled = jest.fn(); - mockIsLLMPluginEnabled.mockResolvedValue(true); - jest.spyOn(require('./promQail/state/helpers'), 'isLLMPluginEnabled').mockImplementation(mockIsLLMPluginEnabled); - setup(); - await waitFor(() => expect(mockIsLLMPluginEnabled).toHaveBeenCalledTimes(1)); - }); - - it('does not check if the LLM plugin is enabled when the `prometheusPromQAIL` feature is disabled', async () => { - jest.replaceProperty(config, 'featureToggles', { - prometheusPromQAIL: false, - }); - const mockIsLLMPluginEnabled = jest.fn(); - mockIsLLMPluginEnabled.mockResolvedValue(true); - jest.spyOn(require('./promQail/state/helpers'), 'isLLMPluginEnabled').mockImplementation(mockIsLLMPluginEnabled); - setup(); - await waitFor(() => expect(mockIsLLMPluginEnabled).toHaveBeenCalledTimes(0)); - }); - // it('tries to load labels when metric selected', async () => { const { languageProvider } = setup(); diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx index d0812e1b0f0..11c79f5932c 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx @@ -1,12 +1,10 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx import { css } from '@emotion/css'; -import { memo, useEffect, useState } from 'react'; +import { memo, useState } from 'react'; import { DataSourceApi, PanelData } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { EditorRow } from '@grafana/plugin-ui'; -import { config } from '@grafana/runtime'; -import { Drawer } from '@grafana/ui'; import { PrometheusDatasource } from '../../datasource'; import promqlGrammar from '../../promql'; @@ -24,9 +22,6 @@ import { PromVisualQuery } from '../types'; import { MetricsLabelsSection } from './MetricsLabelsSection'; import { NestedQueryList } from './NestedQueryList'; import { EXPLAIN_LABEL_FILTER_CONTENT } from './PromQueryBuilderExplained'; -import { PromQail } from './promQail/PromQail'; -import { QueryAssistantButton } from './promQail/QueryAssistantButton'; -import { isLLMPluginEnabled } from './promQail/state/helpers'; export interface PromQueryBuilderProps { query: PromVisualQuery; @@ -40,37 +35,13 @@ export interface PromQueryBuilderProps { export const PromQueryBuilder = memo((props) => { const { datasource, query, onChange, onRunQuery, data, showExplain } = props; const [highlightedOp, setHighlightedOp] = useState(); - const [showDrawer, setShowDrawer] = useState(false); - const [llmAppEnabled, updateLlmAppEnabled] = useState(false); - const { prometheusPromQAIL } = config.featureToggles; // AI/ML + Prometheus const lang = { grammar: promqlGrammar, name: 'promql' }; const initHints = datasource.getInitHints(); - useEffect(() => { - async function checkLlms() { - const check = await isLLMPluginEnabled(); - updateLlmAppEnabled(check); - } - - if (prometheusPromQAIL) { - checkLlms(); - } - }, [prometheusPromQAIL]); - return ( <> - {prometheusPromQAIL && showDrawer && ( - setShowDrawer(false)}> - setShowDrawer(false)} - onChange={onChange} - datasource={datasource} - /> - - )} @@ -108,15 +79,6 @@ export const PromQueryBuilder = memo((props) => { onRunQuery={onRunQuery} highlightedOp={highlightedOp} /> - {prometheusPromQAIL && ( -
          - -
          - )}
          datasource={datasource} diff --git a/packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.test.tsx deleted file mode 100644 index 2f95975f5c4..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.test.tsx +++ /dev/null @@ -1,148 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/promQail/PromQail.test.tsx -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data'; - -import { PrometheusDatasource } from '../../../datasource'; -import PromQlLanguageProvider from '../../../language_provider'; -import { EmptyLanguageProviderMock } from '../../../language_provider.mock'; -import { PromOptions } from '../../../types'; -import { PromVisualQuery } from '../../types'; - -import { PromQail, queryAssistanttestIds } from './PromQail'; - -// don't care about interaction tracking in our unit tests -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - reportInteraction: jest.fn(), -})); - -window.HTMLElement.prototype.scrollIntoView = jest.fn(); - -describe('PromQail', () => { - it('renders the drawer', async () => { - setup(defaultQuery); - await waitFor(() => { - expect(screen.getByText('Query advisor')).toBeInTheDocument(); - }); - }); - - it('shows an option to not show security warning', async () => { - setup(defaultQuery); - await waitFor(() => { - expect(screen.getByText("Don't show this message again")).toBeInTheDocument(); - }); - }); - - it('shows selected metric and asks for a prompt', async () => { - setup(defaultQuery); - - await clickSecurityButton(); - - await waitFor(() => { - expect(screen.getByText('random_metric')).toBeInTheDocument(); - expect(screen.getByText('Do you know what you want to query?')).toBeInTheDocument(); - }); - }); - - it('displays a prompt when the user knows what they want to query', async () => { - setup(defaultQuery); - - await clickSecurityButton(); - - await waitFor(() => { - expect(screen.getByText('random_metric')).toBeInTheDocument(); - expect(screen.getByText('Do you know what you want to query?')).toBeInTheDocument(); - }); - - const aiPrompt = screen.getByTestId(queryAssistanttestIds.clickForAi); - - await userEvent.click(aiPrompt); - - await waitFor(() => { - expect(screen.getByText('What kind of data do you want to see with your metric?')).toBeInTheDocument(); - }); - }); - - it('does not display a prompt when choosing historical', async () => { - setup(defaultQuery); - - await clickSecurityButton(); - - await waitFor(() => { - expect(screen.getByText('random_metric')).toBeInTheDocument(); - expect(screen.getByText('Do you know what you want to query?')).toBeInTheDocument(); - }); - - const historicalPrompt = screen.getByTestId(queryAssistanttestIds.clickForHistorical); - - await userEvent.click(historicalPrompt); - - await waitFor(() => { - expect(screen.queryByText('What kind of data do you want to see with your metric?')).toBeNull(); - }); - }); -}); - -const defaultQuery: PromVisualQuery = { - metric: 'random_metric', - labels: [], - operations: [], -}; - -function createDatasource(withLabels?: boolean) { - const languageProvider = new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider; - - languageProvider.metricsMetadata = { - 'all-metrics': { - type: 'all-metrics-type', - help: 'all-metrics-help', - }, - a: { - type: 'counter', - help: 'a-metric-help', - }, - a_bucket: { - type: 'counter', - help: 'for functions', - }, - }; - - const datasource = new PrometheusDatasource( - { - url: '', - jsonData: {}, - meta: {} as DataSourcePluginMeta, - } as DataSourceInstanceSettings, - undefined, - languageProvider - ); - return datasource; -} - -function createProps(query: PromVisualQuery, datasource: PrometheusDatasource) { - return { - datasource, - onChange: jest.fn(), - closeDrawer: jest.fn(), - query: query, - }; -} - -function setup(query: PromVisualQuery) { - const withLabels: boolean = query.labels.length > 0; - const datasource = createDatasource(withLabels); - const props = createProps(query, datasource); - - // render the drawer only - const { container } = render(); - - return container; -} - -async function clickSecurityButton() { - const securityInfoButton = screen.getByTestId(queryAssistanttestIds.securityInfoButton); - - await userEvent.click(securityInfoButton); -} diff --git a/packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.tsx b/packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.tsx deleted file mode 100644 index 4993cb51127..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.tsx +++ /dev/null @@ -1,616 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/promQail/PromQail.tsx -import { css, cx } from '@emotion/css'; -import { PayloadAction, createSlice } from '@reduxjs/toolkit'; -import { useEffect, useReducer, useRef, useState } from 'react'; - -import { GrafanaTheme2, store } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; -import { Alert, Button, Checkbox, Input, Spinner, useTheme2 } from '@grafana/ui'; - -import { PrometheusDatasource } from '../../../datasource'; -import { PromVisualQuery } from '../../types'; - -import { QuerySuggestionContainer } from './QuerySuggestionContainer'; -// @ts-ignore until we can get these added for icons -import AI_Logo_color from './resources/AI_Logo_color.svg'; -import { promQailExplain, promQailSuggest } from './state/helpers'; -import { createInteraction, initialState } from './state/state'; -import { Interaction, SuggestionType } from './types'; - -export type PromQailProps = { - query: PromVisualQuery; - closeDrawer: () => void; - onChange: (query: PromVisualQuery) => void; - datasource: PrometheusDatasource; -}; - -const SKIP_STARTING_MESSAGE = 'SKIP_STARTING_MESSAGE'; - -export const PromQail = (props: PromQailProps) => { - const { query, closeDrawer, onChange, datasource } = props; - const skipStartingMessage = store.getBool(SKIP_STARTING_MESSAGE, false); - - const [state, dispatch] = useReducer(stateSlice.reducer, initialState(query, !skipStartingMessage)); - - const [labelNames, setLabelNames] = useState([]); - - const suggestions = state.interactions.reduce((acc, int) => acc + int.suggestions.length, 0); - - const responsesEndRef = useRef(null); - - const scrollToBottom = () => { - if (responsesEndRef) { - // @ts-ignore for React.MutableRefObject - responsesEndRef?.current?.scrollIntoView({ behavior: 'smooth' }); - } - }; - - useEffect(() => { - // only scroll when an interaction has been added or the suggestions have been updated - scrollToBottom(); - }, [state.interactions.length, suggestions]); - - useEffect(() => { - const fetchLabels = async () => { - let labelsIndex: Record = await datasource.languageProvider.fetchLabelsWithMatch(query.metric); - setLabelNames(Object.keys(labelsIndex)); - }; - fetchLabels(); - }, [query, datasource]); - - const theme = useTheme2(); - const styles = getStyles(theme); - - return ( -
          - {/* Query Advisor */} - {/* header */} -
          -

          Query advisor

          -
          - {/* Starting message */} -
          -
          - AI logo color Assistant -
          - {state.showStartingMessage ? ( - <> -
          -
            -
          1. - Query Advisor suggests queries based on a metric and requests you type in. -
          2. -
          3. - Query Advisor sends Prometheus metrics, labels and metadata to the LLM provider you've configured. - Be sure to align its usage with your company's internal policies. -
          4. -
          5. - An AI-suggested query may not fully answer your question. Always take a moment to understand a query - before you use it. -
          6. -
          -
          - - Query Advisor is currently in Private Preview. Feedback is appreciated and can be provided on explanations - and suggestions. - - - {/* don't show this message again, store in localstorage */} -
          - { - const val = store.getBool(SKIP_STARTING_MESSAGE, false); - store.set(SKIP_STARTING_MESSAGE, !val); - dispatch(indicateCheckbox(!val)); - }} - label="Don't show this message again" - /> -
          -
          -
          - - -
          -
          - - ) : ( -
          - {/* MAKE THIS TABLE RESPONSIVE */} - {/* FIT SUPER LONG METRICS AND LABELS IN HERE */} -
          Here is the metric you have selected:
          -
          -
          - - - - - - - - {state.query.labels.map((label, idx) => { - const text = idx === 0 ? 'labels' : ''; - return ( - - - - - - ); - })} - -
          metric{state.query.metric} - -
          {text}{`${label.label}${label.op}${label.value}`}
          -
          -
          - - {/* Ask if you know what you want to query? */} - {!state.askForQueryHelp && state.interactions.length === 0 && ( - <> -
          Do you know what you want to query?
          -
          -
          - - -
          -
          - - )} - - {state.interactions.map((interaction: Interaction, idx: number) => { - return ( -
          - {interaction.suggestionType === SuggestionType.AI ? ( - <> -
          What kind of data do you want to see with your metric?
          -
          -
          You do not need to enter in a metric or a label again in the prompt.
          -
          Example: I want to monitor request latency, not errors.
          -
          -
          - 0} - onChange={(e) => { - const prompt = e.currentTarget.value; - - const payload = { - idx: idx, - interaction: { ...interaction, prompt }, - }; - - dispatch(updateInteraction(payload)); - }} - /> -
          - {interaction.suggestions.length === 0 ? ( - interaction.isLoading ? ( - <> -
          - Waiting for OpenAI -
          - - ) : ( - <> -
          -
          - - - -
          -
          - - ) - ) : ( - // LIST OF SUGGESTED QUERIES FROM AI - { - const isLoading = false; - const suggestionType = SuggestionType.AI; - dispatch(addInteraction({ suggestionType, isLoading })); - }} - queryExplain={(suggIdx: number) => - interaction.suggestions[suggIdx].explanation === '' - ? promQailExplain(dispatch, idx, query, interaction, suggIdx, datasource) - : interaction.suggestions[suggIdx].explanation - } - onChange={onChange} - prompt={interaction.prompt ?? ''} - /> - )} - - ) : // HISTORICAL SUGGESTIONS - interaction.isLoading ? ( - <> -
          - Waiting for OpenAI -
          - - ) : ( - // LIST OF SUGGESTED QUERIES FROM HISTORICAL DATA - { - const isLoading = false; - const suggestionType = SuggestionType.AI; - dispatch(addInteraction({ suggestionType, isLoading })); - }} - queryExplain={(suggIdx: number) => - interaction.suggestions[suggIdx].explanation === '' - ? promQailExplain(dispatch, idx, query, interaction, suggIdx, datasource) - : interaction.suggestions[suggIdx].explanation - } - onChange={onChange} - prompt={interaction.prompt ?? ''} - /> - )} -
          - ); - })} -
          - )} -
          -
          -
          - ); -}; - -export const getStyles = (theme: GrafanaTheme2) => { - return { - sectionPadding: css({ - padding: '20px', - }), - header: css({ - display: 'flex', - - button: { - marginLeft: 'auto', - }, - }), - iconSection: css({ - padding: '0 0 10px 0', - color: `${theme.colors.text.secondary}`, - - img: { - paddingRight: '4px', - }, - }), - rightButtonsWrapper: css({ - display: 'flex', - }), - rightButtons: css({ - marginLeft: 'auto', - }), - leftButton: css({ - marginRight: '10px', - }), - dataList: css({ - padding: '0px 28px 0px 28px', - }), - textPadding: css({ - paddingBottom: '12px', - }), - containerPadding: css({ - padding: '28px', - }), - infoContainer: css({ - border: `${theme.colors.border.strong}`, - padding: '16px', - backgroundColor: `${theme.colors.background.secondary}`, - borderRadius: `8px`, - borderBottomLeftRadius: 0, - }), - infoContainerWrapper: css({ - paddingBottom: '24px', - }), - metricTable: css({ - width: '100%', - }), - metricTableName: css({ - width: '15%', - }), - metricTableValue: css({ - fontFamily: `${theme.typography.fontFamilyMonospace}`, - fontSize: `${theme.typography.bodySmall.fontSize}`, - overflow: 'scroll', - textWrap: 'nowrap', - maxWidth: '150px', - width: '60%', - maskImage: `linear-gradient(to right, rgba(0, 0, 0, 1) 90%, rgba(0, 0, 0, 0))`, - }), - metricTableButton: css({ - float: 'right', - }), - queryQuestion: css({ - textAlign: 'end', - padding: '8px 0', - }), - secondaryText: css({ - color: `${theme.colors.text.secondary}`, - }), - loadingMessageContainer: css({ - border: `${theme.colors.border.strong}`, - padding: `16px`, - backgroundColor: `${theme.colors.background.secondary}`, - marginBottom: `20px`, - borderRadius: `8px`, - color: `${theme.colors.text.secondary}`, - fontStyle: 'italic', - }), - floatRight: css({ - float: 'right', - }), - codeText: css({ - fontFamily: `${theme.typography.fontFamilyMonospace}`, - fontSize: `${theme.typography.bodySmall.fontSize}`, - }), - bodySmall: css({ - fontSize: `${theme.typography.bodySmall.fontSize}`, - }), - explainPadding: css({ - paddingLeft: '26px', - }), - bottomMargin: css({ - marginBottom: '20px', - }), - topPadding: css({ - paddingTop: '22px', - }), - doc: css({ - textDecoration: 'underline', - }), - afterButtons: css({ - display: 'flex', - justifyContent: 'flex-end', - }), - feedbackStyle: css({ - margin: 0, - textAlign: 'right', - paddingTop: '22px', - paddingBottom: '22px', - }), - nextInteractionHeight: css({ - height: '88px', - }), - center: css({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }), - inputPadding: css({ - paddingBottom: '24px', - }), - querySuggestion: css({ - display: 'flex', - flexWrap: 'nowrap', - }), - longCode: css({ - width: '90%', - textWrap: 'nowrap', - overflow: 'scroll', - maskImage: `linear-gradient(to right, rgba(0, 0, 0, 1) 90%, rgba(0, 0, 0, 0))`, - - div: { - display: 'inline-block', - }, - }), - useButton: css({ - marginLeft: 'auto', - }), - suggestionFeedback: css({ - textAlign: 'left', - }), - feedbackQuestion: css({ - display: 'flex', - padding: '8px 0px', - h6: { marginBottom: 0 }, - i: { - marginTop: '1px', - }, - }), - explationTextInput: css({ - paddingLeft: '24px', - }), - submitFeedback: css({ - padding: '16px 0', - }), - noMargin: css({ - margin: 0, - }), - enableButtonTooltip: css({ - padding: 8, - }), - enableButtonTooltipText: css({ - color: `${theme.colors.text.secondary}`, - ul: { - marginLeft: 16, - }, - }), - link: css({ - color: `${theme.colors.text.link} !important`, - }), - }; -}; - -export const queryAssistanttestIds = { - promQail: 'prom-qail', - securityInfoButton: 'security-info-button', - clickForHistorical: 'click-for-historical', - clickForAi: 'click-for-ai', - submitPrompt: 'submit-prompt', - refinePrompt: 'refine-prompt', -}; - -const stateSlice = createSlice({ - name: 'metrics-modal-state', - initialState: initialState(), - reducers: { - showExplainer: (state, action: PayloadAction) => { - state.showExplainer = action.payload; - }, - showStartingMessage: (state, action: PayloadAction) => { - state.showStartingMessage = action.payload; - }, - indicateCheckbox: (state, action: PayloadAction) => { - state.indicateCheckbox = action.payload; - }, - askForQueryHelp: (state, action: PayloadAction) => { - state.askForQueryHelp = action.payload; - }, - /* - * start working on a collection of interactions - * { - * askForhelp y n - * prompt question - * queries querySuggestions - * } - * - */ - addInteraction: (state, action: PayloadAction<{ suggestionType: SuggestionType; isLoading: boolean }>) => { - // AI or Historical? - const interaction = createInteraction(action.payload.suggestionType, action.payload.isLoading); - const interactions = state.interactions; - state.interactions = interactions.concat([interaction]); - }, - updateInteraction: (state, action: PayloadAction<{ idx: number; interaction: Interaction }>) => { - // update the interaction by index - // will most likely be the last interaction but we might update previous by giving them cues of helpful or not - const index = action.payload.idx; - const updInteraction = action.payload.interaction; - - state.interactions = state.interactions.map((interaction: Interaction, idx: number) => { - if (idx === index) { - return updInteraction; - } - - return interaction; - }); - }, - }, -}); - -// actions to update the state -export const { showStartingMessage, indicateCheckbox, addInteraction, updateInteraction } = stateSlice.actions; diff --git a/packages/grafana-prometheus/src/querybuilder/components/promQail/QueryAssistantButton.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/promQail/QueryAssistantButton.test.tsx deleted file mode 100644 index 8d1bf6971d4..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/promQail/QueryAssistantButton.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/promQail/QueryAssistantButton.test.tsx -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import { QueryAssistantButton } from './QueryAssistantButton'; - -const setShowDrawer = jest.fn(() => {}); - -describe('QueryAssistantButton', () => { - it('renders the button', async () => { - const props = createProps(true, 'metric', setShowDrawer); - render(); - expect(screen.getByText('Get query suggestions')).toBeInTheDocument(); - }); - - it('shows the LLM app disabled message when LLM app is not set up with vector DB', async () => { - const props = createProps(false, 'metric', setShowDrawer); - render(); - const button = screen.getByText('Get query suggestions'); - await userEvent.hover(button); - await waitFor(() => { - expect(screen.getByText('Install and enable the LLM plugin')).toBeInTheDocument(); - }); - }); - - it('shows the message to select a metric when LLM is enabled and no metric is selected', async () => { - const props = createProps(true, '', setShowDrawer); - render(); - const button = screen.getByText('Get query suggestions'); - await userEvent.hover(button); - await waitFor(() => { - expect(screen.getByText('First, select a metric.')).toBeInTheDocument(); - }); - }); - - it('calls setShowDrawer when button is clicked', async () => { - const props = createProps(true, 'metric', setShowDrawer); - render(); - const button = screen.getByText('Get query suggestions'); - fireEvent.click(button); - expect(setShowDrawer).toHaveBeenCalled(); - }); -}); - -function createProps(llmAppEnabled: boolean, metric: string, setShowDrawer: () => void) { - return { - llmAppEnabled, - metric, - setShowDrawer, - }; -} diff --git a/packages/grafana-prometheus/src/querybuilder/components/promQail/QueryAssistantButton.tsx b/packages/grafana-prometheus/src/querybuilder/components/promQail/QueryAssistantButton.tsx deleted file mode 100644 index 2324f60e402..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/promQail/QueryAssistantButton.tsx +++ /dev/null @@ -1,86 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/promQail/QueryAssistantButton.tsx -import { selectors } from '@grafana/e2e-selectors'; -import { reportInteraction } from '@grafana/runtime'; -import { Button, Tooltip, useTheme2 } from '@grafana/ui'; - -import { getStyles } from './PromQail'; -import AI_Logo_color from './resources/AI_Logo_color.svg'; - -export type Props = { - llmAppEnabled: boolean; - metric: string; - setShowDrawer: (show: boolean) => void; -}; - -export function QueryAssistantButton(props: Props) { - const { llmAppEnabled, metric, setShowDrawer } = props; - - const llmAppDisabled = !llmAppEnabled; - const noMetricSelected = !metric; - - const theme = useTheme2(); - const styles = getStyles(theme); - - const button = () => { - return ( - - ); - }; - - const selectMetricMessage = ( - - {button()} - - ); - - const llmAppMessage = ( - -
          Query Advisor is disabled
          -
          To enable Query Advisor you must:
          -
          - -
          -
          - } - > - {button()} - - ); - - if (llmAppDisabled) { - return llmAppMessage; - } else if (noMetricSelected) { - return selectMetricMessage; - } else { - return button(); - } -} diff --git a/packages/grafana-prometheus/src/querybuilder/components/promQail/QuerySuggestionContainer.tsx b/packages/grafana-prometheus/src/querybuilder/components/promQail/QuerySuggestionContainer.tsx deleted file mode 100644 index ac51393edac..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/promQail/QuerySuggestionContainer.tsx +++ /dev/null @@ -1,102 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/promQail/QuerySuggestionContainer.tsx -import { cx } from '@emotion/css'; -import { useState } from 'react'; - -import { Button, useTheme2 } from '@grafana/ui'; - -import { PromVisualQuery } from '../../types'; - -import { getStyles, queryAssistanttestIds } from './PromQail'; -import { QuerySuggestionItem } from './QuerySuggestionItem'; -import { QuerySuggestion, SuggestionType } from './types'; - -export type Props = { - querySuggestions: QuerySuggestion[]; - suggestionType: SuggestionType; - closeDrawer: () => void; - nextInteraction: () => void; - queryExplain: (idx: number) => void; - onChange: (query: PromVisualQuery) => void; - prompt: string; -}; - -export function QuerySuggestionContainer(props: Props) { - const { suggestionType, querySuggestions, closeDrawer, nextInteraction, queryExplain, onChange, prompt } = props; - - const [hasNextInteraction, updateHasNextInteraction] = useState(false); - - const theme = useTheme2(); - const styles = getStyles(theme); - - let text, secondaryText, refineText; - - if (suggestionType === SuggestionType.Historical) { - text = `Here are ${querySuggestions.length} query suggestions:`; - refineText = 'I want to write a prompt'; - } else if (suggestionType === SuggestionType.AI) { - text = text = 'Here is your query suggestion:'; - secondaryText = - 'This query is based off of natural language descriptions of the most commonly used PromQL queries.'; - refineText = 'Refine prompt'; - } - - return ( - <> - {suggestionType === SuggestionType.Historical ? ( -
          {text}
          - ) : ( - <> -
          {text}
          -
          {secondaryText}
          - - )} - -
          -
          - {querySuggestions.map((qs: QuerySuggestion, idx: number) => { - return ( - { - return acc + '$$' + qs.query; - }, '')} - prompt={prompt ?? ''} - /> - ); - })} -
          -
          - {!hasNextInteraction && ( -
          -
          - -
          -
          - -
          -
          - )} - - ); -} diff --git a/packages/grafana-prometheus/src/querybuilder/components/promQail/QuerySuggestionItem.tsx b/packages/grafana-prometheus/src/querybuilder/components/promQail/QuerySuggestionItem.tsx deleted file mode 100644 index a173c8c4efb..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/promQail/QuerySuggestionItem.tsx +++ /dev/null @@ -1,322 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/promQail/QuerySuggestionItem.tsx -import { cx } from '@emotion/css'; -import { FormEvent, useState } from 'react'; - -import { SelectableValue } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; -import { Button, RadioButtonList, Spinner, TextArea, Toggletip, useTheme2 } from '@grafana/ui'; - -import { buildVisualQueryFromString } from '../../parsing'; -import { PromVisualQuery } from '../../types'; - -import { getStyles } from './PromQail'; -import { QuerySuggestion } from './types'; - -export type Props = { - querySuggestion: QuerySuggestion; - order: number; - queryExplain: (idx: number) => void; - historical: boolean; - onChange: (query: PromVisualQuery) => void; - closeDrawer: () => void; - last: boolean; - prompt: string; - allSuggestions: string | undefined; -}; - -const suggestionOptions: SelectableValue[] = [ - { label: 'Yes', value: 'yes' }, - { label: 'No', value: 'no' }, -]; -const explationOptions: SelectableValue[] = [ - { label: 'Too vague', value: 'too vague' }, - { label: 'Too technical', value: 'too technical' }, - { label: 'Inaccurate', value: 'inaccurate' }, - { label: 'Other', value: 'other' }, -]; - -export function QuerySuggestionItem(props: Props) { - const { querySuggestion, order, queryExplain, historical, onChange, closeDrawer, last, allSuggestions, prompt } = - props; - const [showExp, updShowExp] = useState(false); - - const [gaveExplanationFeedback, updateGaveExplanationFeedback] = useState(false); - const [gaveSuggestionFeedback, updateGaveSuggestionFeedback] = useState(false); - - const [suggestionFeedback, setSuggestionFeedback] = useState({ - radioInput: '', - text: '', - }); - - const [explanationFeedback, setExplanationFeedback] = useState({ - radioInput: '', - text: '', - }); - - const theme = useTheme2(); - const styles = getStyles(theme); - - const { query, explanation } = querySuggestion; - - const feedbackToggleTip = (type: string) => { - const updateRadioFeedback = (value: string) => { - if (type === 'explanation') { - setExplanationFeedback({ - ...explanationFeedback, - radioInput: value, - }); - } else { - setSuggestionFeedback({ - ...suggestionFeedback, - radioInput: value, - }); - } - }; - - const updateTextFeedback = (e: FormEvent) => { - if (type === 'explanation') { - setExplanationFeedback({ - ...explanationFeedback, - text: e.currentTarget.value, - }); - } else { - setSuggestionFeedback({ - ...suggestionFeedback, - text: e.currentTarget.value, - }); - } - }; - - const disabledButton = () => - type === 'explanation' ? !explanationFeedback.radioInput : !suggestionFeedback.radioInput; - - const questionOne = - type === 'explanation' ? 'Why was the explanation not helpful?' : 'Were the query suggestions helpful?'; - - return ( -
          -
          -
          -
          {questionOne}
          - (Required) -
          - -
          -
          - {type !== 'explanation' && ( -
          -
          How can we improve the query suggestions?
          -
          - )} -