From 0c10600f95229e3b908e9f717911ec573f928811 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Tue, 29 Jul 2025 16:06:07 +0400 Subject: [PATCH 001/126] Live: mention the lack of tls support for ha (#108837) * clarify live docs * pretty prettier --- docs/sources/setup-grafana/set-up-grafana-live.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/sources/setup-grafana/set-up-grafana-live.md b/docs/sources/setup-grafana/set-up-grafana-live.md index 18ecd7f0c43..cfd5725d708 100644 --- a/docs/sources/setup-grafana/set-up-grafana-live.md +++ b/docs/sources/setup-grafana/set-up-grafana-live.md @@ -234,3 +234,8 @@ Live currently does not support Redis Sentinel. We recommend using a Redis Clust ``` {{< /admonition >}} + +{{< admonition type="note" >}} +The Redis Live HA engine does not currently support TLS. + +{{< /admonition >}} From 1716173f71afd4baa593d8590a6ffc7530b6e74d Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 29 Jul 2025 07:15:26 -0500 Subject: [PATCH 002/126] Alerting: Move some tests to full integration tests (#108747) * Alerting: Move some tests to full integration tests * add back two error handling tests * imports --------- Co-authored-by: Ryan McKinley --- .../ngalert/provisioning/alert_rules_test.go | 106 +--- pkg/services/ngalert/store/namespace_test.go | 427 +--------------- pkg/tests/api/alerting/api_namespace_test.go | 478 ++++++++++++++++++ .../api/alerting/api_provisioning_test.go | 125 +++++ pkg/tests/api/alerting/testing.go | 31 ++ 5 files changed, 656 insertions(+), 511 deletions(-) create mode 100644 pkg/tests/api/alerting/api_namespace_test.go diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index ae9eb265b6b..ea488b83971 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -17,30 +17,23 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/expr" - "github.com/grafana/grafana/pkg/services/apiserver" - "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" - "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" - "github.com/grafana/grafana/pkg/services/search/sort" - "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" - "github.com/grafana/grafana/pkg/util" - "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - "github.com/grafana/grafana/pkg/services/dashboards" "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/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" - "github.com/grafana/grafana/pkg/services/ngalert/testutil" + "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) +// note: additional integration tests are in /pkg/tests/api/alerting/api_provisioning_test.go + func TestIntegrationAlertRuleService(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") @@ -1989,93 +1982,6 @@ func TestDeleteRuleGroups(t *testing.T) { }) } -func TestIntegrationProvisiongWithFullpath(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode") - } - tracer := tracing.InitializeTracerForTest() - inProcBus := bus.ProvideBus(tracer) - sqlStore, cfg := db.InitTestDBWithCfg(t) - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - _, dashboardStore := testutil.SetupDashboardService(t, sqlStore, folderStore, cfg) - ac := acmock.New() - features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders) - fStore := folderimpl.ProvideStore(sqlStore) - folderService := folderimpl.ProvideService( - fStore, ac, inProcBus, dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - - ruleService := createAlertRuleService(t, folderService) - var orgID int64 = 1 - - signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ - orgID: { - dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersAll}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, - dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}}, - }} - namespaceUID := "my-namespace" - namespaceTitle := namespaceUID - rootFolder, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{ - UID: namespaceUID, - Title: namespaceTitle, - OrgID: orgID, - SignedInUser: &signedInUser, - }) - require.NoError(t, err) - - t.Run("for a rule under a root folder should set the right fullpath", func(t *testing.T) { - r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(&signedInUser), []models.AlertRule{ - createTestRule("my-cool-group", "my-cool-group", orgID, namespaceUID), - }) - require.NoError(t, err) - require.Len(t, r, 1) - - res, err := ruleService.GetAlertRuleWithFolderFullpath(context.Background(), &signedInUser, r[0].UID) - require.NoError(t, err) - assert.Equal(t, namespaceTitle, res.FolderFullpath) - - res2, err := ruleService.GetAlertRuleGroupWithFolderFullpath(context.Background(), &signedInUser, namespaceUID, "my-cool-group") - require.NoError(t, err) - assert.Equal(t, namespaceTitle, res2.FolderFullpath) - - res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, &FilterOptions{NamespaceUIDs: []string{namespaceUID}}) - require.NoError(t, err) - assert.Equal(t, namespaceTitle, res3[0].FolderFullpath) - }) - - t.Run("for a rule under a subfolder should set the right fullpath", func(t *testing.T) { - otherNamespaceUID := "my-other-namespace" - otherNamespaceTitle := "my-other-namespace containing multiple //" - _, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{ - UID: otherNamespaceUID, - Title: otherNamespaceTitle, - OrgID: orgID, - ParentUID: rootFolder.UID, - SignedInUser: &signedInUser, - }) - require.NoError(t, err) - - r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(&signedInUser), []models.AlertRule{ - createTestRule("my-cool-group-2", "my-cool-group-2", orgID, otherNamespaceUID), - }) - require.NoError(t, err) - require.Len(t, r, 1) - - res, err := ruleService.GetAlertRuleWithFolderFullpath(context.Background(), &signedInUser, r[0].UID) - require.NoError(t, err) - assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res.FolderFullpath) - - res2, err := ruleService.GetAlertRuleGroupWithFolderFullpath(context.Background(), &signedInUser, otherNamespaceUID, "my-cool-group-2") - require.NoError(t, err) - assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res2.FolderFullpath) - - res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, &FilterOptions{NamespaceUIDs: []string{otherNamespaceUID}}) - require.NoError(t, err) - assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res3[0].FolderFullpath) - }) -} - func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery { generic := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) { a, ok := cmd.(fakes.GenericRecordedQuery) diff --git a/pkg/services/ngalert/store/namespace_test.go b/pkg/services/ngalert/store/namespace_test.go index 6cc1c610dba..477ce0ef151 100644 --- a/pkg/services/ngalert/store/namespace_test.go +++ b/pkg/services/ngalert/store/namespace_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" @@ -67,422 +68,26 @@ func TestIntegration_GetUserVisibleNamespaces(t *testing.T) { }) } -func TestIntegration_GetNamespaceByUID(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") +func TestGetNamespaceByTitle(t *testing.T) { + folderService := foldertest.NewFakeService() + folderService.ExpectedError = dashboards.ErrFolderNotFound + store := DBstore{ + FolderService: folderService, } + _, err := store.GetNamespaceByTitle(context.Background(), "Test Folder", 1, nil, folder.RootFolderUID) + require.Error(t, err) + require.ErrorIs(t, err, dashboards.ErrFolderNotFound) - 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) - - u := &user.SignedInUser{ - UserID: 1, - OrgID: 1, - OrgRole: org.RoleAdmin, - IsGrafanaAdmin: true, - } - - uid := uuid.NewString() - parentUid := uuid.NewString() - title := "folder/title" - parentTitle := "parent-title" - createFolder(t, store, parentUid, parentTitle, 1, "") - createFolder(t, store, uid, title, 1, parentUid) - - actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u) - require.NoError(t, err) - require.Equal(t, title, actual.Title) - require.Equal(t, uid, actual.UID) - require.Equal(t, title, actual.Fullpath) - - t.Run("error when user does not have permissions", func(t *testing.T) { - someUser := &user.SignedInUser{ - UserID: 2, - OrgID: 1, - OrgRole: org.RoleViewer, - } - _, err = store.GetNamespaceByUID(context.Background(), uid, 1, someUser) - require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied) - }) - - t.Run("error when folder does not exist", func(t *testing.T) { - nonExistentUID := uuid.NewString() - _, err := store.GetNamespaceByUID(context.Background(), nonExistentUID, 1, u) - require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied) - }) - - t.Run("when nested folders are enabled full path should be populated with correct value", func(t *testing.T) { - store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)) - actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u) - require.NoError(t, err) - require.Equal(t, title, actual.Title) - require.Equal(t, uid, actual.UID) - require.Equal(t, "parent-title/folder\\/title", actual.Fullpath) - }) + // note: most tests are in /pkg/tests/api/alerting/api_namespace_test.go } -func TestIntegration_GetNamespaceByTitle(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } +func TestGetOrCreateNamespaceByTitle(t *testing.T) { + store := DBstore{} + _, err := store.GetOrCreateNamespaceByTitle(context.Background(), "", 1, nil, folder.RootFolderUID) + require.Error(t, err) + require.Contains(t, err.Error(), "title is empty") - 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.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)) - - u := &user.SignedInUser{ - UserID: 1, - OrgID: 1, - OrgRole: org.RoleAdmin, - IsGrafanaAdmin: true, - } - - // Create parent folder - parentUID := uuid.NewString() - parentTitle := "parent-folder" - createFolder(t, store, parentUID, parentTitle, 1, "") - - // Create child folder under parent - childUID := uuid.NewString() - childTitle := "child-folder" - createFolder(t, store, childUID, childTitle, 1, parentUID) - - // Create another folder with same title but under root - sameTitleInRoot := uuid.NewString() - createFolder(t, store, sameTitleInRoot, childTitle, 1, "") - - t.Run("should find folder by title and parent UID", func(t *testing.T) { - actual, err := store.GetNamespaceByTitle(context.Background(), childTitle, 1, u, parentUID) - require.NoError(t, err) - require.Equal(t, childTitle, actual.Title) - require.Equal(t, childUID, actual.UID) - require.Equal(t, parentUID, actual.ParentUID) - }) - - t.Run("should find folder by title in root", func(t *testing.T) { - actual, err := store.GetNamespaceByTitle(context.Background(), childTitle, 1, u, folder.RootFolderUID) - require.NoError(t, err) - require.Equal(t, childTitle, actual.Title) - require.Equal(t, sameTitleInRoot, actual.UID) - require.Equal(t, folder.RootFolderUID, actual.ParentUID) - }) - - t.Run("should return ErrFolderNotFound when folder with title doesn't exist under specified parent", func(t *testing.T) { - nonExistentTitle := "non-existent-folder" - f, err := store.GetNamespaceByTitle(context.Background(), nonExistentTitle, 1, u, parentUID) - require.Nil(t, f) - require.ErrorIs(t, err, dashboards.ErrFolderNotFound) - }) -} - -func TestIntegration_GetOrCreateNamespaceByTitle(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - u := &user.SignedInUser{ - UserID: 1, - OrgID: 1, - OrgRole: org.RoleAdmin, - IsGrafanaAdmin: true, - } - - setupStore := func(t *testing.T) *DBstore { - 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.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)) - - return store - } - - t.Run("should return error when title is empty", func(t *testing.T) { - store := setupStore(t) - _, err := store.GetOrCreateNamespaceByTitle(context.Background(), "", 1, u, folder.RootFolderUID) - require.Error(t, err) - require.Contains(t, err.Error(), "title is empty") - }) - - t.Run("should create folder when it does not exist", func(t *testing.T) { - store := setupStore(t) - - f, err := store.GetOrCreateNamespaceByTitle(context.Background(), "new folder", 1, u, folder.RootFolderUID) - require.NoError(t, err) - require.Equal(t, "new folder", f.Title) - require.NotEmpty(t, f.UID) - require.Equal(t, folder.RootFolderUID, f.ParentUID) - - folders, err := store.FolderService.GetFolders( - context.Background(), - folder.GetFoldersQuery{ - OrgID: 1, - WithFullpath: true, - SignedInUser: u, - }, - ) - require.NoError(t, err) - require.Len(t, folders, 1) - }) - - t.Run("should return existing folder when it exists", func(t *testing.T) { - store := setupStore(t) - - title := "existing folder" - createFolder(t, store, "", title, 1, "") - f, err := store.GetOrCreateNamespaceByTitle(context.Background(), title, 1, u, folder.RootFolderUID) - require.NoError(t, err) - require.Equal(t, title, f.Title) - - folders, err := store.FolderService.GetFolders( - context.Background(), - folder.GetFoldersQuery{ - OrgID: 1, - WithFullpath: true, - SignedInUser: u, - }, - ) - require.NoError(t, err) - require.Len(t, folders, 1) - }) - - t.Run("should create folder under specified parent when it does not exist", func(t *testing.T) { - store := setupStore(t) - - // Create parent folder first - parentTitle := "parent folder" - parentFolder, err := store.GetOrCreateNamespaceByTitle(context.Background(), parentTitle, 1, u, folder.RootFolderUID) - require.NoError(t, err) - - // Now create a child folder under the parent - childTitle := "child folder" - childFolder, err := store.GetOrCreateNamespaceByTitle(context.Background(), childTitle, 1, u, parentFolder.UID) - require.NoError(t, err) - - // Verify the child folder was created under the parent - folders, err := store.FolderService.GetChildren(context.Background(), &folder.GetChildrenQuery{UID: parentFolder.UID, OrgID: 1, SignedInUser: u}) - require.NoError(t, err) - require.Len(t, folders, 1) - require.Equal(t, childFolder.UID, folders[0].UID) - - folders, err = store.FolderService.GetChildren(context.Background(), &folder.GetChildrenQuery{UID: folder.RootFolderUID, OrgID: 1, SignedInUser: u}) - require.NoError(t, err) - require.Len(t, folders, 1) - require.Equal(t, parentFolder.UID, folders[0].UID) - }) - - t.Run("should get correct folder when same title exists under different parents", func(t *testing.T) { - store := setupStore(t) - - // Create first parent folder - parent1Title := "parent folder 1" - parent1, err := store.GetOrCreateNamespaceByTitle(context.Background(), parent1Title, 1, u, folder.RootFolderUID) - require.NoError(t, err) - - // Create second parent folder - parent2Title := "parent folder 2" - parent2, err := store.GetOrCreateNamespaceByTitle(context.Background(), parent2Title, 1, u, folder.RootFolderUID) - require.NoError(t, err) - - // Create folders with same title under different parents - sameTitle := "same title folder" - - // Create under first parent - folder1, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent1.UID) - require.NoError(t, err) - - // Create under second parent - folder2, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent2.UID) - require.NoError(t, err) - - // Create under root - folder3, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, folder.RootFolderUID) - require.NoError(t, err) - - // Verify we get the correct folders when specifying the parent - gotFolder1, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent1.UID) - require.NoError(t, err) - require.Equal(t, folder1.UID, gotFolder1.UID) - require.Equal(t, parent1.UID, gotFolder1.ParentUID) - - gotFolder2, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent2.UID) - require.NoError(t, err) - require.Equal(t, folder2.UID, gotFolder2.UID) - require.Equal(t, parent2.UID, gotFolder2.ParentUID) - - gotFolder3, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, folder.RootFolderUID) - require.NoError(t, err) - require.Equal(t, folder3.UID, gotFolder3.UID) - require.Equal(t, folder.RootFolderUID, gotFolder3.ParentUID) - }) - - t.Run("should create folder with deterministic UID and handle race conditions", func(t *testing.T) { - store := setupStore(t) - - folderTitle := "race condition test folder" - parentUID := folder.RootFolderUID - - // Calculate the expected UID that would be generated - expectedUID, err := generateAlertingFolderUID(folderTitle, parentUID, 1) - require.NoError(t, err) - - // Create a folder first, simulating another concurrent call that succeeded first - createFolder(t, store, expectedUID, folderTitle, 1, parentUID) - - // Now try to create a folder with the same title and parent - // This should not create a duplicate folder but return the existing one - f, err := store.GetOrCreateNamespaceByTitle(context.Background(), folderTitle, 1, u, parentUID) - require.NoError(t, err) - require.Equal(t, expectedUID, f.UID, "Should return folder with same UID as would be deterministically generated") - require.Equal(t, folderTitle, f.Title) - require.Equal(t, parentUID, f.ParentUID) - - // Verify only one folder was created - folders, err := store.FolderService.GetFolders( - context.Background(), - folder.GetFoldersQuery{ - OrgID: 1, - WithFullpath: true, - SignedInUser: u, - }, - ) - require.NoError(t, err) - require.Len(t, folders, 1, "Only one folder should exist") - }) - - t.Run("should handle special characters in folder titles", func(t *testing.T) { - store := setupStore(t) - specialTitles := []string{ - "folder/with/slashes", - "folder with spaces", - "folder-with-dashes", - "folder_with_underscores", - "folder.with.dots", - "!@#$%^&*()", - } - - for _, title := range specialTitles { - t.Run(title, func(t *testing.T) { - f, err := store.GetOrCreateNamespaceByTitle(context.Background(), title, 1, u, folder.RootFolderUID) - require.NoError(t, err) - require.Equal(t, title, f.Title) - - // Verify retrieval works - retrieved, err := store.GetNamespaceByTitle(context.Background(), title, 1, u, folder.RootFolderUID) - require.NoError(t, err) - require.Equal(t, f.UID, retrieved.UID) - }) - } - }) -} - -func TestIntegration_GetNamespaceChildren(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - 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.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)) - - admin := &user.SignedInUser{ - UserID: 1, - OrgID: 1, - OrgRole: org.RoleAdmin, - IsGrafanaAdmin: true, - } - - // Create root folders - rootFolder1 := uuid.NewString() - rootFolder2 := uuid.NewString() - createFolder(t, store, rootFolder1, "Root Folder 1", 1, "") - createFolder(t, store, rootFolder2, "Root Folder 2", 1, "") - - // Create child folders under root folder 1 - child1 := uuid.NewString() - child2 := uuid.NewString() - createFolder(t, store, child1, "Child Folder 1", 1, rootFolder1) - createFolder(t, store, child2, "Child Folder 2", 1, rootFolder1) - - // Create nested child under child1 - nestedChild := uuid.NewString() - createFolder(t, store, nestedChild, "Nested Child", 1, child1) - - differentOrgID := int64(999) - createFolder(t, store, util.GenerateShortUID(), "Root Folder 1", differentOrgID, "") - - /* - * Folder structure: - * - * Root Folder 1 - * - Child Folder 1 - * - Nested Child - * - Child Folder 2 - * Root Folder 2 - */ - - t.Run("should return direct children of a folder", func(t *testing.T) { - children, err := store.GetNamespaceChildren(context.Background(), rootFolder1, 1, admin) - require.NoError(t, err) - require.Len(t, children, 2) - - require.ElementsMatch(t, []string{child1, child2}, []string{children[0].UID, children[1].UID}) - - // Verify parent UID - for _, child := range children { - require.Equal(t, rootFolder1, child.ParentUID) - } - }) - - t.Run("should return direct children of a nested folder", func(t *testing.T) { - children, err := store.GetNamespaceChildren(context.Background(), child1, 1, admin) - require.NoError(t, err) - require.Len(t, children, 1) - require.Equal(t, nestedChild, children[0].UID) - require.Equal(t, child1, children[0].ParentUID) - }) - - t.Run("should return nil when folder does not exist", func(t *testing.T) { - nonExistentUID := uuid.NewString() - children, err := store.GetNamespaceChildren(context.Background(), nonExistentUID, 1, admin) - require.NotNil(t, children) - require.Empty(t, children) - require.Nil(t, err) - }) - - t.Run("should return empty array for folders with no children", func(t *testing.T) { - children, err := store.GetNamespaceChildren(context.Background(), rootFolder2, 1, admin) - require.Empty(t, children) - require.NotNil(t, children) - require.Nil(t, err) - }) - - t.Run("should return no children for a different org", func(t *testing.T) { - children, err := store.GetNamespaceChildren(context.Background(), rootFolder1, differentOrgID, admin) - require.Empty(t, children) - require.Nil(t, err) - }) - - t.Run("should return children from root folder", func(t *testing.T) { - children, err := store.GetNamespaceChildren(context.Background(), "", 1, admin) - require.NoError(t, err) - require.Equal(t, len(children), 2) - require.ElementsMatch(t, []string{rootFolder1, rootFolder2}, []string{children[0].UID, children[1].UID}) - }) + // note: most tests are in /pkg/tests/api/alerting/api_namespace_test.go } func TestGenerateAlertingFolderUID(t *testing.T) { diff --git a/pkg/tests/api/alerting/api_namespace_test.go b/pkg/tests/api/alerting/api_namespace_test.go new file mode 100644 index 00000000000..5ef2508a36b --- /dev/null +++ b/pkg/tests/api/alerting/api_namespace_test.go @@ -0,0 +1,478 @@ +package alerting + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" + + "bytes" + + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests/testinfra" +) + +func TestIntegration_NamespacingForRules(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) + store, cfg := env.SQLStore, env.Cfg + orgID := int64(1) + createUser(t, store, cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleEditor), + OrgID: orgID, + Password: "editor", + Login: "editor", + }) + createUser(t, store, cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + OrgID: orgID, + Password: "viewer", + Login: "viewer", + }) + + adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin") + editorClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor") + viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "viewer") + + // create test folders, with a rule in each folder + folder1UID := "test-folder-1" + folder2UID := "test-folder-2" + folder3UID := "test-folder-3" + adminClient.CreateFolder(t, folder1UID, "Test Folder 1") + adminClient.CreateFolder(t, folder2UID, "Test Folder 2") + adminClient.CreateFolder(t, folder3UID, "Test Folder 3") + rule1 := createTestAlertRule("Test Rule 1", folder1UID) + rule2 := createTestAlertRule("Test Rule 2", folder2UID) + rule3 := createTestAlertRule("Test Rule 3", folder3UID) + group1 := apimodels.PostableRuleGroupConfig{ + Name: "test-group-1", + Interval: 60, + Rules: []apimodels.PostableExtendedRuleNode{rule1}, + } + group2 := apimodels.PostableRuleGroupConfig{ + Name: "test-group-2", + Interval: 60, + Rules: []apimodels.PostableExtendedRuleNode{rule2}, + } + group3 := apimodels.PostableRuleGroupConfig{ + Name: "test-group-3", + Interval: 60, + Rules: []apimodels.PostableExtendedRuleNode{rule3}, + } + adminClient.PostRulesGroup(t, folder1UID, &group1, false) + adminClient.PostRulesGroup(t, folder2UID, &group2, false) + adminClient.PostRulesGroup(t, folder3UID, &group3, false) + + t.Run("admin, editor, and viewer should be able to see all rules in a given folder", func(t *testing.T) { + // admin + rules, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID) + require.Equal(t, http.StatusAccepted, status) + require.Len(t, rules, 1) + + // editor + rules, status, _ = editorClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID) + require.Equal(t, http.StatusAccepted, status) + require.Len(t, rules, 1) + + // viewer + rules, status, _ = viewerClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID) + require.Equal(t, http.StatusAccepted, status) + require.Len(t, rules, 1) + }) + + t.Run("admin should be able to access restricted folder, but no one else can", func(t *testing.T) { + restrictedFolderUID := "restricted-folder" + adminClient.CreateFolder(t, restrictedFolderUID, "Restricted Folder") + restrictedRule := createTestAlertRule("Restricted Rule", restrictedFolderUID) + restrictedGroup := apimodels.PostableRuleGroupConfig{ + Name: "restricted-group", + Interval: 60, + Rules: []apimodels.PostableExtendedRuleNode{restrictedRule}, + } + adminClient.PostRulesGroup(t, restrictedFolderUID, &restrictedGroup, false) + setFolderPermissions(t, grafanaListedAddr, restrictedFolderUID, []map[string]interface{}{ + { + "userId": 1, + "permission": 4, + }, + }) + + // admin ok + _, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID) + require.Equal(t, http.StatusAccepted, status) + + // editor and viewer forbidden + _, status, _ = editorClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID) + require.Equal(t, http.StatusForbidden, status) + + _, status, _ = viewerClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID) + require.Equal(t, http.StatusForbidden, status) + }) + + t.Run("errors when a folder does not exist", func(t *testing.T) { + _, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, "non-existent-folder") + // even if a folder does not exist, it will return a forbidden error (so users cannot enumerate folders) + require.Equal(t, http.StatusForbidden, status) + }) + + t.Run("permissions are respected for nested folders", func(t *testing.T) { + parentFolderUID := "parent-folder" + childFolderUID := "child-folder" + adminClient.CreateFolder(t, parentFolderUID, "Parent Folder") + adminClient.CreateFolder(t, childFolderUID, "Child Folder", parentFolderUID) + parentRule := createTestAlertRule("Parent Rule", parentFolderUID) + parentGroup := apimodels.PostableRuleGroupConfig{ + Name: "parent-group", + Interval: 60, + Rules: []apimodels.PostableExtendedRuleNode{parentRule}, + } + adminClient.PostRulesGroup(t, parentFolderUID, &parentGroup, false) + childRule := createTestAlertRule("Child Rule", childFolderUID) + childGroup := apimodels.PostableRuleGroupConfig{ + Name: "child-group", + Interval: 60, + Rules: []apimodels.PostableExtendedRuleNode{childRule}, + } + adminClient.PostRulesGroup(t, childFolderUID, &childGroup, false) + + // allow admin to access parent folder + setFolderPermissions(t, grafanaListedAddr, parentFolderUID, []map[string]interface{}{ + { + "userId": 1, + "permission": 4, + }, + }) + + // admin can get both folders + allRules, status, _ := adminClient.GetAllRulesWithStatus(t) + require.Equal(t, http.StatusOK, status) + require.Contains(t, allRules, "Parent Folder") + require.Contains(t, allRules, "Parent Folder/Child Folder") + + // editor cannot access either + allRules, status, _ = editorClient.GetAllRulesWithStatus(t) + require.Equal(t, http.StatusOK, status) + require.NotContains(t, allRules, "Parent Folder") + require.NotContains(t, allRules, "Parent Folder/Child Folder") + + // viewer cannot access either folder + allRules, status, _ = viewerClient.GetAllRulesWithStatus(t) + require.Equal(t, http.StatusOK, status) + require.NotContains(t, allRules, "Parent Folder") + require.NotContains(t, allRules, "Parent Folder/Child Folder") + }) + + t.Run("org separation", func(t *testing.T) { + orgService, err := orgimpl.ProvideService(store, cfg, quotaimpl.ProvideService(store, cfg)) + require.NoError(t, err) + newOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "Test Org 2"}) + require.NoError(t, err) + createUser(t, store, cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + OrgID: newOrg.ID, + Password: "other-admin", + Login: "other-admin-folder-perms", + }) + otherOrgFolderUID := "other-org-folder" + otherOrgClient := newAlertingApiClient(grafanaListedAddr, "other-admin-folder-perms", "other-admin") + otherOrgClient.CreateFolder(t, otherOrgFolderUID, "Other Org Folder") + otherOrgRule := createTestAlertRule("Other Org Rule", otherOrgFolderUID) + otherOrgGroup := apimodels.PostableRuleGroupConfig{ + Name: "other-org-group", + Interval: 60, + Rules: []apimodels.PostableExtendedRuleNode{otherOrgRule}, + } + otherOrgClient.PostRulesGroup(t, otherOrgFolderUID, &otherOrgGroup, false) + + // admin from org 1 cannot access org 2 alert rules + allRules, status, _ := adminClient.GetAllRulesWithStatus(t) + require.Equal(t, http.StatusOK, status) + require.NotContains(t, allRules, otherOrgFolderUID) + + // admin from org 2 cannot access org 1 alert rules + allRules, status, _ = otherOrgClient.GetAllRulesWithStatus(t) + require.Equal(t, http.StatusOK, status) + require.NotContains(t, allRules, folder1UID) + require.NotContains(t, allRules, folder2UID) + require.NotContains(t, allRules, folder3UID) + }) +} + +func TestIntegration_NamespacingForPrometheusRules(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + }) + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) + store, cfg := env.SQLStore, env.Cfg + orgID := int64(1) + createUser(t, store, cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleEditor), + OrgID: orgID, + Password: "editor", + Login: "editor", + }) + createUser(t, store, cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + OrgID: orgID, + Password: "viewer", + Login: "viewer", + }) + adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin") + editorClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor") + viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "viewer") + + // create prometheus rules in 3 separate folders + ds := adminClient.CreateDatasource(t, "prometheus") + dsUID := ds.Body.Datasource.UID + folder1UID := "prometheus-folder-1" + folder2UID := "prometheus-folder-2" + folder3UID := "prometheus-folder-3" + adminClient.CreateFolder(t, folder1UID, "Prometheus Folder 1") + adminClient.CreateFolder(t, folder2UID, "Prometheus Folder 2") + adminClient.CreateFolder(t, folder3UID, "Prometheus Folder 3") + duration1m := model.Duration(1 * 60 * 1000) + prometheusRules1 := map[string][]apimodels.PrometheusRuleGroup{ + "Prometheus Folder 1": { + { + Name: "test-group-1", + Rules: []apimodels.PrometheusRule{ + { + Alert: "HighCPUUsage", + Expr: "cpu_usage > 80", + For: &duration1m, + Labels: map[string]string{ + "severity": "warning", + }, + Annotations: map[string]string{ + "summary": "High CPU usage detected", + }, + }, + }, + }, + }, + } + prometheusRules2 := map[string][]apimodels.PrometheusRuleGroup{ + "Prometheus Folder 2": { + { + Name: "test-group-2", + Rules: []apimodels.PrometheusRule{ + { + Alert: "HighMemoryUsage", + Expr: "memory_usage > 90", + For: &duration1m, + Labels: map[string]string{ + "severity": "critical", + }, + Annotations: map[string]string{ + "summary": "High memory usage detected", + }, + }, + }, + }, + }, + } + prometheusRules3 := map[string][]apimodels.PrometheusRuleGroup{ + "Prometheus Folder 3": { + { + Name: "test-group-3", + Rules: []apimodels.PrometheusRule{ + { + Alert: "DiskSpaceLow", + Expr: "disk_usage > 95", + For: &duration1m, + Labels: map[string]string{ + "severity": "warning", + }, + Annotations: map[string]string{ + "summary": "Disk space is running low", + }, + }, + }, + }, + }, + } + // then import them + headers := map[string]string{ + "Content-Type": "application/json", + "X-Datasource-UID": dsUID, + } + adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules1, headers) + adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules2, headers) + adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules3, headers) + + t.Run("admin, editor, and viewer should be able to get all Prometheus rules", func(t *testing.T) { + // admin + rules := adminClient.ConvertPrometheusGetAllRules(t, headers) + require.Len(t, rules, 3) + require.Contains(t, rules, "Prometheus Folder 1") + require.Contains(t, rules, "Prometheus Folder 2") + require.Contains(t, rules, "Prometheus Folder 3") + + // editor + rules = editorClient.ConvertPrometheusGetAllRules(t, headers) + require.Len(t, rules, 3) + require.Contains(t, rules, "Prometheus Folder 1") + require.Contains(t, rules, "Prometheus Folder 2") + require.Contains(t, rules, "Prometheus Folder 3") + + // viewer + rules = viewerClient.ConvertPrometheusGetAllRules(t, headers) + require.Len(t, rules, 3) + require.Contains(t, rules, "Prometheus Folder 1") + require.Contains(t, rules, "Prometheus Folder 2") + require.Contains(t, rules, "Prometheus Folder 3") + }) + + t.Run("only admin can view restricted folder", func(t *testing.T) { + restrictedFolderUID := "restricted-prometheus-folder" + adminClient.CreateFolder(t, restrictedFolderUID, "Restricted Prometheus Folder") + restrictedPrometheusRules := map[string][]apimodels.PrometheusRuleGroup{ + "Restricted Prometheus Folder": { + { + Name: "restricted-group", + Rules: []apimodels.PrometheusRule{ + { + Alert: "RestrictedAlert", + Expr: "restricted_metric > 100", + For: &duration1m, + Labels: map[string]string{ + "severity": "critical", + }, + }, + }, + }, + }, + } + adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, restrictedPrometheusRules, headers) + setFolderPermissions(t, grafanaListedAddr, restrictedFolderUID, []map[string]interface{}{ + { + "userId": 1, + "permission": 4, + }, + }) + + // admin can see the restricted folder + rules := adminClient.ConvertPrometheusGetAllRules(t, headers) + require.Contains(t, rules, "Restricted Prometheus Folder") + + // editor and viewer cannot + rules = editorClient.ConvertPrometheusGetAllRules(t, headers) + require.NotContains(t, rules, "Restricted Prometheus Folder") + rules = viewerClient.ConvertPrometheusGetAllRules(t, headers) + require.NotContains(t, rules, "Restricted Prometheus Folder") + }) + + t.Run("should maintain org separation for Prometheus rules", func(t *testing.T) { + orgService, err := orgimpl.ProvideService(store, cfg, quotaimpl.ProvideService(store, cfg)) + require.NoError(t, err) + newOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "Prometheus Test Org 2"}) + require.NoError(t, err) + createUser(t, store, cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + OrgID: newOrg.ID, + Password: "other-prometheus-admin", + Login: "other-prometheus-admin", + }) + otherOrgClient := newAlertingApiClient(grafanaListedAddr, "other-prometheus-admin", "other-prometheus-admin") + otherOrgDs := otherOrgClient.CreateDatasource(t, "prometheus") + otherOrgDsUID := otherOrgDs.Body.Datasource.UID + otherOrgFolderUID := "other-org-prometheus-folder" + otherOrgClient.CreateFolder(t, otherOrgFolderUID, "Other Org Prometheus Folder") + otherOrgPrometheusRules := map[string][]apimodels.PrometheusRuleGroup{ + "Other Org Prometheus Folder": { + { + Name: "other-org-group", + Rules: []apimodels.PrometheusRule{ + { + Alert: "OtherOrgAlert", + Expr: "other_org_metric > 75", + For: &duration1m, + }, + }, + }, + }, + } + otherOrgHeaders := map[string]string{ + "Content-Type": "application/json", + "X-Datasource-UID": otherOrgDsUID, + } + otherOrgClient.ConvertPrometheusPostRuleGroups(t, otherOrgDsUID, otherOrgPrometheusRules, otherOrgHeaders) + + // admin from org 1 cannot see org 2 rules + rules := adminClient.ConvertPrometheusGetAllRules(t, headers) + require.NotContains(t, rules, "Other Org Prometheus Folder") + + // admin from org 2 cannot see org 1 rules + rules = otherOrgClient.ConvertPrometheusGetAllRules(t, otherOrgHeaders) + require.NotContains(t, rules, "Prometheus Folder 1") + require.NotContains(t, rules, "Prometheus Folder 2") + require.NotContains(t, rules, "Prometheus Folder 3") + require.Contains(t, rules, "Other Org Prometheus Folder") + }) +} + +func createTestAlertRule(title, folderUID string) apimodels.PostableExtendedRuleNode { + return apimodels.PostableExtendedRuleNode{ + GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ + Title: title, + Condition: "A", + Data: []apimodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: apimodels.RelativeTimeRange{ + From: 600, + To: 0, + }, + DatasourceUID: "-100", + Model: json.RawMessage(`{ + "type": "math", + "expression": "2 + 3 > 1" + }`), + }, + }, + NoDataState: "NoData", + ExecErrState: "Error", + }, + } +} + +func setFolderPermissions(t *testing.T, grafanaListedAddr string, folderUID string, permissions []map[string]interface{}) { + t.Helper() + + permissionPayload := map[string]interface{}{ + "items": permissions, + } + + payloadBytes, err := json.Marshal(permissionPayload) + require.NoError(t, err) + + u := fmt.Sprintf("http://admin:admin@%s/api/folders/%s/permissions", grafanaListedAddr, folderUID) + resp, err := http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) +} diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index 08b4e50e204..54b3e71b61e 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -1184,3 +1184,128 @@ func TestIntegrationExportFileProvisionContactPoints(t *testing.T) { }) }) } + +func TestIntegrationFullpath(t *testing.T) { + dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) + + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleEditor), + Password: "password", + Login: "grafana", + }) + + apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password") + + namespaceUID := "my-namespace" + namespaceTitle := namespaceUID + apiClient.CreateFolder(t, namespaceUID, namespaceTitle) + + t.Run("for a rule under a root folder should set the right fullpath", func(t *testing.T) { + interval, err := model.ParseDuration("1m") + require.NoError(t, err) + doubleInterval := 2 * interval + rules := definitions.PostableRuleGroupConfig{ + Name: "group", + Interval: interval, + Rules: []definitions.PostableExtendedRuleNode{ + { + ApiRuleNode: &definitions.ApiRuleNode{ + For: &doubleInterval, + Labels: map[string]string{"label1": "val1"}, + Annotations: map[string]string{"annotation1": "val1"}, + }, + GrafanaManagedAlert: &definitions.PostableGrafanaRule{ + Title: "rule", + Condition: "A", + 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(`{ + "type": "math", + "expression": "2 + 3 > 1" + }`), + }, + }, + }, + }, + }, + } + + resp, status, _ := apiClient.PostRulesGroupWithStatus(t, namespaceUID, &rules, false) + require.Equal(t, http.StatusAccepted, status) + require.Len(t, resp.Created, 1) + ruleUID := resp.Created[0] + + status, response := apiClient.GetProvisioningAlertRuleExport(t, ruleUID, &definitions.ExportQueryParams{Format: "json"}) + require.Equal(t, http.StatusOK, status) + var export definitions.AlertingFileExport + require.NoError(t, json.Unmarshal([]byte(response), &export)) + require.Len(t, export.Groups, 1) + assert.Equal(t, "", export.Groups[0].Folder) + }) + + t.Run("for a rule under a subfolder should set the right fullpath", func(t *testing.T) { + otherNamespaceUID := "my-other-namespace" + otherNamespaceTitle := "my-other-namespace containing multiple //" + apiClient.CreateFolder(t, otherNamespaceUID, otherNamespaceTitle, namespaceUID) + + interval, err := model.ParseDuration("1m") + require.NoError(t, err) + doubleInterval := 2 * interval + rules := definitions.PostableRuleGroupConfig{ + Name: "group-2", + Interval: interval, + Rules: []definitions.PostableExtendedRuleNode{ + { + ApiRuleNode: &definitions.ApiRuleNode{ + For: &doubleInterval, + Labels: map[string]string{"label1": "val1"}, + Annotations: map[string]string{"annotation1": "val1"}, + }, + GrafanaManagedAlert: &definitions.PostableGrafanaRule{ + Title: "rule-2", + Condition: "A", + 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(`{ + "type": "math", + "expression": "2 + 3 > 1" + }`), + }, + }, + }, + }, + }, + } + + resp, status, _ := apiClient.PostRulesGroupWithStatus(t, otherNamespaceUID, &rules, false) + require.Equal(t, http.StatusAccepted, status) + require.Len(t, resp.Created, 1) + ruleUID := resp.Created[0] + + status, response := apiClient.GetProvisioningAlertRuleExport(t, ruleUID, &definitions.ExportQueryParams{Format: "json"}) + require.Equal(t, http.StatusOK, status) + var export definitions.AlertingFileExport + require.NoError(t, json.Unmarshal([]byte(response), &export)) + require.Len(t, export.Groups, 1) + assert.Equal(t, "my-namespace/my-other-namespace containing multiple //", export.Groups[0].Folder) + }) +} diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index 654369326c1..056f813cf1d 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -1473,3 +1473,34 @@ func createUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCom require.NoError(t, err) return u.ID } + +func (a apiClient) GetProvisioningAlertRuleExport(t *testing.T, ruleUID string, params *apimodels.ExportQueryParams) (int, string) { + t.Helper() + u, err := url.Parse(fmt.Sprintf("%s/api/v1/provisioning/alert-rules/%s/export", a.url, ruleUID)) + require.NoError(t, err) + if params != nil { + q := url.Values{} + if params.Format != "" { + q.Set("format", params.Format) + } + if params.Download { + q.Set("download", "true") + } + 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() + }() + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + return resp.StatusCode, string(b) +} From 720e72423468b9b158ee33a214d77c06ff729663 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 29 Jul 2025 13:25:56 +0100 Subject: [PATCH 003/126] Playwright: Rename panel test so it has `.spec.ts` suffix (#108855) rename test so it runs in interactive mode --- e2e-playwright/smoke-tests-suite/{panels.ts => panels.spec.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename e2e-playwright/smoke-tests-suite/{panels.ts => panels.spec.ts} (100%) diff --git a/e2e-playwright/smoke-tests-suite/panels.ts b/e2e-playwright/smoke-tests-suite/panels.spec.ts similarity index 100% rename from e2e-playwright/smoke-tests-suite/panels.ts rename to e2e-playwright/smoke-tests-suite/panels.spec.ts From c7c02685942be18817d8d957158ea76eea6a3f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Tue, 29 Jul 2025 14:29:35 +0200 Subject: [PATCH 004/126] Provisioning: bulk delete resources by name (#108833) * Add resource reference to spec * Add FindResourcePath * Fix formatting * Use ForKind client * Add unit test for new method * Format code * Add integration tests * Fix unit tests * Fix formatting * Find out preferred version based on group and kind * Handle trailing slash for folders * Format code * Fix linting * Add integration test for folder bulk deletion * Format code * Format discovery file * Deduplicate paths for deletion --- pkg/apis/provisioning/v0alpha1/jobs.go | 17 + .../v0alpha1/zz_generated.deepcopy.go | 21 + .../v0alpha1/zz_generated.openapi.go | 50 ++ ...enerated.openapi_violation_exceptions.list | 1 + .../apis/provisioning/jobs/delete/worker.go | 96 ++- .../provisioning/jobs/delete/worker_test.go | 624 +++++++++++++++++- pkg/registry/apis/provisioning/register.go | 2 +- .../apis/provisioning/resources/client.go | 32 +- .../apis/provisioning/resources/repository.go | 41 ++ .../repository_resources_factory_mock.go | 2 +- .../resources/repository_resources_mock.go | 60 +- .../provisioning/resources/repository_test.go | 437 ++++++++++++ pkg/services/apiserver/client/discovery.go | 42 ++ .../provisioning.grafana.app-v0alpha1.json | 29 + .../apis/provisioning/provisioning_test.go | 334 ++++++++++ .../provisioning/v0alpha1/endpoints.gen.ts | 11 +- 16 files changed, 1776 insertions(+), 23 deletions(-) create mode 100644 pkg/registry/apis/provisioning/resources/repository_test.go diff --git a/pkg/apis/provisioning/v0alpha1/jobs.go b/pkg/apis/provisioning/v0alpha1/jobs.go index ceea231400b..66c141ebbc8 100644 --- a/pkg/apis/provisioning/v0alpha1/jobs.go +++ b/pkg/apis/provisioning/v0alpha1/jobs.go @@ -141,6 +141,23 @@ type DeleteJobOptions struct { // - nested/deep/ (for a directory) // FIXME: we should validate this in admission hooks Paths []string `json:"paths,omitempty"` + + // Resources to delete + // This option has been created because currently the frontend does not use + // standarized app platform APIs. For performance and API consistency reasons, the preferred option + // is it to use the paths. + Resources []ResourceRef `json:"resources,omitempty"` +} + +type ResourceRef struct { + // Name is the name of the resource, such as a dashboard UID. + Name string `json:"name,omitempty"` + + // Kind is the type of resource, for example, "Dashboard". + Kind string `json:"kind,omitempty"` + + // Group is the group of the resource, such as "dashboard.grafana.app". + Group string `json:"group,omitempty"` } // The job status diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go index bed49953b63..a39d8f42d72 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go @@ -56,6 +56,11 @@ func (in *DeleteJobOptions) DeepCopyInto(out *DeleteJobOptions) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]ResourceRef, len(*in)) + copy(*out, *in) + } return } @@ -835,6 +840,22 @@ func (in *ResourceObjects) DeepCopy() *ResourceObjects { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceRef) DeepCopyInto(out *ResourceRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceRef. +func (in *ResourceRef) DeepCopy() *ResourceRef { + if in == nil { + return nil + } + out := new(ResourceRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceRepositoryInfo) DeepCopyInto(out *ResourceRepositoryInfo) { *out = *in diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index b81342c30c3..8c497089383 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -48,6 +48,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceList": schema_pkg_apis_provisioning_v0alpha1_ResourceList(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceListItem": schema_pkg_apis_provisioning_v0alpha1_ResourceListItem(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceObjects": schema_pkg_apis_provisioning_v0alpha1_ResourceObjects(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceRef": schema_pkg_apis_provisioning_v0alpha1_ResourceRef(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceRepositoryInfo": schema_pkg_apis_provisioning_v0alpha1_ResourceRepositoryInfo(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceStats": schema_pkg_apis_provisioning_v0alpha1_ResourceStats(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceType": schema_pkg_apis_provisioning_v0alpha1_ResourceType(ref), @@ -184,9 +185,25 @@ func schema_pkg_apis_provisioning_v0alpha1_DeleteJobOptions(ref common.Reference }, }, }, + "resources": { + SchemaProps: spec.SchemaProps{ + Description: "Resources to delete This option has been created because currently the frontend does not use standarized app platform APIs. For performance and API consistency reasons, the preferred option is it to use the paths.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceRef"), + }, + }, + }, + }, + }, }, }, }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceRef"}, } } @@ -1824,6 +1841,39 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceObjects(ref common.ReferenceC } } +func schema_pkg_apis_provisioning_v0alpha1_ResourceRef(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{ + Description: "Name is the name of the resource, such as a dashboard UID.", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is the type of resource, for example, \"Dashboard\".", + Type: []string{"string"}, + Format: "", + }, + }, + "group": { + SchemaProps: spec.SchemaProps{ + Description: "Group is the group of the resource, such as \"dashboard.grafana.app\".", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_pkg_apis_provisioning_v0alpha1_ResourceRepositoryInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list index fc982653184..847f8aaab1a 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,4 +1,5 @@ API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,DeleteJobOptions,Paths +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,DeleteJobOptions,Resources API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,FileList,Items API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,HistoryList,Items API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,JobResourceSummary,Errors diff --git a/pkg/registry/apis/provisioning/jobs/delete/worker.go b/pkg/registry/apis/provisioning/jobs/delete/worker.go index 74202643ccd..006d296d393 100644 --- a/pkg/registry/apis/provisioning/jobs/delete/worker.go +++ b/pkg/registry/apis/provisioning/jobs/delete/worker.go @@ -6,20 +6,25 @@ import ( "fmt" "time" + "k8s.io/apimachinery/pkg/runtime/schema" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" ) type Worker struct { - syncWorker jobs.Worker - wrapFn repository.WrapWithStageFn + syncWorker jobs.Worker + wrapFn repository.WrapWithStageFn + resourcesFactory resources.RepositoryResourcesFactory } -func NewWorker(syncWorker jobs.Worker, wrapFn repository.WrapWithStageFn) *Worker { +func NewWorker(syncWorker jobs.Worker, wrapFn repository.WrapWithStageFn, resourcesFactory resources.RepositoryResourcesFactory) *Worker { return &Worker{ - syncWorker: syncWorker, - wrapFn: wrapFn, + syncWorker: syncWorker, + wrapFn: wrapFn, + resourcesFactory: resourcesFactory, } } @@ -31,10 +36,11 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr if job.Spec.Delete == nil { return errors.New("missing delete settings") } - opts := *job.Spec.Delete + opts := *job.Spec.Delete paths := opts.Paths - progress.SetTotal(ctx, len(paths)) + + progress.SetTotal(ctx, len(paths)+len(opts.Resources)) progress.StrictMaxErrors(1) // Fail fast on any error during deletion fn := func(repo repository.Repository, _ bool) error { @@ -43,6 +49,18 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr return errors.New("delete job submitted targeting repository that is not a ReaderWriter") } + // Resolve ResourceRef entries to file paths using RepositoryResources + if len(opts.Resources) > 0 { + resolvedPaths, err := w.resolveResourcesToPaths(ctx, rw, progress, opts.Resources) + if err != nil { + return err + } + paths = append(paths, resolvedPaths...) + } + + // Deduplicate paths to avoid attempting to delete the same file multiple times + paths = deduplicatePaths(paths) + return w.deleteFiles(ctx, rw, progress, opts, paths...) } @@ -97,3 +115,67 @@ func (w *Worker) deleteFiles(ctx context.Context, rw repository.ReaderWriter, pr return nil } + +// resolveResourcesToPaths converts ResourceRef entries to file paths, recording errors for individual resources +func (w *Worker) resolveResourcesToPaths(ctx context.Context, rw repository.ReaderWriter, progress jobs.JobProgressRecorder, resources []provisioning.ResourceRef) ([]string, error) { + if len(resources) == 0 { + return nil, nil + } + + progress.SetMessage(ctx, "Resolving resource paths") + repositoryResources, err := w.resourcesFactory.Client(ctx, rw) + if err != nil { + return nil, fmt.Errorf("create repository resources client: %w", err) + } + + resolvedPaths := make([]string, 0, len(resources)) + for _, resource := range resources { + result := jobs.JobResourceResult{ + Name: resource.Name, + Group: resource.Group, + Action: repository.FileActionDeleted, // Will be used for deletion later + } + + gvk := schema.GroupVersionKind{ + Group: resource.Group, + Kind: resource.Kind, + // Version is left empty so ForKind will use the preferred version + } + + progress.SetMessage(ctx, fmt.Sprintf("Finding path for resource %s/%s/%s", resource.Group, resource.Kind, resource.Name)) + resourcePath, err := repositoryResources.FindResourcePath(ctx, resource.Name, gvk) + if err != nil { + result.Error = fmt.Errorf("find path for resource %s/%s/%s: %w", resource.Group, resource.Kind, resource.Name, err) + progress.Record(ctx, result) + // Continue with next resource instead of failing fast + if err := progress.TooManyErrors(); err != nil { + return resolvedPaths, err + } + continue + } + + result.Path = resourcePath + resolvedPaths = append(resolvedPaths, resourcePath) + } + + return resolvedPaths, nil +} + +// deduplicatePaths removes duplicate file paths from the slice while preserving order +func deduplicatePaths(paths []string) []string { + if len(paths) <= 1 { + return paths + } + + seen := make(map[string]bool, len(paths)) + result := make([]string, 0, len(paths)) + + for _, path := range paths { + if !seen[path] { + seen[path] = true + result = append(result, path) + } + } + + return result +} diff --git a/pkg/registry/apis/provisioning/jobs/delete/worker_test.go b/pkg/registry/apis/provisioning/jobs/delete/worker_test.go index fbfe81a9762..5d63687dd2c 100644 --- a/pkg/registry/apis/provisioning/jobs/delete/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/delete/worker_test.go @@ -6,9 +6,13 @@ import ( "testing" "time" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -22,6 +26,15 @@ func (m *mockReaderWriter) Delete(ctx context.Context, path, ref, message string return args.Error(0) } +// simpleRepository implements only the base Repository interface, not ReaderWriter +type simpleRepository struct{} + +func (s *simpleRepository) Config() *provisioning.Repository { return nil } +func (s *simpleRepository) Validate() field.ErrorList { return nil } +func (s *simpleRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { + return nil, nil +} + func TestDeleteWorker_IsSupported(t *testing.T) { tests := []struct { name string @@ -59,7 +72,7 @@ func TestDeleteWorker_IsSupported(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - worker := NewWorker(nil, nil) + worker := NewWorker(nil, nil, nil) result := worker.IsSupported(context.Background(), tt.job) require.Equal(t, tt.expected, result) }) @@ -73,7 +86,7 @@ func TestDeleteWorker_ProcessMissingDeleteSettings(t *testing.T) { }, } - worker := NewWorker(nil, nil) + worker := NewWorker(nil, nil, nil) err := worker.Process(context.Background(), nil, job, nil) require.EqualError(t, err, "missing delete settings") } @@ -102,7 +115,7 @@ func TestDeleteWorker_ProcessNotReaderWriter(t *testing.T) { mockProgress.On("SetTotal", mock.Anything, 1).Return() mockProgress.On("StrictMaxErrors", 1).Return() - worker := NewWorker(nil, mockWrapFn.Execute) + worker := NewWorker(nil, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "delete files from repository: delete job submitted targeting repository that is not a ReaderWriter") } @@ -125,7 +138,7 @@ func TestDeleteWorker_ProcessWrapFnError(t *testing.T) { mockProgress.On("SetTotal", mock.Anything, 1).Return() mockProgress.On("StrictMaxErrors", 1).Return() - worker := NewWorker(nil, mockWrapFn.Execute) + worker := NewWorker(nil, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "delete files from repository: stage failed") } @@ -172,7 +185,7 @@ func TestDeleteWorker_ProcessDeleteFilesSuccess(t *testing.T) { return result.Path == "test/path2" && result.Action == repository.FileActionDeleted && result.Error == nil })).Return() - worker := NewWorker(nil, mockWrapFn.Execute) + worker := NewWorker(nil, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -210,7 +223,7 @@ func TestDeleteWorker_ProcessDeleteFilesWithError(t *testing.T) { })).Return() mockProgress.On("TooManyErrors").Return(errors.New("too many errors")) - worker := NewWorker(nil, mockWrapFn.Execute) + worker := NewWorker(nil, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "delete files from repository: too many errors") } @@ -254,7 +267,7 @@ func TestDeleteWorker_ProcessWithSyncWorker(t *testing.T) { return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental }), mockProgress).Return(nil) - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -294,7 +307,7 @@ func TestDeleteWorker_ProcessSyncWorkerError(t *testing.T) { syncError := errors.New("sync failed") mockSyncWorker.On("Process", mock.Anything, mockRepo, mock.Anything, mockProgress).Return(syncError) - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "pull resources: sync failed") } @@ -363,7 +376,7 @@ func TestDeleteWorker_deleteFiles(t *testing.T) { } } - worker := NewWorker(nil, nil) + worker := NewWorker(nil, nil, nil) err := worker.deleteFiles(context.Background(), mockRepo, mockProgress, opts, tt.paths...) if tt.expectedError != "" { @@ -377,3 +390,596 @@ func TestDeleteWorker_deleteFiles(t *testing.T) { }) } } + +func TestDeleteWorker_ProcessWithResourceRefs(t *testing.T) { + job := provisioning.Job{ + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Paths: []string{"test/path1"}, + Resources: []provisioning.ResourceRef{ + { + Name: "test-dashboard", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + { + Name: "test-folder", + Kind: "Folder", + Group: "folder.grafana.app", + }, + }, + Ref: "main", + }, + }, + } + + mockRepo := &mockReaderWriter{ + MockRepository: repository.NewMockRepository(t), + } + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockWrapFn := repository.NewMockWrapWithStageFn(t) + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + mockRepositoryResources := resources.NewMockRepositoryResources(t) + + // Mock repository resources factory and client + mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil) + + // Mock FindResourcePath calls + mockRepositoryResources.On("FindResourcePath", mock.Anything, "test-dashboard", schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + // Version is empty - ForKind will discover the preferred version + }).Return("dashboards/test-dashboard.json", nil) + + mockRepositoryResources.On("FindResourcePath", mock.Anything, "test-folder", schema.GroupVersionKind{ + Group: "folder.grafana.app", + Kind: "Folder", + // Version is empty - ForKind will discover the preferred version + }).Return("folders/test-folder.json", nil) + + mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool { + return !opts.PushOnWrites && opts.Timeout == 10*time.Minute + }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(mockRepo, false) + }) + + // Progress tracking - expects 3 total (1 path + 2 resources) + mockProgress.On("SetTotal", mock.Anything, 3).Return() + mockProgress.On("StrictMaxErrors", 1).Return() + mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/test-dashboard").Return() + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource folder.grafana.app/Folder/test-folder").Return() + mockProgress.On("SetMessage", mock.Anything, "Deleting test/path1").Return() + mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/test-dashboard.json").Return() + mockProgress.On("SetMessage", mock.Anything, "Deleting folders/test-folder.json").Return() + mockProgress.On("TooManyErrors").Return(nil).Times(3) + + // Mock file deletions + mockRepo.On("Delete", mock.Anything, "test/path1", "main", "Delete test/path1").Return(nil) + mockRepo.On("Delete", mock.Anything, "dashboards/test-dashboard.json", "main", "Delete dashboards/test-dashboard.json").Return(nil) + mockRepo.On("Delete", mock.Anything, "folders/test-folder.json", "main", "Delete folders/test-folder.json").Return(nil) + + // Mock progress records + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Path == "test/path1" && result.Action == repository.FileActionDeleted && result.Error == nil + })).Return() + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Path == "dashboards/test-dashboard.json" && result.Action == repository.FileActionDeleted && result.Error == nil + })).Return() + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Path == "folders/test-folder.json" && result.Action == repository.FileActionDeleted && result.Error == nil + })).Return() + + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + err := worker.Process(context.Background(), mockRepo, job, mockProgress) + require.NoError(t, err) + + mockResourcesFactory.AssertExpectations(t) + mockRepositoryResources.AssertExpectations(t) +} + +func TestDeleteWorker_ProcessResourceRefsOnly(t *testing.T) { + job := provisioning.Job{ + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "test-dashboard", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + Ref: "main", + }, + }, + } + + mockRepo := &mockReaderWriter{ + MockRepository: repository.NewMockRepository(t), + } + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockWrapFn := repository.NewMockWrapWithStageFn(t) + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + mockRepositoryResources := resources.NewMockRepositoryResources(t) + + mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil) + + mockRepositoryResources.On("FindResourcePath", mock.Anything, "test-dashboard", schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + // Version is empty - ForKind will discover the preferred version + }).Return("dashboards/test-dashboard.json", nil) + + mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(mockRepo, false) + }) + + mockProgress.On("SetTotal", mock.Anything, 1).Return() + mockProgress.On("StrictMaxErrors", 1).Return() + mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/test-dashboard").Return() + mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/test-dashboard.json").Return() + mockProgress.On("TooManyErrors").Return(nil) + + mockRepo.On("Delete", mock.Anything, "dashboards/test-dashboard.json", "main", "Delete dashboards/test-dashboard.json").Return(nil) + + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Path == "dashboards/test-dashboard.json" && result.Action == repository.FileActionDeleted && result.Error == nil + })).Return() + + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + err := worker.Process(context.Background(), mockRepo, job, mockProgress) + require.NoError(t, err) +} + +func TestDeleteWorker_ProcessResourceResolutionError(t *testing.T) { + job := provisioning.Job{ + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "nonexistent-dashboard", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + }, + } + + mockRepo := &mockReaderWriter{ + MockRepository: repository.NewMockRepository(t), + } + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockWrapFn := repository.NewMockWrapWithStageFn(t) + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + mockRepositoryResources := resources.NewMockRepositoryResources(t) + + mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil) + + findPathError := errors.New("resource not found in repository: dashboard.grafana.app/dashboards/nonexistent-dashboard") + mockRepositoryResources.On("FindResourcePath", mock.Anything, "nonexistent-dashboard", schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + // Version is empty - ForKind will discover the preferred version + }).Return("", findPathError) + + mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(mockRepo, false) + }) + + mockProgress.On("SetTotal", mock.Anything, 1).Return() + mockProgress.On("StrictMaxErrors", 1).Return() + mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/nonexistent-dashboard").Return() + + // Expect error to be recorded, not thrown + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Name == "nonexistent-dashboard" && + result.Group == "dashboard.grafana.app" && + result.Action == repository.FileActionDeleted && + result.Error != nil + })).Return() + mockProgress.On("TooManyErrors").Return(nil) + + // Mock sync worker behavior that happens when no ref is specified + mockProgress.On("ResetResults").Return() + mockProgress.On("SetMessage", mock.Anything, "pull resources").Return() + + mockSyncWorker := jobs.NewMockWorker(t) + mockSyncWorker.On("Process", mock.Anything, mockRepo, mock.MatchedBy(func(syncJob provisioning.Job) bool { + return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental + }), mockProgress).Return(nil) + + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory) + err := worker.Process(context.Background(), mockRepo, job, mockProgress) + require.NoError(t, err) // Should succeed even with resource resolution error +} + +func TestDeleteWorker_ProcessResourcesFactoryError(t *testing.T) { + job := provisioning.Job{ + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "test-dashboard", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + }, + } + + mockRepo := &mockReaderWriter{ + MockRepository: repository.NewMockRepository(t), + } + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockWrapFn := repository.NewMockWrapWithStageFn(t) + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + + factoryError := errors.New("failed to create repository resources client") + mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(nil, factoryError) + + mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(mockRepo, false) + }) + + mockProgress.On("SetTotal", mock.Anything, 1).Return() + mockProgress.On("StrictMaxErrors", 1).Return() + mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() + + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + err := worker.Process(context.Background(), mockRepo, job, mockProgress) + require.EqualError(t, err, "delete files from repository: create repository resources client: failed to create repository resources client") +} + +func TestDeleteWorker_ProcessResourceRefsNotReaderWriter(t *testing.T) { + job := provisioning.Job{ + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "test-dashboard", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + }, + } + + // Create a simple repository that doesn't implement ReaderWriter + mockRepo := &simpleRepository{} + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockWrapFn := repository.NewMockWrapWithStageFn(t) + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + + // Mock the wrap function that will call our function and get the ReaderWriter error + mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(mockRepo, false) + }) + + // The ReaderWriter check should fail immediately, so no resource resolution calls should happen + mockProgress.On("SetTotal", mock.Anything, 1).Return() + mockProgress.On("StrictMaxErrors", 1).Return() + + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + err := worker.Process(context.Background(), mockRepo, job, mockProgress) + require.EqualError(t, err, "delete files from repository: delete job submitted targeting repository that is not a ReaderWriter") +} + +func TestDeleteWorker_ProcessResourceResolutionTooManyErrors(t *testing.T) { + job := provisioning.Job{ + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "nonexistent-dashboard", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + }, + } + + mockRepo := &mockReaderWriter{ + MockRepository: repository.NewMockRepository(t), + } + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockWrapFn := repository.NewMockWrapWithStageFn(t) + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + mockRepositoryResources := resources.NewMockRepositoryResources(t) + + mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil) + + findPathError := errors.New("resource not found in repository") + mockRepositoryResources.On("FindResourcePath", mock.Anything, "nonexistent-dashboard", schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + // Version is empty - ForKind will discover the preferred version + }).Return("", findPathError) + + mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(mockRepo, false) + }) + + mockProgress.On("SetTotal", mock.Anything, 1).Return() + mockProgress.On("StrictMaxErrors", 1).Return() + mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/nonexistent-dashboard").Return() + + // Mock recording error and TooManyErrors returning error + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Name == "nonexistent-dashboard" && result.Error != nil + })).Return() + mockProgress.On("TooManyErrors").Return(errors.New("too many errors")) + + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + err := worker.Process(context.Background(), mockRepo, job, mockProgress) + require.EqualError(t, err, "delete files from repository: too many errors") +} + +func TestDeleteWorker_ProcessMixedResourcesWithPartialFailure(t *testing.T) { + job := provisioning.Job{ + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "valid-dashboard", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + { + Name: "nonexistent-dashboard", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + { + Name: "valid-folder", + Kind: "Folder", + Group: "folder.grafana.app", + }, + }, + Ref: "main", + }, + }, + } + + mockRepo := &mockReaderWriter{ + MockRepository: repository.NewMockRepository(t), + } + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockWrapFn := repository.NewMockWrapWithStageFn(t) + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + mockRepositoryResources := resources.NewMockRepositoryResources(t) + + mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil) + + // First resource succeeds + mockRepositoryResources.On("FindResourcePath", mock.Anything, "valid-dashboard", schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + // Version is empty - ForKind will discover the preferred version + }).Return("dashboards/valid-dashboard.json", nil) + + // Second resource fails + findPathError := errors.New("resource not found") + mockRepositoryResources.On("FindResourcePath", mock.Anything, "nonexistent-dashboard", schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + // Version is empty - ForKind will discover the preferred version + }).Return("", findPathError) + + // Third resource succeeds + mockRepositoryResources.On("FindResourcePath", mock.Anything, "valid-folder", schema.GroupVersionKind{ + Group: "folder.grafana.app", + Kind: "Folder", + // Version is empty - ForKind will discover the preferred version + }).Return("folders/valid-folder.json", nil) + + mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(mockRepo, false) + }) + + mockProgress.On("SetTotal", mock.Anything, 3).Return() + mockProgress.On("StrictMaxErrors", 1).Return() + mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/valid-dashboard").Return() + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/nonexistent-dashboard").Return() + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource folder.grafana.app/Folder/valid-folder").Return() + mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/valid-dashboard.json").Return() + mockProgress.On("SetMessage", mock.Anything, "Deleting folders/valid-folder.json").Return() + + // Record the error for the failed resource + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Name == "nonexistent-dashboard" && result.Error != nil + })).Return() + + // Allow continuing after error + mockProgress.On("TooManyErrors").Return(nil).Times(3) // Called after each resource resolution and file deletion + + // Mock successful file deletions for resolved resources + mockRepo.On("Delete", mock.Anything, "dashboards/valid-dashboard.json", "main", "Delete dashboards/valid-dashboard.json").Return(nil) + mockRepo.On("Delete", mock.Anything, "folders/valid-folder.json", "main", "Delete folders/valid-folder.json").Return(nil) + + // Record successful deletions + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Path == "dashboards/valid-dashboard.json" && result.Error == nil + })).Return() + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Path == "folders/valid-folder.json" && result.Error == nil + })).Return() + + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + err := worker.Process(context.Background(), mockRepo, job, mockProgress) + require.NoError(t, err) // Should succeed overall, with only the failed resource recorded as error +} + +func TestDeleteWorker_ProcessWithPathDeduplication(t *testing.T) { + // Test that duplicate paths from explicit paths and resource resolution are deduplicated + job := provisioning.Job{ + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Ref: "main", // Add ref to avoid sync worker execution + Paths: []string{"dashboards/test-dashboard.json", "folders/test-folder/"}, // Explicit paths + Resources: []provisioning.ResourceRef{ + { + Name: "test-dashboard", // This will resolve to "dashboards/test-dashboard.json" (duplicate) + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + { + Name: "test-folder", // This will resolve to "folders/test-folder/" (duplicate) + Kind: "Folder", + Group: "folder.grafana.app", + }, + { + Name: "unique-dashboard", // This will resolve to "dashboards/unique-dashboard.json" (unique) + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + }, + } + + mockRepo := &mockReaderWriter{ + MockRepository: repository.NewMockRepository(t), + } + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockWrapFn := repository.NewMockWrapWithStageFn(t) + + // Mock resources factory and repository resources + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + mockRepositoryResources := resources.NewMockRepositoryResources(t) + + mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepositoryResources, nil) + + mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(mockRepo, false) + }) + + // Expect total of 5 items (2 explicit paths + 3 resources), but only 3 unique paths will be deleted + mockProgress.On("SetTotal", mock.Anything, 5).Return() + mockProgress.On("StrictMaxErrors", 1).Return() + + // Resource resolution phase + mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() + + // Mock resource path resolution - note duplicates with explicit paths + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/test-dashboard").Return() + mockRepositoryResources.On("FindResourcePath", mock.Anything, "test-dashboard", schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }).Return("dashboards/test-dashboard.json", nil) // Duplicate of explicit path + + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource folder.grafana.app/Folder/test-folder").Return() + mockRepositoryResources.On("FindResourcePath", mock.Anything, "test-folder", schema.GroupVersionKind{ + Group: "folder.grafana.app", + Kind: "Folder", + }).Return("folders/test-folder/", nil) // Duplicate of explicit path + + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/unique-dashboard").Return() + mockRepositoryResources.On("FindResourcePath", mock.Anything, "unique-dashboard", schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }).Return("dashboards/unique-dashboard.json", nil) // Unique path + + // Note: successful resource resolution does not call Record - only failures do + + // Deletion phase - should only delete 3 unique paths (deduplication working) + mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/test-dashboard.json").Return() + mockRepo.On("Delete", mock.Anything, "dashboards/test-dashboard.json", "main", "Delete dashboards/test-dashboard.json").Return(nil) + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Path == "dashboards/test-dashboard.json" && result.Action == repository.FileActionDeleted && result.Error == nil + })).Return() + mockProgress.On("TooManyErrors").Return(nil) + + mockProgress.On("SetMessage", mock.Anything, "Deleting folders/test-folder/").Return() + mockRepo.On("Delete", mock.Anything, "folders/test-folder/", "main", "Delete folders/test-folder/").Return(nil) + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Path == "folders/test-folder/" && result.Action == repository.FileActionDeleted && result.Error == nil + })).Return() + + mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/unique-dashboard.json").Return() + mockRepo.On("Delete", mock.Anything, "dashboards/unique-dashboard.json", "main", "Delete dashboards/unique-dashboard.json").Return(nil) + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Path == "dashboards/unique-dashboard.json" && result.Action == repository.FileActionDeleted && result.Error == nil + })).Return() + + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + err := worker.Process(context.Background(), mockRepo, job, mockProgress) + require.NoError(t, err) + + // Verify all mocks were called as expected - key point is that each file is only deleted once + mockRepo.AssertExpectations(t) + mockProgress.AssertExpectations(t) + mockResourcesFactory.AssertExpectations(t) + mockRepositoryResources.AssertExpectations(t) +} + +func TestDeduplicatePaths(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + name: "empty slice", + input: []string{}, + expected: []string{}, + }, + { + name: "single path", + input: []string{"path1"}, + expected: []string{"path1"}, + }, + { + name: "no duplicates", + input: []string{"path1", "path2", "path3"}, + expected: []string{"path1", "path2", "path3"}, + }, + { + name: "with duplicates", + input: []string{"path1", "path2", "path1", "path3", "path2"}, + expected: []string{"path1", "path2", "path3"}, + }, + { + name: "all same paths", + input: []string{"path1", "path1", "path1"}, + expected: []string{"path1"}, + }, + { + name: "mixed paths with folder trailing slash", + input: []string{"folder/", "file.json", "folder/", "nested/file.json", "file.json"}, + expected: []string{"folder/", "file.json", "nested/file.json"}, + }, + { + name: "preserves order", + input: []string{"c", "a", "b", "a", "c"}, + expected: []string{"c", "a", "b"}, + }, + { + name: "realistic scenario - explicit paths and resource refs resolve to same paths", + input: []string{"dashboards/dashboard1.json", "folder/", "dashboards/dashboard1.json", "alerts/alert1.yaml", "folder/"}, + expected: []string{"dashboards/dashboard1.json", "folder/", "alerts/alert1.yaml"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := deduplicatePaths(tt.input) + require.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index be4cf583824..96a5145fd30 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -617,7 +617,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH b.storageStatus, ) - deleteWorker := deletepkg.NewWorker(syncWorker, stageIfPossible) + deleteWorker := deletepkg.NewWorker(syncWorker, stageIfPossible, b.repositoryResources) workers := []jobs.Worker{ deleteWorker, exportWorker, diff --git a/pkg/registry/apis/provisioning/resources/client.go b/pkg/registry/apis/provisioning/resources/client.go index afc5261b2e8..2f6410c349c 100644 --- a/pkg/registry/apis/provisioning/resources/client.go +++ b/pkg/registry/apis/provisioning/resources/client.go @@ -107,6 +107,9 @@ type clientInfo struct { client dynamic.ResourceInterface } +// ForKind returns a client for a kind. +// If the kind has a version, it will be used. +// If the kind does not have a version, the preferred version will be used. func (c *resourceClients) ForKind(gvk schema.GroupVersionKind) (dynamic.ResourceInterface, schema.GroupVersionResource, error) { c.mutex.Lock() defer c.mutex.Unlock() @@ -116,9 +119,29 @@ func (c *resourceClients) ForKind(gvk schema.GroupVersionKind) (dynamic.Resource return info.client, info.gvr, nil } - gvr, err := c.discovery.GetResourceForKind(gvk) - if err != nil { - return nil, schema.GroupVersionResource{}, err + var err error + var gvr schema.GroupVersionResource + var versionless schema.GroupVersionKind + if gvk.Version == "" { + versionless = gvk + gvr, gvk, err = c.discovery.GetPreferredVersionForKind(schema.GroupKind{ + Group: gvk.Group, + Kind: gvk.Kind, + }) + if err != nil { + return nil, schema.GroupVersionResource{}, err + } + + info, ok := c.byKind[gvk] + if ok && info.client != nil { + c.byKind[versionless] = info + return info.client, info.gvr, nil + } + } else { + gvr, err = c.discovery.GetResourceForKind(gvk) + if err != nil { + return nil, schema.GroupVersionResource{}, err + } } info = &clientInfo{ gvk: gvk, @@ -127,6 +150,9 @@ func (c *resourceClients) ForKind(gvk schema.GroupVersionKind) (dynamic.Resource } c.byKind[gvk] = info c.byResource[gvr] = info + if versionless.Group != "" { + c.byKind[versionless] = info + } return info.client, info.gvr, nil } diff --git a/pkg/registry/apis/provisioning/resources/repository.go b/pkg/registry/apis/provisioning/resources/repository.go index 44bfe42f74e..d12747b7bc8 100644 --- a/pkg/registry/apis/provisioning/resources/repository.go +++ b/pkg/registry/apis/provisioning/resources/repository.go @@ -4,11 +4,15 @@ import ( "context" "fmt" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/grafana/grafana/pkg/apimachinery/utils" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" ) //go:generate mockery --name RepositoryResourcesFactory --structname MockRepositoryResourcesFactory --inpackage --filename repository_resources_factory_mock.go --with-expecter @@ -28,6 +32,7 @@ type RepositoryResources interface { // Resource from file WriteResourceFromFile(ctx context.Context, path, ref string) (string, schema.GroupVersionKind, error) RemoveResourceFromFile(ctx context.Context, path, ref string) (string, schema.GroupVersionKind, error) + FindResourcePath(ctx context.Context, name string, gvk schema.GroupVersionKind) (string, error) RenameResourceFile(ctx context.Context, path, previousRef, newPath, newRef string) (string, schema.GroupVersionKind, error) // Stats Stats(ctx context.Context) (*provisioning.ResourceStats, error) @@ -55,6 +60,42 @@ func (r *repositoryResources) List(ctx context.Context) (*provisioning.ResourceL return r.lister.List(ctx, r.namespace, r.repoName) } +// FindResourcePath finds the repository file path for a resource by its name and GroupVersionKind +func (r *repositoryResources) FindResourcePath(ctx context.Context, name string, gvk schema.GroupVersionKind) (string, error) { + // Use ForKind to get the dynamic client for this resource type + client, gvr, err := r.clients.ForKind(gvk) + if err != nil { + return "", fmt.Errorf("get client for kind %s: %w", gvk.Kind, err) + } + + // Get the specific resource by name using the dynamic client (already namespaced) + obj, err := client.Get(ctx, name, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return "", fmt.Errorf("resource not found: %s/%s/%s", gvr.Group, gvr.Resource, name) + } + return "", fmt.Errorf("failed to get resource %s/%s/%s: %w", gvr.Group, gvr.Resource, name, err) + } + + // Extract the source path from annotations + annotations := obj.GetAnnotations() + if annotations == nil { + return "", fmt.Errorf("resource %s/%s/%s has no annotations", gvr.Group, gvr.Resource, name) + } + + sourcePath, exists := annotations[utils.AnnoKeySourcePath] + if !exists || sourcePath == "" { + return "", fmt.Errorf("resource %s/%s/%s has no source path annotation", gvr.Group, gvr.Resource, name) + } + + // For folder resources, ensure the path has a trailing slash for proper deletion + if gvk.Kind == "Folder" && !safepath.IsDir(sourcePath) { + sourcePath = sourcePath + "/" + } + + return sourcePath, nil +} + func NewRepositoryResourcesFactory(parsers ParserFactory, clients ClientFactory, lister ResourceLister) RepositoryResourcesFactory { return &repositoryResourcesFactory{parsers, clients, lister} } diff --git a/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go b/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go index 9bf9473d180..3e55762e261 100644 --- a/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go +++ b/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery v2.52.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/repository_resources_mock.go b/pkg/registry/apis/provisioning/resources/repository_resources_mock.go index 7e11d9b9eda..9ff7171f68c 100644 --- a/pkg/registry/apis/provisioning/resources/repository_resources_mock.go +++ b/pkg/registry/apis/provisioning/resources/repository_resources_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery v2.52.4. DO NOT EDIT. package resources @@ -181,6 +181,64 @@ func (_c *MockRepositoryResources_EnsureFolderTreeExists_Call) RunAndReturn(run return _c } +// FindResourcePath provides a mock function with given fields: ctx, name, gvk +func (_m *MockRepositoryResources) FindResourcePath(ctx context.Context, name string, gvk schema.GroupVersionKind) (string, error) { + ret := _m.Called(ctx, name, gvk) + + if len(ret) == 0 { + panic("no return value specified for FindResourcePath") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, schema.GroupVersionKind) (string, error)); ok { + return rf(ctx, name, gvk) + } + if rf, ok := ret.Get(0).(func(context.Context, string, schema.GroupVersionKind) string); ok { + r0 = rf(ctx, name, gvk) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, schema.GroupVersionKind) error); ok { + r1 = rf(ctx, name, gvk) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRepositoryResources_FindResourcePath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindResourcePath' +type MockRepositoryResources_FindResourcePath_Call struct { + *mock.Call +} + +// FindResourcePath is a helper method to define mock.On call +// - ctx context.Context +// - name string +// - gvk schema.GroupVersionKind +func (_e *MockRepositoryResources_Expecter) FindResourcePath(ctx interface{}, name interface{}, gvk interface{}) *MockRepositoryResources_FindResourcePath_Call { + return &MockRepositoryResources_FindResourcePath_Call{Call: _e.mock.On("FindResourcePath", ctx, name, gvk)} +} + +func (_c *MockRepositoryResources_FindResourcePath_Call) Run(run func(ctx context.Context, name string, gvk schema.GroupVersionKind)) *MockRepositoryResources_FindResourcePath_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(schema.GroupVersionKind)) + }) + return _c +} + +func (_c *MockRepositoryResources_FindResourcePath_Call) Return(_a0 string, _a1 error) *MockRepositoryResources_FindResourcePath_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRepositoryResources_FindResourcePath_Call) RunAndReturn(run func(context.Context, string, schema.GroupVersionKind) (string, error)) *MockRepositoryResources_FindResourcePath_Call { + _c.Call.Return(run) + return _c +} + // List provides a mock function with given fields: ctx func (_m *MockRepositoryResources) List(ctx context.Context) (*v0alpha1.ResourceList, error) { ret := _m.Called(ctx) diff --git a/pkg/registry/apis/provisioning/resources/repository_test.go b/pkg/registry/apis/provisioning/resources/repository_test.go new file mode 100644 index 00000000000..c53512c3c0a --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/repository_test.go @@ -0,0 +1,437 @@ +package resources + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" + + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +func TestRepositoryResources_FindResourcePath(t *testing.T) { + tests := []struct { + name string + resourceName string + gvk schema.GroupVersionKind + expectedGVR schema.GroupVersionResource + forKindError error + getError error + resourceObj *unstructured.Unstructured + expectedPath string + expectedError string + }{ + { + name: "dashboard found successfully", + resourceName: "test-dashboard", + gvk: schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }, + expectedGVR: schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Resource: "dashboards", + }, + resourceObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "test-dashboard", + "namespace": "test-namespace", + "annotations": map[string]interface{}{ + utils.AnnoKeySourcePath: "dashboards/test-dashboard.json", + }, + }, + }, + }, + expectedPath: "dashboards/test-dashboard.json", + }, + { + name: "folder found successfully", + resourceName: "test-folder", + gvk: schema.GroupVersionKind{ + Group: "folder.grafana.app", + Kind: "Folder", + }, + expectedGVR: schema.GroupVersionResource{ + Group: "folder.grafana.app", + Version: "v0alpha1", + Resource: "folders", + }, + resourceObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "test-folder", + "namespace": "test-namespace", + "annotations": map[string]interface{}{ + utils.AnnoKeySourcePath: "folders/test-folder", + }, + }, + }, + }, + expectedPath: "folders/test-folder/", // Trailing slash added for folder resources + }, + { + name: "ForKind fails", + resourceName: "test-dashboard", + gvk: schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }, + forKindError: errors.New("kind not found"), + expectedError: "get client for kind Dashboard: kind not found", + }, + { + name: "resource not found", + resourceName: "nonexistent-dashboard", + gvk: schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }, + expectedGVR: schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Resource: "dashboards", + }, + getError: apierrors.NewNotFound(schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, "nonexistent-dashboard"), + expectedError: "resource not found: dashboard.grafana.app/dashboards/nonexistent-dashboard", + }, + { + name: "Get operation fails with other error", + resourceName: "test-dashboard", + gvk: schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }, + expectedGVR: schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Resource: "dashboards", + }, + getError: errors.New("internal server error"), + expectedError: "failed to get resource dashboard.grafana.app/dashboards/test-dashboard: internal server error", + }, + { + name: "resource has no annotations", + resourceName: "test-dashboard", + gvk: schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }, + expectedGVR: schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Resource: "dashboards", + }, + resourceObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "test-dashboard", + "namespace": "test-namespace", + // No annotations + }, + }, + }, + expectedError: "resource dashboard.grafana.app/dashboards/test-dashboard has no annotations", + }, + { + name: "resource has empty annotations", + resourceName: "test-dashboard", + gvk: schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }, + expectedGVR: schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Resource: "dashboards", + }, + resourceObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "test-dashboard", + "namespace": "test-namespace", + "annotations": map[string]interface{}{}, + }, + }, + }, + expectedError: "resource dashboard.grafana.app/dashboards/test-dashboard has no source path annotation", + }, + { + name: "resource has empty source path annotation", + resourceName: "test-dashboard", + gvk: schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }, + expectedGVR: schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Resource: "dashboards", + }, + resourceObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "test-dashboard", + "namespace": "test-namespace", + "annotations": map[string]interface{}{ + utils.AnnoKeySourcePath: "", // Empty path + }, + }, + }, + }, + expectedError: "resource dashboard.grafana.app/dashboards/test-dashboard has no source path annotation", + }, + { + name: "resource with nested folder path", + resourceName: "nested-dashboard", + gvk: schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }, + expectedGVR: schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Resource: "dashboards", + }, + resourceObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "nested-dashboard", + "namespace": "test-namespace", + "annotations": map[string]interface{}{ + utils.AnnoKeySourcePath: "team-a/subfolder/nested-dashboard.json", + }, + }, + }, + }, + expectedPath: "team-a/subfolder/nested-dashboard.json", + }, + { + name: "folder without trailing slash gets slash added", + resourceName: "test-folder", + gvk: schema.GroupVersionKind{ + Group: "folder.grafana.app", + Kind: "Folder", + }, + expectedGVR: schema.GroupVersionResource{ + Group: "folder.grafana.app", + Version: "v0alpha1", + Resource: "folders", + }, + resourceObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "test-folder", + "namespace": "test-namespace", + "annotations": map[string]interface{}{ + utils.AnnoKeySourcePath: "folders/test-folder", // No trailing slash + }, + }, + }, + }, + expectedPath: "folders/test-folder/", // Should have trailing slash added + }, + { + name: "folder with trailing slash keeps slash", + resourceName: "test-folder-2", + gvk: schema.GroupVersionKind{ + Group: "folder.grafana.app", + Kind: "Folder", + }, + expectedGVR: schema.GroupVersionResource{ + Group: "folder.grafana.app", + Version: "v0alpha1", + Resource: "folders", + }, + resourceObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "test-folder-2", + "namespace": "test-namespace", + "annotations": map[string]interface{}{ + utils.AnnoKeySourcePath: "folders/test-folder-2/", // Already has trailing slash + }, + }, + }, + }, + expectedPath: "folders/test-folder-2/", // Should keep existing trailing slash + }, + { + name: "non-folder resource keeps path unchanged", + resourceName: "test-dashboard", + gvk: schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", // Not a folder + }, + expectedGVR: schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + Resource: "dashboards", + }, + resourceObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "test-dashboard", + "namespace": "test-namespace", + "annotations": map[string]interface{}{ + utils.AnnoKeySourcePath: "dashboards/test-dashboard", // No trailing slash + }, + }, + }, + }, + expectedPath: "dashboards/test-dashboard", // Should remain unchanged for non-folders + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create mocks + mockClients := NewMockResourceClients(t) + mockClient := &MockDynamicResourceInterface{} + + // Create repository resources with mocked dependencies + resourcesManager := &ResourcesManager{ + clients: mockClients, + } + + repositoryResources := &repositoryResources{ + ResourcesManager: resourcesManager, + namespace: "test-namespace", + repoName: "test-repo", + } + + // Mock ForKind call + if tt.forKindError != nil { + mockClients.On("ForKind", tt.gvk).Return(nil, schema.GroupVersionResource{}, tt.forKindError) + } else { + mockClients.On("ForKind", tt.gvk).Return(mockClient, tt.expectedGVR, nil) + + // Mock Get call if ForKind succeeds + if tt.getError != nil { + mockClient.On("Get", mock.Anything, tt.resourceName, metav1.GetOptions{}, mock.Anything).Return(nil, tt.getError) + } else { + mockClient.On("Get", mock.Anything, tt.resourceName, metav1.GetOptions{}, mock.Anything).Return(tt.resourceObj, nil) + } + } + + // Execute the method + result, err := repositoryResources.FindResourcePath(context.Background(), tt.resourceName, tt.gvk) + + // Assert results + if tt.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + require.Empty(t, result) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedPath, result) + } + + // Verify all mocks were called as expected + mockClients.AssertExpectations(t) + if tt.forKindError == nil { + mockClient.AssertExpectations(t) + } + }) + } +} + +// MockDynamicResourceInterface is a mock for dynamic.ResourceInterface +type MockDynamicResourceInterface struct { + mock.Mock +} + +func (m *MockDynamicResourceInterface) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) { + args := m.Called(ctx, obj, options, subresources) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*unstructured.Unstructured), args.Error(1) +} + +func (m *MockDynamicResourceInterface) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) { + args := m.Called(ctx, obj, options, subresources) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*unstructured.Unstructured), args.Error(1) +} + +func (m *MockDynamicResourceInterface) UpdateStatus(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions) (*unstructured.Unstructured, error) { + args := m.Called(ctx, obj, options) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*unstructured.Unstructured), args.Error(1) +} + +func (m *MockDynamicResourceInterface) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error { + args := m.Called(ctx, name, options, subresources) + return args.Error(0) +} + +func (m *MockDynamicResourceInterface) DeleteCollection(ctx context.Context, options metav1.DeleteOptions, listOptions metav1.ListOptions) error { + args := m.Called(ctx, options, listOptions) + return args.Error(0) +} + +func (m *MockDynamicResourceInterface) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { + args := m.Called(ctx, name, options, subresources) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*unstructured.Unstructured), args.Error(1) +} + +func (m *MockDynamicResourceInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) { + args := m.Called(ctx, opts) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*unstructured.UnstructuredList), args.Error(1) +} + +func (m *MockDynamicResourceInterface) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + args := m.Called(ctx, opts) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(watch.Interface), args.Error(1) +} + +func (m *MockDynamicResourceInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) { + args := m.Called(ctx, name, pt, data, options, subresources) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*unstructured.Unstructured), args.Error(1) +} + +func (m *MockDynamicResourceInterface) Apply(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions, subresources ...string) (*unstructured.Unstructured, error) { + args := m.Called(ctx, name, obj, options, subresources) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*unstructured.Unstructured), args.Error(1) +} + +func (m *MockDynamicResourceInterface) ApplyStatus(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions) (*unstructured.Unstructured, error) { + args := m.Called(ctx, name, obj, options) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*unstructured.Unstructured), args.Error(1) +} + +// Ensure MockDynamicResourceInterface implements dynamic.ResourceInterface +var _ dynamic.ResourceInterface = (*MockDynamicResourceInterface)(nil) diff --git a/pkg/services/apiserver/client/discovery.go b/pkg/services/apiserver/client/discovery.go index 3c9b91f1ab4..db5fb26cccb 100644 --- a/pkg/services/apiserver/client/discovery.go +++ b/pkg/services/apiserver/client/discovery.go @@ -14,6 +14,7 @@ type DiscoveryClient interface { GetResourceForKind(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) GetKindForResource(gvr schema.GroupVersionResource) (schema.GroupVersionKind, error) GetPreferredVesion(gr schema.GroupResource) (schema.GroupVersionResource, schema.GroupVersionKind, error) + GetPreferredVersionForKind(gk schema.GroupKind) (schema.GroupVersionResource, schema.GroupVersionKind, error) } type DiscoveryClientImpl struct { @@ -92,3 +93,44 @@ func (d *DiscoveryClientImpl) GetPreferredVesion(gr schema.GroupResource) (schem } return schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf("preferred version not found for %s", gr.String()) } + +func (d *DiscoveryClientImpl) GetPreferredVersionForKind(gk schema.GroupKind) (schema.GroupVersionResource, schema.GroupVersionKind, error) { + apiList, err := d.ServerPreferredResources() + if err != nil { + return schema.GroupVersionResource{}, schema.GroupVersionKind{}, err + } + for _, apis := range apiList { + // Check if this API group matches our target group + if !strings.HasPrefix(apis.GroupVersion, gk.Group) { + continue + } + + // Parse the group/version + var group, version string + if strings.Contains(apis.GroupVersion, "/") { + parts := strings.Split(apis.GroupVersion, "/") + group = parts[0] + version = parts[1] + } else { + // Core API group (e.g., "v1") + group = "" + version = apis.GroupVersion + } + + // Look for our target kind in this API group version + for _, resource := range apis.APIResources { + if resource.Kind == gk.Kind { + return schema.GroupVersionResource{ + Group: group, + Version: version, + Resource: resource.Name, + }, schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: resource.Kind, + }, nil + } + } + } + return schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf("preferred version not found for kind %s in group %s", gk.Kind, gk.Group) +} diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index bd54e67e82a..c4d341ea41d 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -2645,6 +2645,18 @@ "ref": { "description": "Ref to the branch or commit hash to delete from", "type": "string" + }, + "resources": { + "description": "Resources to delete This option has been created because currently the frontend does not use standarized app platform APIs. For performance and API consistency reasons, the preferred option is it to use the paths.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ResourceRef" + } + ] + } } } }, @@ -3816,6 +3828,23 @@ } } }, + "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ResourceRef": { + "type": "object", + "properties": { + "group": { + "description": "Group is the group of the resource, such as \"dashboard.grafana.app\".", + "type": "string" + }, + "kind": { + "description": "Kind is the type of resource, for example, \"Dashboard\".", + "type": "string" + }, + "name": { + "description": "Name is the name of the resource, such as a dashboard UID.", + "type": "string" + } + } + }, "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ResourceRepositoryInfo": { "type": "object", "required": [ diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index 15049df999d..ecd3f5755c1 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -927,6 +927,340 @@ func TestIntegrationProvisioning_DeleteJob(t *testing.T) { require.Equal(t, 0, len(dashboards.Items), "should have 0 dashboards after deleting all") }) + t.Run("delete by resource reference", func(t *testing.T) { + // Create modified test files with unique UIDs for ResourceRef testing + // Read and modify the testdata files to have unique UIDs that don't conflict with existing resources + allPanelsContent := helper.LoadFile("testdata/all-panels.json") + textOptionsContent := helper.LoadFile("testdata/text-options.json") + timelineDemoContent := helper.LoadFile("testdata/timeline-demo.json") + + // Modify UIDs to be unique for ResourceRef tests + allPanelsModified := strings.Replace(string(allPanelsContent), `"uid": "n1jR8vnnz"`, `"uid": "resourceref1"`, 1) + textOptionsModified := strings.Replace(string(textOptionsContent), `"uid": "WZ7AhQiVz"`, `"uid": "resourceref2"`, 1) + timelineDemoModified := strings.Replace(string(timelineDemoContent), `"uid": "mIJjFy8Kz"`, `"uid": "resourceref3"`, 1) + + // Create temporary files and copy them to the provisioning path + tmpDir := t.TempDir() + tmpFile1 := filepath.Join(tmpDir, "resource-test-1.json") + tmpFile2 := filepath.Join(tmpDir, "resource-test-2.json") + tmpFile3 := filepath.Join(tmpDir, "resource-test-3.json") + + require.NoError(t, os.WriteFile(tmpFile1, []byte(allPanelsModified), 0644)) + require.NoError(t, os.WriteFile(tmpFile2, []byte(textOptionsModified), 0644)) + require.NoError(t, os.WriteFile(tmpFile3, []byte(timelineDemoModified), 0644)) + + // Copy the temporary files to the provisioning path + helper.CopyToProvisioningPath(t, tmpFile1, "resource-test-1.json") // UID: resourceref1 + helper.CopyToProvisioningPath(t, tmpFile2, "resource-test-2.json") // UID: resourceref2 + helper.CopyToProvisioningPath(t, tmpFile3, "nested/resource-test-3.json") // UID: resourceref3 + + // Trigger sync to populate the new resources + helper.SyncAndWait(t, repo, nil) + + // Verify the new resources are created + dashboards, err := helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.GreaterOrEqual(t, len(dashboards.Items), 3, "should have at least 3 dashboards after adding test resources") + + // Debug: print the actual dashboard names/UIDs to verify they match our expectations + for i, dashboard := range dashboards.Items { + t.Logf("Dashboard %d: name=%s, UID=%s", i+1, dashboard.GetName(), dashboard.GetUID()) + } + + t.Run("delete single dashboard by resource reference", func(t *testing.T) { + // Create delete job for single dashboard using ResourceRef + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(asJSON(&provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "resourceref1", // UID from modified all-panels.json + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + })). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error(), "should be able to create delete job with ResourceRef") + + // Wait for job to complete + helper.AwaitJobs(t, repo) + + // Verify corresponding file is deleted from repository + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "resource-test-1.json") + require.Error(t, err, "file should be deleted from repository") + require.True(t, apierrors.IsNotFound(err), "should be not found error") + + // Verify dashboard is removed from Grafana (check count like other successful tests) + dashboards, err = helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 2, len(dashboards.Items), "should have 2 dashboards after deleting 1 from 3") + + // Verify other resources still exist + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "resource-test-2.json") + require.NoError(t, err, "other files should still exist") + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "nested", "resource-test-3.json") + require.NoError(t, err, "nested files should still exist") + }) + + t.Run("delete multiple resources by reference", func(t *testing.T) { + // Create delete job for multiple resources using ResourceRef + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(asJSON(&provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "resourceref2", // UID from modified text-options.json + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + { + Name: "resourceref3", // UID from modified timeline-demo.json + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + })). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error(), "should be able to create delete job with multiple ResourceRefs") + + // Wait for job to complete + helper.AwaitJobs(t, repo) + + // Verify both dashboards are removed from Grafana + _, err = helper.DashboardsV1.Resource.Get(ctx, "resourceref2", metav1.GetOptions{}) + require.Error(t, err, "text-options dashboard should be deleted") + require.True(t, apierrors.IsNotFound(err)) + + _, err = helper.DashboardsV1.Resource.Get(ctx, "resourceref3", metav1.GetOptions{}) + require.Error(t, err, "timeline-demo dashboard should be deleted") + require.True(t, apierrors.IsNotFound(err)) + + // Verify corresponding files are deleted from repository + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "resource-test-2.json") + require.Error(t, err, "resource-test-2.json should be deleted") + + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "nested", "resource-test-3.json") + require.Error(t, err, "nested/resource-test-3.json should be deleted") + + // Verify specific dashboards are removed from Grafana + dashboards, err = helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 0, len(dashboards.Items), "should have 0 dashboards after deleting 2 more (2 -> 0)") + }) + + t.Run("mixed deletion - paths and resources", func(t *testing.T) { + // Setup fresh resources for mixed test - reuse the modified content with unique UIDs + tmpMixed1 := filepath.Join(tmpDir, "mixed-test-1.json") + tmpMixed2 := filepath.Join(tmpDir, "mixed-test-2.json") + tmpMixed3 := filepath.Join(tmpDir, "mixed-test-3.json") + + require.NoError(t, os.WriteFile(tmpMixed1, []byte(allPanelsModified), 0644)) + require.NoError(t, os.WriteFile(tmpMixed2, []byte(textOptionsModified), 0644)) + require.NoError(t, os.WriteFile(tmpMixed3, []byte(timelineDemoModified), 0644)) + + helper.CopyToProvisioningPath(t, tmpMixed1, "mixed-test-1.json") // UID: resourceref1 + helper.CopyToProvisioningPath(t, tmpMixed2, "mixed-test-2.json") // UID: resourceref2 + helper.CopyToProvisioningPath(t, tmpMixed3, "mixed-test-3.json") // UID: resourceref3 + + helper.SyncAndWait(t, repo, nil) + + // Create delete job that combines both paths and resource references + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(asJSON(&provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Paths: []string{"mixed-test-1.json"}, // Delete by path + Resources: []provisioning.ResourceRef{ + { + Name: "resourceref2", // Delete by resource reference + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + })). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error(), "should be able to create mixed delete job") + + // Wait for job to complete + helper.AwaitJobs(t, repo) + + // Verify both targeted resources are deleted from Grafana + _, err = helper.DashboardsV1.Resource.Get(ctx, "resourceref1", metav1.GetOptions{}) + require.Error(t, err, "dashboard deleted by path should be removed") + + _, err = helper.DashboardsV1.Resource.Get(ctx, "resourceref2", metav1.GetOptions{}) + require.Error(t, err, "dashboard deleted by resource ref should be removed") + + // Verify the untargeted resource still exists + _, err = helper.DashboardsV1.Resource.Get(ctx, "resourceref3", metav1.GetOptions{}) + require.NoError(t, err, "untargeted dashboard should still exist") + + // Verify files are properly deleted/preserved in repository + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "mixed-test-1.json") + require.Error(t, err, "file deleted by path should be removed") + + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "mixed-test-2.json") + require.Error(t, err, "file for resource deleted by ref should be removed") + + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "mixed-test-3.json") + require.NoError(t, err, "untargeted file should still exist") + }) + + t.Run("delete folder by resource reference", func(t *testing.T) { + // Create a dashboard inside a folder to automatically create the folder structure + // This follows the same pattern as other tests in this file + testDashboard := strings.Replace(string(allPanelsContent), `"uid": "n1jR8vnnz"`, `"uid": "folder-dash"`, 1) + + // Write the modified dashboard to a temporary file first + tmpFolderDash := filepath.Join(tmpDir, "folder-dashboard.json") + require.NoError(t, os.WriteFile(tmpFolderDash, []byte(testDashboard), 0644)) + + // Copy it to the folder structure using the helper + helper.CopyToProvisioningPath(t, tmpFolderDash, "test-folder/dashboard-in-folder.json") + + // Sync to create the folder and its contents + helper.SyncAndWait(t, repo, nil) + + // Verify folder was created in Grafana as a Folder resource + folders, err := helper.Folders.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + + var testFolder *unstructured.Unstructured + for _, folder := range folders.Items { + // Folder names are generated with suffixes, so check if it starts with "test-folder" + if strings.HasPrefix(folder.GetName(), "test-folder") { + testFolder = &folder + break + } + } + require.NotNil(t, testFolder, "test-folder should exist as a Folder resource") + testFolderName := testFolder.GetName() + + // Verify dashboard inside the folder exists + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "test-folder", "dashboard-in-folder.json") + require.NoError(t, err, "dashboard inside folder should exist") + + // Create delete job for the folder using ResourceRef (use the actual generated name) + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(asJSON(&provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: testFolderName, // Use the actual generated folder name + Kind: "Folder", + Group: "folder.grafana.app", + }, + }, + }, + })). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error(), "should be able to create delete job for folder") + + // Wait for job to complete + helper.AwaitJobs(t, repo) + + // Verify folder is deleted from Grafana + _, err = helper.Folders.Resource.Get(ctx, testFolderName, metav1.GetOptions{}) + require.Error(t, err, "folder should be deleted from Grafana") + require.True(t, apierrors.IsNotFound(err), "should be not found error") + + // Verify folder contents are also deleted from repository + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "test-folder", "dashboard-in-folder.json") + require.Error(t, err, "dashboard inside deleted folder should also be deleted") + require.True(t, apierrors.IsNotFound(err), "should be not found error") + }) + + t.Run("delete non-existent resource by reference", func(t *testing.T) { + // Create delete job for non-existent resource + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(asJSON(&provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "non-existent-uid", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + })). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error(), "should be able to create delete job") + + // Wait for job to complete - should record error but continue + require.EventuallyWithT(t, func(collect *assert.CollectT) { + list := &unstructured.UnstructuredList{} + err := helper.AdminREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Do(ctx).Into(list) + assert.NoError(collect, err, "should be able to list jobs") + assert.NotEmpty(collect, list.Items, "expect at least one job") + + // Find the most recent delete job + var deleteJob *unstructured.Unstructured + for _, elem := range list.Items { + assert.Equal(collect, repo, elem.GetLabels()["provisioning.grafana.app/repository"], "should have repo label") + + action := mustNestedString(elem.Object, "spec", "action") + if action == "delete" { + // Get the most recent one (they should be ordered by creation time) + deleteJob = &elem + } + } + if !assert.NotNil(collect, deleteJob, "should find a delete job") { + return + } + + state := mustNestedString(deleteJob.Object, "status", "state") + // The job should complete but record errors for individual resource resolution failures + if state == "error" || state == "completed" || state == "success" { + // Any of these states is acceptable - the key is that resource resolution errors are recorded + // and don't fail the entire job due to error-tolerant implementation + return + } + assert.Fail(collect, "job should complete or error, but got state: %s", state) + }, time.Second*10, time.Millisecond*100, "Expected delete job to handle non-existent resource") + }) + + // Repository cleanup is handled by the main test function + }) + t.Run("delete non-existent file", func(t *testing.T) { // Create delete job for non-existent file result := helper.AdminREST.Post(). diff --git a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts index 20a6c603f32..0cba5dbf96a 100644 --- a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts @@ -758,11 +758,21 @@ export type ObjectMeta = { Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ uid?: string; }; +export type ResourceRef = { + /** Group is the group of the resource, such as "dashboard.grafana.app". */ + group?: string; + /** Kind is the type of resource, for example, "Dashboard". */ + kind?: string; + /** Name is the name of the resource, such as a dashboard UID. */ + name?: string; +}; export type DeleteJobOptions = { /** Paths to be deleted. Examples: - dashboard.json (for a file) - a/b/c/other-dashboard.json (for a file) - nested/deep/ (for a directory) FIXME: we should validate this in admission hooks */ paths?: string[]; /** Ref to the branch or commit hash to delete from */ ref?: string; + /** Resources to delete This option has been created because currently the frontend does not use standarized app platform APIs. For performance and API consistency reasons, the preferred option is it to use the paths. */ + resources?: ResourceRef[]; }; export type MigrateJobOptions = { /** Preserve history (if possible) */ @@ -792,7 +802,6 @@ export type ExportJobOptions = { /** Message to use when committing the changes in a single commit */ message?: string; /** FIXME: we should validate this in admission hooks Prefix in target file system */ - /** Prefix in target file system */ path?: string; }; export type JobSpec = { From 73d64d3e465748cee17a58e717b1057f5be4550e Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:34:38 +0100 Subject: [PATCH 005/126] Undo/Redo: Add undo/redo support for duplication in custom grid layout (#108850) * Undo/Redo: Add undo/redo support for duplication in custom grid layout * Fix i18n --- .../DefaultGridLayoutManager.tsx | 40 +++++++++++++++---- public/locales/en-US/grafana.json | 1 + 2 files changed, 34 insertions(+), 7 deletions(-) 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 c4e1c481b0e..6fbeb6064ec 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -237,7 +237,11 @@ export class DefaultGridLayoutManager // when we duplicate a panel we don't want to clone the alert state delete panelData.state.data?.alertState; - const newPanel = new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }); + const newPanel = new VizPanel({ + ...panelState, + $data: panelData, + key: getVizPanelKeyForPanelId(newPanelId), + }); newGridItem = new DashboardGridItem({ x: gridItem.state.x, @@ -252,16 +256,38 @@ export class DefaultGridLayoutManager body: newPanel, }); - if (gridItem.parent instanceof SceneGridRow) { - const row = gridItem.parent; + // No undo/redo support in legacy edit mode + if (!config.featureToggles.dashboardNewLayouts) { + if (gridItem.parent instanceof SceneGridRow) { + const row = gridItem.parent; - row.setState({ children: [...row.state.children, newGridItem] }); - grid.forceRender(); + row.setState({ children: [...row.state.children, newGridItem] }); + grid.forceRender(); + return; + } + + grid.setState({ children: [...grid.state.children, newGridItem] }); + this.publishEvent(new NewObjectAddedToCanvasEvent(newPanel), true); return; } - grid.setState({ children: [...grid.state.children, newGridItem] }); - this.publishEvent(new NewObjectAddedToCanvasEvent(newPanel), true); + const parent = gridItem.parent instanceof SceneGridRow ? gridItem.parent : grid; + dashboardEditActions.edit({ + description: t('dashboard.edit-actions.duplicate-panel', 'Duplicate panel'), + addedObject: newGridItem.state.body, + source: this, + perform: () => { + const oldGridItemIndex = parent.state.children.indexOf(gridItem); + const newChildrenArray = [...parent.state.children]; + newChildrenArray.splice(oldGridItemIndex + 1, 0, newGridItem); + parent.setState({ children: newChildrenArray }); + }, + undo: () => { + parent.setState({ + children: parent.state.children.filter((child) => child !== newGridItem), + }); + }, + }); } public duplicate(): DashboardLayoutManager { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 722e201bae1..2c50b0db303 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4567,6 +4567,7 @@ "edit-actions": { "add": "Add {{typeName}}", "add-conditional-rule": "Add {{ruleDescription}} rule", + "duplicate-panel": "Duplicate panel", "edit-query-result-rule": "Change query result rule", "edit-template-variable-rule": "Change template variable rule", "edit-time-range-rule": "Change time range rule", From ef9f9c2d8e670e42c7fafdca2adb0b4c268ff358 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 29 Jul 2025 07:51:37 -0500 Subject: [PATCH 006/126] Secrets service: Allow decrypt through gRPC connection (#108365) --- .../secrets/mocks/decrypt_service_mock.go | 45 ++++ .../apis/provisioning/secrets/repository.go | 4 +- .../apis/provisioning/secrets/secret.go | 12 +- .../apis/provisioning/secrets/secret_test.go | 19 +- .../secrets/secure_value_client_mock.go | 3 +- pkg/registry/apis/secret/contracts/decrypt.go | 4 + .../apis/secret/decrypt/grpc_client.go | 178 +++++++++++++ .../apis/secret/decrypt/local_client.go | 39 +++ pkg/registry/apis/secret/decrypt/service.go | 81 ++++-- .../apis/secret/decrypt/service_test.go | 243 ++++++++++++++++-- .../apis/secret/testutils/testutils.go | 19 +- pkg/server/wire_gen.go | 14 +- pkg/setting/setting_secrets_manager.go | 14 + pkg/storage/secret/metadata/decrypt_store.go | 28 +- .../secret/metadata/decrypt_store_test.go | 76 +++++- 15 files changed, 717 insertions(+), 62 deletions(-) create mode 100644 pkg/registry/apis/secret/decrypt/grpc_client.go create mode 100644 pkg/registry/apis/secret/decrypt/local_client.go diff --git a/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go b/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go index 14e2d028689..b5cca7ebe62 100644 --- a/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go +++ b/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go @@ -22,6 +22,51 @@ func (_m *MockDecryptService) EXPECT() *MockDecryptService_Expecter { return &MockDecryptService_Expecter{mock: &_m.Mock} } +// Close provides a mock function with no fields +func (_m *MockDecryptService) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockDecryptService_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type MockDecryptService_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *MockDecryptService_Expecter) Close() *MockDecryptService_Close_Call { + return &MockDecryptService_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *MockDecryptService_Close_Call) Run(run func()) *MockDecryptService_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockDecryptService_Close_Call) Return(_a0 error) *MockDecryptService_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockDecryptService_Close_Call) RunAndReturn(run func() error) *MockDecryptService_Close_Call { + _c.Call.Return(run) + return _c +} + // Decrypt provides a mock function with given fields: ctx, namespace, names func (_m *MockDecryptService) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]contracts.DecryptResult, error) { _va := make([]interface{}, len(names)) diff --git a/pkg/registry/apis/provisioning/secrets/repository.go b/pkg/registry/apis/provisioning/secrets/repository.go index c933d72bf92..29a470dd993 100644 --- a/pkg/registry/apis/provisioning/secrets/repository.go +++ b/pkg/registry/apis/provisioning/secrets/repository.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/services/featuremgmt" legacysecrets "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/setting" ) func ProvideRepositorySecrets( @@ -18,8 +19,9 @@ func ProvideRepositorySecrets( legacySecretsSvc legacysecrets.Service, secretsSvc contracts.SecureValueClient, decryptSvc secret.DecryptService, + cfg *setting.Cfg, ) RepositorySecrets { - return NewRepositorySecrets(features, NewSecretsService(secretsSvc, decryptSvc), NewSingleTenant(legacySecretsSvc)) + return NewRepositorySecrets(features, NewSecretsService(secretsSvc, decryptSvc, cfg), NewSingleTenant(legacySecretsSvc)) } //go:generate mockery --name RepositorySecrets --structname MockRepositorySecrets --inpackage --filename repository_secrets_mock.go --with-expecter diff --git a/pkg/registry/apis/provisioning/secrets/secret.go b/pkg/registry/apis/provisioning/secrets/secret.go index de3e43deb63..9127e7b7868 100644 --- a/pkg/registry/apis/provisioning/secrets/secret.go +++ b/pkg/registry/apis/provisioning/secrets/secret.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/setting" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -31,12 +32,14 @@ var _ Service = (*secretsService)(nil) type secretsService struct { secureValues SecureValueClient decryptSvc secret.DecryptService + cfg *setting.Cfg } -func NewSecretsService(secretsSvc SecureValueClient, decryptSvc secret.DecryptService) Service { +func NewSecretsService(secretsSvc SecureValueClient, decryptSvc secret.DecryptService, cfg *setting.Cfg) Service { return &secretsService{ secureValues: secretsSvc, decryptSvc: decryptSvc, + cfg: cfg, } } @@ -74,6 +77,11 @@ func (s *secretsService) Encrypt(ctx context.Context, namespace, name string, da return result.GetName(), nil } + decrypters := []string{svcName} + if s.cfg.SecretsManagement.DecryptGrafanaServiceName != "" { + decrypters = append(decrypters, s.cfg.SecretsManagement.DecryptGrafanaServiceName) + } + // Create the secret directly as unstructured secret := &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -86,7 +94,7 @@ func (s *secretsService) Encrypt(ctx context.Context, namespace, name string, da "spec": map[string]interface{}{ "description": "provisioning: " + name, "value": data, - "decrypters": []string{svcName}, + "decrypters": decrypters, }, }, } diff --git a/pkg/registry/apis/provisioning/secrets/secret_test.go b/pkg/registry/apis/provisioning/secrets/secret_test.go index 25b71bef1ad..e393ac7d8dd 100644 --- a/pkg/registry/apis/provisioning/secrets/secret_test.go +++ b/pkg/registry/apis/provisioning/secrets/secret_test.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets/mocks" "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -50,7 +51,7 @@ func TestNewSecretsService(t *testing.T) { mockSecretsSvc := NewMockSecureValueClient(t) mockDecryptSvc := &mocks.MockDecryptService{} - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, &setting.Cfg{}) assert.NotNil(t, svc) assert.IsType(t, &secretsService{}, svc) @@ -208,7 +209,7 @@ func TestSecretsService_Encrypt(t *testing.T) { tt.setupMocks(mockSecretsSvc, mockDecryptSvc, mockResourceInterface) - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, &setting.Cfg{}) ctx := context.Background() @@ -232,7 +233,7 @@ func TestSecretsService_Encrypt_ClientError(t *testing.T) { // Setup client to return error mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(nil, errors.New("client error")) - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, &setting.Cfg{}) ctx := context.Background() @@ -331,7 +332,7 @@ func TestSecretsService_Decrypt(t *testing.T) { tt.setupMocks(mockSecretsSvc, mockDecryptSvc) - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, &setting.Cfg{}) ctx := context.Background() @@ -368,7 +369,7 @@ func TestSecretsService_Decrypt_ServiceIdentityContext(t *testing.T) { "test-secret": mockResult, }, nil) - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, &setting.Cfg{}) ctx := context.Background() result, err := svc.Decrypt(ctx, "test-namespace", "test-secret") @@ -430,7 +431,7 @@ func TestSecretsService_Delete(t *testing.T) { tt.setupMocks(mockSecretsSvc, mockDecryptSvc, mockResourceInterface) - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, &setting.Cfg{}) ctx := context.Background() err := svc.Delete(ctx, tt.namespace, tt.secretName) @@ -515,7 +516,7 @@ func TestSecretsService_Encrypt_WithK8sNotFoundError(t *testing.T) { } mockResourceInterface.createErr = nil - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, &setting.Cfg{}) ctx := context.Background() result, err := svc.Encrypt(ctx, "test-namespace", "test-secret", "secret-data") @@ -536,7 +537,7 @@ func TestSecretsService_Delete_WithK8sNotFoundError(t *testing.T) { k8sNotFoundErr := apierrors.NewNotFound(schema.GroupResource{Group: "secret.grafana.app", Resource: "securevalues"}, "test-secret") mockResourceInterface.deleteErr = k8sNotFoundErr - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, &setting.Cfg{}) ctx := context.Background() err := svc.Delete(ctx, "test-namespace", "test-secret") @@ -557,7 +558,7 @@ func TestSecretsService_Delete_WithGrafanaNotFoundError(t *testing.T) { // Mock Delete call to return Grafana not found error mockResourceInterface.deleteErr = contracts.ErrSecureValueNotFound - svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, &setting.Cfg{}) ctx := context.Background() err := svc.Delete(ctx, "test-namespace", "test-secret") diff --git a/pkg/registry/apis/provisioning/secrets/secure_value_client_mock.go b/pkg/registry/apis/provisioning/secrets/secure_value_client_mock.go index b197c783e07..e00d66395ef 100644 --- a/pkg/registry/apis/provisioning/secrets/secure_value_client_mock.go +++ b/pkg/registry/apis/provisioning/secrets/secure_value_client_mock.go @@ -5,8 +5,9 @@ package secrets import ( context "context" - mock "github.com/stretchr/testify/mock" dynamic "k8s.io/client-go/dynamic" + + mock "github.com/stretchr/testify/mock" ) // MockSecureValueClient is an autogenerated mock type for the SecureValueClient type diff --git a/pkg/registry/apis/secret/contracts/decrypt.go b/pkg/registry/apis/secret/contracts/decrypt.go index 9c0d8e8eb2b..32f9f7f6c1d 100644 --- a/pkg/registry/apis/secret/contracts/decrypt.go +++ b/pkg/registry/apis/secret/contracts/decrypt.go @@ -8,6 +8,9 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" ) +// HeaderGrafanaServiceIdentityName is used to pass the service identity in the gRPC request metadata. +const HeaderGrafanaServiceIdentityName = "X-Grafana-Service-Identity-Name" + var ( ErrDecryptNotFound = errors.New("not found") ErrDecryptNotAuthorized = errors.New("not authorized") @@ -27,6 +30,7 @@ type DecryptAuthorizer interface { // DecryptService is the inferface for the decrypt service. type DecryptService interface { Decrypt(ctx context.Context, namespace string, names ...string) (map[string]DecryptResult, error) + Close() error } // DecryptResult is the (union) result of a decryption operation. diff --git a/pkg/registry/apis/secret/decrypt/grpc_client.go b/pkg/registry/apis/secret/decrypt/grpc_client.go new file mode 100644 index 00000000000..9851eec4526 --- /dev/null +++ b/pkg/registry/apis/secret/decrypt/grpc_client.go @@ -0,0 +1,178 @@ +package decrypt + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "os" + + "github.com/fullstorydev/grpchan" + authnlib "github.com/grafana/authlib/authn" + claims "github.com/grafana/authlib/types" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + + decryptv1beta1 "github.com/grafana/grafana/apps/secret/decrypt/v1beta1" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" +) + +type GRPCDecryptClient struct { + conn *grpc.ClientConn + client decryptv1beta1.SecureValueDecrypterClient +} + +var _ contracts.DecryptService = &GRPCDecryptClient{} + +type TLSConfig struct { + UseTLS bool + CertFile string + KeyFile string + CAFile string + ServerName string + InsecureSkipVerify bool +} + +func NewGRPCDecryptClient(tokenExchanger authnlib.TokenExchanger, tracer trace.Tracer, namespace, address string) (*GRPCDecryptClient, error) { + return NewGRPCDecryptClientWithTLS(tokenExchanger, tracer, namespace, address, TLSConfig{}) +} + +func NewGRPCDecryptClientWithTLS( + tokenExchanger authnlib.TokenExchanger, + tracer trace.Tracer, + namespace string, + address string, + tlsConfig TLSConfig, +) (*GRPCDecryptClient, error) { + var opts []grpc.DialOption + if tlsConfig.UseTLS { + creds, err := createTLSCredentials(tlsConfig) + if err != nil { + return nil, fmt.Errorf("failed to setup TLS: %w", err) + } + + opts = append(opts, grpc.WithTransportCredentials(creds)) + } else { + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + + conn, err := grpc.NewClient(address, opts...) + if err != nil { + return nil, fmt.Errorf("failed to connect to grpc decrypt server at %s: %w", address, err) + } + + tokenExchangerInterceptor := authnlib.NewGrpcClientInterceptor( + tokenExchanger, + authnlib.WithClientInterceptorTracer(tracer), + authnlib.WithClientInterceptorNamespace(namespace), + authnlib.WithClientInterceptorAudience([]string{secretv1beta1.APIGroup}), + ) + + clientConn := grpchan.InterceptClientConn( + conn, + tokenExchangerInterceptor.UnaryClientInterceptor, + tokenExchangerInterceptor.StreamClientInterceptor, + ) + + return &GRPCDecryptClient{ + conn: conn, + client: decryptv1beta1.NewSecureValueDecrypterClient(clientConn), + }, nil +} + +func createTLSCredentials(config TLSConfig) (credentials.TransportCredentials, error) { + tlsConfig := &tls.Config{} + + if config.CAFile != "" { + caCert, err := os.ReadFile(config.CAFile) + if err != nil { + return nil, fmt.Errorf("failed to read CA: %w", err) + } + + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("failed to append CA") + } + tlsConfig.RootCAs = caCertPool + } + + if config.CertFile != "" && config.KeyFile != "" { + cert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile) + if err != nil { + return nil, fmt.Errorf("failed to load client certificate: %w", err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + + if config.ServerName != "" { + tlsConfig.ServerName = config.ServerName + } + + if config.InsecureSkipVerify { + tlsConfig.InsecureSkipVerify = true + } + + return credentials.NewTLS(tlsConfig), nil +} + +func (g *GRPCDecryptClient) Close() error { + if g.conn != nil { + return g.conn.Close() + } + return nil +} + +func (g *GRPCDecryptClient) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]contracts.DecryptResult, error) { + authInfo, ok := claims.AuthInfoFrom(ctx) + if !ok { + return nil, errors.New("missing auth info in context") + } + + // Up until here the identity is the one set by the internal service, but when the request goes out to the gRPC server, + // the aggregator will use the access token which contains a different service identity for grafana as a whole. + // This is used for logging purposes only. + serviceIdentityList, ok := authInfo.GetExtra()[authnlib.ServiceIdentityKey] + if !ok || len(serviceIdentityList) != 1 { + return nil, errors.New("invalid service identity in auth info") + } + + serviceIdentity := serviceIdentityList[0] + if len(serviceIdentity) == 0 { + return nil, errors.New("empty service identity in auth info") + } + + req := &decryptv1beta1.SecureValueDecryptRequest{ + Namespace: namespace, + Names: names, + } + + // Decryption will still use the service identity from the auth token, + // but we also pass the service identity from the request metadata for auditing purposes. + md := metadata.New(map[string]string{ + contracts.HeaderGrafanaServiceIdentityName: serviceIdentity, + }) + ctx = metadata.NewOutgoingContext(ctx, md) + + resp, err := g.client.DecryptSecureValues(ctx, req) + if err != nil { + return nil, fmt.Errorf("grpc decrypt failed: %w", err) + } + + results := make(map[string]contracts.DecryptResult, len(resp.GetDecryptedValues())) + + for name, result := range resp.GetDecryptedValues() { + if result.GetErrorMessage() != "" { + results[name] = contracts.NewDecryptResultErr(errors.New(result.GetErrorMessage())) + } else { + exposedSecureValue := secretv1beta1.NewExposedSecureValue(result.GetValue()) + results[name] = contracts.NewDecryptResultValue(&exposedSecureValue) + } + } + + return results, nil +} diff --git a/pkg/registry/apis/secret/decrypt/local_client.go b/pkg/registry/apis/secret/decrypt/local_client.go new file mode 100644 index 00000000000..6aa3f89ebe6 --- /dev/null +++ b/pkg/registry/apis/secret/decrypt/local_client.go @@ -0,0 +1,39 @@ +package decrypt + +import ( + "context" + + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" +) + +type LocalDecryptClient struct { + decryptStorage contracts.DecryptStorage +} + +var _ contracts.DecryptService = &LocalDecryptClient{} + +func NewLocalDecryptClient(decryptStorage contracts.DecryptStorage) (*LocalDecryptClient, error) { + return &LocalDecryptClient{ + decryptStorage: decryptStorage, + }, nil +} + +func (c *LocalDecryptClient) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]contracts.DecryptResult, error) { + results := make(map[string]contracts.DecryptResult, len(names)) + + for _, name := range names { + exposedSecureValue, err := c.decryptStorage.Decrypt(ctx, xkube.Namespace(namespace), name) + if err != nil { + results[name] = contracts.NewDecryptResultErr(err) + } else { + results[name] = contracts.NewDecryptResultValue(&exposedSecureValue) + } + } + + return results, nil +} + +func (c *LocalDecryptClient) Close() error { + return nil +} diff --git a/pkg/registry/apis/secret/decrypt/service.go b/pkg/registry/apis/secret/decrypt/service.go index 38556c0c4f9..91706a2aae1 100644 --- a/pkg/registry/apis/secret/decrypt/service.go +++ b/pkg/registry/apis/secret/decrypt/service.go @@ -1,33 +1,78 @@ package decrypt import ( - "context" + "fmt" + + authnlib "github.com/grafana/authlib/authn" + claims "github.com/grafana/authlib/types" + "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/services/authn/grpcutils" + "github.com/grafana/grafana/pkg/setting" ) -type OSSDecryptService struct { - decryptStore contracts.DecryptStorage -} +func ProvideDecryptService(cfg *setting.Cfg, tracer trace.Tracer, decryptStorage contracts.DecryptStorage) (contracts.DecryptService, error) { + switch cfg.SecretsManagement.DecryptServerType { + case "grpc": + grpcClientConfig := grpcutils.ReadGrpcClientConfig(cfg) -func ProvideDecryptService(decryptStore contracts.DecryptStorage) contracts.DecryptService { - return &OSSDecryptService{ - decryptStore: decryptStore, - } -} + if cfg.SecretsManagement.DecryptServerAddress == "" { + return nil, fmt.Errorf("decrypt_server_address is required when decrypt_server_type is grpc") + } -func (d *OSSDecryptService) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]contracts.DecryptResult, error) { - results := make(map[string]contracts.DecryptResult, len(names)) + if grpcClientConfig.Token == "" || grpcClientConfig.TokenExchangeURL == "" { + return nil, fmt.Errorf("grpc_client_authentication.token and grpc_client_authentication.token_exchange_url are required when secrets_manager.decrypt_server_type is grpc") + } - for _, name := range names { - exposedSecureValue, err := d.decryptStore.Decrypt(ctx, xkube.Namespace(namespace), name) + nsInfo, err := claims.ParseNamespace(grpcClientConfig.TokenNamespace) if err != nil { - results[name] = contracts.NewDecryptResultErr(err) - } else { - results[name] = contracts.NewDecryptResultValue(&exposedSecureValue) + return nil, fmt.Errorf("failed to parse token namespace %v: %w", grpcClientConfig.TokenNamespace, err) + } + if nsInfo.OrgID < 1 { + return nil, fmt.Errorf("invalid token namepsace %v", grpcClientConfig.TokenNamespace) + } + + tokenExchangeClient, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{ + Token: grpcClientConfig.Token, + TokenExchangeURL: grpcClientConfig.TokenExchangeURL, + }) + if err != nil { + return nil, fmt.Errorf("failed to create token exchange client: %w", err) + } + + tlsConfig := readTLSFromConfig(cfg) + + client, err := NewGRPCDecryptClientWithTLS(tokenExchangeClient, tracer, grpcClientConfig.TokenNamespace, cfg.SecretsManagement.DecryptServerAddress, tlsConfig) + if err != nil { + return nil, fmt.Errorf("failed to create grpc decrypt client: %w", err) + } + + return client, nil + + case "local", "": + return NewLocalDecryptClient(decryptStorage) + } + + return nil, fmt.Errorf("unsupported storage type: %s", cfg.SecretsManagement.DecryptServerType) +} + +func readTLSFromConfig(cfg *setting.Cfg) TLSConfig { + if !cfg.SecretsManagement.DecryptServerUseTLS { + return TLSConfig{ + UseTLS: false, + InsecureSkipVerify: true, } } - return results, nil + apiServer := cfg.SectionWithEnvOverrides("grafana-apiserver") + + return TLSConfig{ + UseTLS: true, + CertFile: apiServer.Key("proxy_client_cert_file").MustString(""), + KeyFile: apiServer.Key("proxy_client_key_file").MustString(""), + CAFile: apiServer.Key("apiservice_ca_bundle_file").MustString(""), + ServerName: cfg.SecretsManagement.DecryptServerTLSServerName, + InsecureSkipVerify: cfg.SecretsManagement.DecryptServerTLSSkipVerify, + } } diff --git a/pkg/registry/apis/secret/decrypt/service_test.go b/pkg/registry/apis/secret/decrypt/service_test.go index 3203e3ebbe7..53091f27bc4 100644 --- a/pkg/registry/apis/secret/decrypt/service_test.go +++ b/pkg/registry/apis/secret/decrypt/service_test.go @@ -2,34 +2,52 @@ package decrypt import ( "context" + "crypto/tls" "errors" + "net/http" + "net/http/httptest" + "strings" "testing" + decryptv1beta1 "github.com/grafana/grafana/apps/secret/decrypt/v1beta1" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/madflojo/testcerts" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" + "golang.org/x/net/nettest" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/services/authn/clients" + "github.com/grafana/grafana/pkg/setting" ) func TestDecryptService(t *testing.T) { t.Parallel() ctx := context.Background() + tracer := noop.NewTracerProvider().Tracer("test") t.Run("when there are only errors from the storage, the service returns them in the map", func(t *testing.T) { t.Parallel() mockErr := errors.New("mock error") - mockStorage := &MockDecryptStorage{} + mockStorage := &mockDecryptStorage{} mockStorage.On("Decrypt", mock.Anything, mock.Anything, mock.Anything).Return(secretv1beta1.ExposedSecureValue(""), mockErr) decryptedValuesResp := map[string]contracts.DecryptResult{ "secure-value-1": contracts.NewDecryptResultErr(mockErr), } - decryptService := &OSSDecryptService{ - decryptStore: mockStorage, - } + cfg := setting.NewCfg() + cfg.SecretsManagement.DecryptServerType = "local" + + decryptService, err := ProvideDecryptService(cfg, tracer, mockStorage) + require.NoError(t, err) resp, err := decryptService.Decrypt(ctx, "default", "secure-value-1") require.NotNil(t, resp) @@ -40,7 +58,7 @@ func TestDecryptService(t *testing.T) { t.Run("when there is no error from the storage, it returns a map of the decrypted values", func(t *testing.T) { t.Parallel() - mockStorage := &MockDecryptStorage{} + mockStorage := &mockDecryptStorage{} // Set up the mock to return a different value for each name in the test exposedSecureValue1 := secretv1beta1.NewExposedSecureValue("value1") exposedSecureValue2 := secretv1beta1.NewExposedSecureValue("value2") @@ -54,9 +72,11 @@ func TestDecryptService(t *testing.T) { "secure-value-2": contracts.NewDecryptResultValue(&exposedSecureValue2), } - decryptService := &OSSDecryptService{ - decryptStore: mockStorage, - } + cfg := setting.NewCfg() + cfg.SecretsManagement.DecryptServerType = "local" + + decryptService, err := ProvideDecryptService(cfg, tracer, mockStorage) + require.NoError(t, err) resp, err := decryptService.Decrypt(ctx, "default", "secure-value-1", "secure-value-2") require.NotNil(t, resp) @@ -68,7 +88,7 @@ func TestDecryptService(t *testing.T) { t.Parallel() mockErr := errors.New("mock error") - mockStorage := &MockDecryptStorage{} + mockStorage := &mockDecryptStorage{} exposedSecureValue := secretv1beta1.NewExposedSecureValue("value") mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-1"). Return(exposedSecureValue, nil) @@ -80,22 +100,213 @@ func TestDecryptService(t *testing.T) { "secure-value-2": contracts.NewDecryptResultErr(mockErr), } - decryptService := &OSSDecryptService{ - decryptStore: mockStorage, - } + cfg := setting.NewCfg() + cfg.SecretsManagement.DecryptServerType = "local" + + decryptService, err := ProvideDecryptService(cfg, tracer, mockStorage) + require.NoError(t, err) resp, err := decryptService.Decrypt(ctx, "default", "secure-value-1", "secure-value-2") require.NotNil(t, resp) require.NoError(t, err) require.EqualValues(t, decryptedValuesResp, resp) }) + + t.Run("when storage type is unsupported, it returns an error", func(t *testing.T) { + t.Parallel() + + cfg := setting.NewCfg() + cfg.SecretsManagement.DecryptServerType = "unsupported" + + decryptService, err := ProvideDecryptService(cfg, tracer, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported storage type") + require.Nil(t, decryptService) + }) + + t.Run("when storage type is grpc but token exchange config is missing, it returns an error", func(t *testing.T) { + t.Parallel() + + cfg := setting.NewCfg() + cfg.SecretsManagement.DecryptServerType = "grpc" + cfg.SecretsManagement.DecryptServerAddress = "127.0.0.1:10000" + + _, err := ProvideDecryptService(cfg, tracer, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "grpc_client_authentication.token and grpc_client_authentication.token_exchange_url are required") + }) + + t.Run("when storage type is grpc but storage address is missing, it returns an error", func(t *testing.T) { + t.Parallel() + + cfg := setting.NewCfg() + cfg.SecretsManagement.DecryptServerType = "grpc" + + _, err := ProvideDecryptService(cfg, tracer, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "decrypt_server_address is required") + }) + + t.Run("happy path with grpc+tls server with fake toke exchanger and server", func(t *testing.T) { + t.Parallel() + + respTokenExchanged := "test-token" + tokenExchangeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := `{ + "data": { + "token": "` + respTokenExchanged + `" + } + }` + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(response)) + })) + t.Cleanup(tokenExchangeServer.Close) + + // Set up gRPC Server with TLS + listener, err := nettest.NewLocalListener("tcp") + require.NoError(t, err) + + certPaths := createX509TestDir(t) + + serverCert, err := tls.LoadX509KeyPair(certPaths.serverCert, certPaths.serverKey) + require.NoError(t, err) + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{serverCert}, + ClientAuth: tls.NoClientCert, + InsecureSkipVerify: false, + ServerName: "localhost", + } + + grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig))) + t.Cleanup(grpcServer.Stop) + + decryptServer := &mockDecryptServer{} + decryptServer.On("DecryptSecureValues", mock.Anything, mock.Anything).Return( + &decryptv1beta1.SecureValueDecryptResponseCollection{ + DecryptedValues: map[string]*decryptv1beta1.Result{ + "secure-value-1": { + Result: &decryptv1beta1.Result_Value{Value: "decrypted-value-1"}, + }, + }, + }, + nil, + ) + + decryptv1beta1.RegisterSecureValueDecrypterServer(grpcServer, decryptServer) + + go func() { + _ = grpcServer.Serve(listener) + <-t.Context().Done() + }() + + // Populate configuration with gRPC+TLS options and mock token exchanger + grafanaSvcIdentity := "svc-identity-decrypter" + namespace := "stacks-1234" + + cfg := setting.NewCfg() + cfg.SecretsManagement.DecryptServerType = "grpc" + cfg.SecretsManagement.DecryptServerAddress = listener.Addr().String() + cfg.SecretsManagement.DecryptGrafanaServiceName = grafanaSvcIdentity + cfg.SecretsManagement.DecryptServerUseTLS = true + cfg.SecretsManagement.DecryptServerTLSServerName = "localhost" + cfg.SecretsManagement.DecryptServerTLSSkipVerify = false + + grpcClientAuth := cfg.Raw.Section("grpc_client_authentication") + _, err = grpcClientAuth.NewKey("token", "test-token") + require.NoError(t, err) + _, err = grpcClientAuth.NewKey("token_exchange_url", tokenExchangeServer.URL) + require.NoError(t, err) + _, err = grpcClientAuth.NewKey("token_namespace", namespace) + require.NoError(t, err) + + apiServer := cfg.Raw.Section("grafana-apiserver") + _, err = apiServer.NewKey("proxy_client_cert_file", certPaths.clientCert) + require.NoError(t, err) + _, err = apiServer.NewKey("proxy_client_key_file", certPaths.clientKey) + require.NoError(t, err) + _, err = apiServer.NewKey("apiservice_ca_bundle_file", certPaths.ca) + require.NoError(t, err) + + // Create and test decryption, using the mock grpc server as we dont test the business logic here + decryptService, err := ProvideDecryptService(cfg, tracer, nil) + require.NoError(t, err) + require.NotNil(t, decryptService) + + t.Cleanup(func() { require.NoError(t, decryptService.Close()) }) + + svcIdentity := "provsysoning-test" + authCtx := identity.WithServiceIdentityContext(ctx, 1, identity.WithServiceIdentityName(svcIdentity)) + + result, err := decryptService.Decrypt(authCtx, namespace, "secure-value-1") + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, result, 1) + require.NotEmpty(t, result["secure-value-1"]) + + requestContext := decryptServer.Calls[0].Arguments[0].(context.Context) + + md, ok := metadata.FromIncomingContext(requestContext) + require.True(t, ok) + require.NotEmpty(t, md) + require.Equal(t, svcIdentity, md[strings.ToLower(contracts.HeaderGrafanaServiceIdentityName)][0]) + require.Equal(t, respTokenExchanged, md[strings.ToLower(clients.ExtJWTAuthenticationHeaderName)][0]) + }) } -type MockDecryptStorage struct { +type mockDecryptStorage struct { mock.Mock } -func (m *MockDecryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, name string) (secretv1beta1.ExposedSecureValue, error) { +func (m *mockDecryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, name string) (secretv1beta1.ExposedSecureValue, error) { args := m.Called(ctx, namespace, name) return args.Get(0).(secretv1beta1.ExposedSecureValue), args.Error(1) } + +type mockDecryptServer struct { + mock.Mock +} + +var _ decryptv1beta1.SecureValueDecrypterServer = (*mockDecryptServer)(nil) + +func (m *mockDecryptServer) DecryptSecureValues(ctx context.Context, req *decryptv1beta1.SecureValueDecryptRequest) (*decryptv1beta1.SecureValueDecryptResponseCollection, error) { + args := m.Called(ctx, req) + return args.Get(0).(*decryptv1beta1.SecureValueDecryptResponseCollection), args.Error(1) +} + +type certPaths struct { + clientCert string + clientKey string + serverCert string + serverKey string + ca string +} + +func createX509TestDir(t *testing.T) certPaths { + t.Helper() + + tmpDir := t.TempDir() + + ca := testcerts.NewCA() + caCertFile, _, err := ca.ToTempFile(tmpDir) + require.NoError(t, err) + + serverKp, err := ca.NewKeyPair("localhost") + require.NoError(t, err) + + serverCertFile, serverKeyFile, err := serverKp.ToTempFile(tmpDir) + require.NoError(t, err) + + clientKp, err := ca.NewKeyPair() + require.NoError(t, err) + clientCertFile, clientKeyFile, err := clientKp.ToTempFile(tmpDir) + require.NoError(t, err) + + return certPaths{ + clientCert: clientCertFile.Name(), + clientKey: clientKeyFile.Name(), + serverCert: serverCertFile.Name(), + serverKey: serverKeyFile.Name(), + ca: caCertFile.Name(), + } +} diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 448283abd09..f2d24b05905 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -59,6 +59,7 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { } tracer := noop.NewTracerProvider().Tracer("test") + testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) database := database.ProvideDatabase(testDB, tracer) @@ -77,11 +78,11 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { }) defaultKey := "SdlklWklckeLS" - cfg := &setting.Cfg{ - SecretsManagement: setting.SecretsManagerSettings{ - CurrentEncryptionProvider: "secret_key.v1", - ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": defaultKey}}, - }, + cfg := setting.NewCfg() + cfg.SecretsManagement = setting.SecretsManagerSettings{ + DecryptServerType: "local", + CurrentEncryptionProvider: "secret_key.v1", + ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": defaultKey}}, } store, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, nil) require.NoError(t, err) @@ -126,7 +127,13 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { decryptStorage, err := metadata.ProvideDecryptStorage(tracer, keeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, nil) require.NoError(t, err) - decryptService := decrypt.ProvideDecryptService(decryptStorage) + testCfg := setting.NewCfg() + testCfg.SecretsManagement = setting.SecretsManagerSettings{ + DecryptServerType: "local", + } + + decryptService, err := decrypt.ProvideDecryptService(testCfg, tracer, decryptStorage) + require.NoError(t, err) return Sut{ SecureValueService: secureValueService, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 481ad2b1845..5d13b55fafe 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -788,8 +788,11 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - v3 := decrypt.ProvideDecryptService(decryptStorage) - repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueClient, v3) + v3, err := decrypt.ProvideDecryptService(cfg, tracer, decryptStorage) + if err != nil { + return nil, err + } + repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueClient, v3, cfg) webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, repositorySecrets, factory, renderingService, resourceClient, eventualRestConfigProvider) v4 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder) apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, repositorySecrets, tracingService, v4) @@ -1346,8 +1349,11 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - v3 := decrypt.ProvideDecryptService(decryptStorage) - repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueClient, v3) + v3, err := decrypt.ProvideDecryptService(cfg, tracer, decryptStorage) + if err != nil { + return nil, err + } + repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueClient, v3, cfg) webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, repositorySecrets, factory, renderingService, resourceClient, eventualRestConfigProvider) v4 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder) apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, repositorySecrets, tracingService, v4) diff --git a/pkg/setting/setting_secrets_manager.go b/pkg/setting/setting_secrets_manager.go index b855307635f..043706dd182 100644 --- a/pkg/setting/setting_secrets_manager.go +++ b/pkg/setting/setting_secrets_manager.go @@ -15,12 +15,26 @@ type SecretsManagerSettings struct { // ConfiguredKMSProviders is a map of KMS providers found in the config file. The keys are in the format of ., and the values are a map of the properties in that section // In OSS, the provider type can only be "secret_key". In Enterprise, it can additionally be one of: "aws_kms", "azure_keyvault", "google_kms", "hashicorp_vault" ConfiguredKMSProviders map[string]map[string]string + + DecryptServerType string // "local" or "grpc" + DecryptServerUseTLS bool // Applicable for decrypt_server_type=grpc. Whether to use TLS for the decrypt server + DecryptServerTLSSkipVerify bool // Applicable for decrypt_server_type=grpc. Whether to skip TLS verification for the decrypt server + DecryptServerTLSServerName string // Applicable for decrypt_server_type=grpc. Server name to use for TLS verification + DecryptServerAddress string // Applicable for decrypt_server_type=grpc. Address for external secrets server + DecryptGrafanaServiceName string // Service name to use for background grafana decryption } func (cfg *Cfg) readSecretsManagerSettings() { secretsMgmt := cfg.Raw.Section("secrets_manager") cfg.SecretsManagement.CurrentEncryptionProvider = secretsMgmt.Key("encryption_provider").MustString(MisconfiguredProvider) + cfg.SecretsManagement.DecryptServerType = valueAsString(secretsMgmt, "decrypt_server_type", "local") + cfg.SecretsManagement.DecryptServerUseTLS = secretsMgmt.Key("decrypt_server_use_tls").MustBool(false) + cfg.SecretsManagement.DecryptServerTLSSkipVerify = secretsMgmt.Key("decrypt_server_tls_skip_verify").MustBool(false) + cfg.SecretsManagement.DecryptServerTLSServerName = valueAsString(secretsMgmt, "decrypt_server_tls_server_name", "") + cfg.SecretsManagement.DecryptServerAddress = valueAsString(secretsMgmt, "decrypt_server_address", "") + cfg.SecretsManagement.DecryptGrafanaServiceName = valueAsString(secretsMgmt, "decrypt_grafana_service_name", "") + // Extract available KMS providers from configuration sections providers := make(map[string]map[string]string) for _, section := range cfg.Raw.Sections() { diff --git a/pkg/storage/secret/metadata/decrypt_store.go b/pkg/storage/secret/metadata/decrypt_store.go index c1b402b2224..de98a817a86 100644 --- a/pkg/storage/secret/metadata/decrypt_store.go +++ b/pkg/storage/secret/metadata/decrypt_store.go @@ -7,12 +7,13 @@ import ( "time" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/logging" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/metadata" - "github.com/grafana/grafana-app-sdk/logging" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" @@ -65,14 +66,33 @@ func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, defer func() { span.SetAttributes(attribute.String("decrypter.identity", decrypterIdentity)) + args := []any{ + "namespace", namespace.String(), + "secret_name", name, + "decrypter_identity", decrypterIdentity, + } + + // The service identity used for decryption is always what is from the signed token, but if the request is + // coming from grafana, the service identity will be grafana, but the request metadata will contain + // additional service identity information (such as coming from the provisioning service in grafana). + // we do this for auditing purposes. + if md, ok := metadata.FromIncomingContext(ctx); ok { + if svcIdentities := md.Get(contracts.HeaderGrafanaServiceIdentityName); len(svcIdentities) > 0 { + args = append(args, "grafana_decrypter_identity", svcIdentities[0]) + span.SetAttributes(attribute.String("grafana_decrypter.identity", svcIdentities[0])) + } + } + if decryptErr == nil { - logging.FromContext(ctx).Info("Audit log:", "operation", "decrypt_secret_success", "namespace", namespace, "secret_name", name, "decrypter_identity", decrypterIdentity) + args = append(args, "operation", "decrypt_secret_success") } else { span.SetStatus(codes.Error, "Decrypt failed") span.RecordError(decryptErr) - - logging.FromContext(ctx).Info("Audit log:", "operation", "decrypt_secret_error", "namespace", namespace, "secret_name", name, "decrypter_identity", decrypterIdentity, "error", decryptErr) + args = append(args, "operation", "decrypt_secret_error", "error", decryptErr.Error()) } + + logging.FromContext(ctx).Info("Secrets Audit Log", args...) + success := decryptErr == nil s.metrics.DecryptDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds()) s.metrics.DecryptRequestCount.WithLabelValues(strconv.FormatBool(success)).Inc() diff --git a/pkg/storage/secret/metadata/decrypt_store_test.go b/pkg/storage/secret/metadata/decrypt_store_test.go index 08e22e6bc6e..03d0daace36 100644 --- a/pkg/storage/secret/metadata/decrypt_store_test.go +++ b/pkg/storage/secret/metadata/decrypt_store_test.go @@ -6,7 +6,9 @@ import ( "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/logging" "github.com/stretchr/testify/require" + grpcmetadata "google.golang.org/grpc/metadata" "k8s.io/utils/ptr" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" @@ -248,10 +250,67 @@ func TestIntegrationDecrypt(t *testing.T) { require.Empty(t, exposed) }) - // TODO: add more tests for keeper failure scenarios, lets see how the async work will change this though. + t.Run("happy path with grpc metadata in request, also record the metadata as part of the service identity", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + tokenSvcIdentity := "svc" + stSvcIdentity := "st-svc" + + // Create auth context with proper permissions that match the decrypters + authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues:decrypt"}, tokenSvcIdentity, types.TypeUser) + + // Needs to be incoming because we are pretending we received the metadata from a gRPC request + ctx = grpcmetadata.NewIncomingContext(authCtx, grpcmetadata.New(map[string]string{ + contracts.HeaderGrafanaServiceIdentityName: stSvcIdentity, + })) + + // Setup service + sut := testutils.Setup(t) + + // Create a secure value + spec := secretv1beta1.SecureValueSpec{ + Description: "description", + Decrypters: []string{tokenSvcIdentity}, + Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")), + } + sv := &secretv1beta1.SecureValue{Spec: spec} + sv.Name = "sv-test" + sv.Namespace = "default" + + _, err := sut.CreateSv(ctx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + + fakeLogger := &mockLogger{} + + loggerCtx := logging.Context(ctx, fakeLogger) + + exposed, err := sut.DecryptStorage.Decrypt(loggerCtx, "default", "sv-test") + require.NoError(t, err) + require.NotEmpty(t, exposed) + require.Equal(t, "value", exposed.DangerouslyExposeAndConsumeValue()) + + require.Len(t, fakeLogger.InfoArgs, 1) + args := fakeLogger.InfoArgs[0] + require.Contains(t, args, "grafana_decrypter_identity") + require.Contains(t, args, "decrypter_identity") + for i, arg := range args { + if arg == "grafana_decrypter_identity" { + require.Equal(t, stSvcIdentity, args[i+1].(string)) + } + + if arg == "decrypter_identity" { + require.Equal(t, tokenSvcIdentity, args[i+1].(string)) + } + } + }) } func createAuthContext(ctx context.Context, namespace string, permissions []string, svc string, identityType types.IdentityType) context.Context { + ctx = logging.Context(ctx, logging.DefaultLogger) + requester := &identity.StaticRequester{ Type: identityType, Namespace: namespace, @@ -269,3 +328,18 @@ func createAuthContext(ctx context.Context, namespace string, permissions []stri return types.WithAuthInfo(ctx, requester) } + +type mockLogger struct { + logging.Logger + InfoMsgs []string + InfoArgs [][]any +} + +func (m *mockLogger) Info(msg string, args ...any) { + m.InfoMsgs = append(m.InfoMsgs, msg) + m.InfoArgs = append(m.InfoArgs, args) +} + +func (m *mockLogger) WithContext(ctx context.Context) logging.Logger { + return m +} From 2c7349f8634706b9e4e7154e00d2635cd6ea44ec Mon Sep 17 00:00:00 2001 From: Tania <10127682+undef1nd@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:05:59 +0200 Subject: [PATCH 007/126] OpenFeature: Use types from Go Feature Flag pkg (#108802) * Use go feature flag types * Add go-feature-flag module * Update go.mod * Update test * Fix --- apps/advisor/go.mod | 10 ++- apps/advisor/go.sum | 20 ++++-- apps/investigations/go.mod | 6 ++ apps/investigations/go.sum | 23 ++++-- go.mod | 11 ++- go.sum | 22 ++++-- go.work.sum | 76 ++++++++++++++++++++ pkg/promlib/go.mod | 2 +- pkg/promlib/go.sum | 4 +- pkg/registry/apis/ofrep/proxy.go | 7 +- pkg/registry/apis/ofrep/static.go | 4 +- pkg/services/featuremgmt/static_evaluator.go | 56 +++++++-------- pkg/tests/apis/features/features_test.go | 13 ++-- 13 files changed, 189 insertions(+), 65 deletions(-) diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 9379b0af3e3..09013e3c916 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -39,6 +39,7 @@ require ( github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow-go/v18 v18.3.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect @@ -52,8 +53,10 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect github.com/aws/smithy-go v1.22.4 // indirect + github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/bluele/gcache v0.0.2 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect @@ -68,6 +71,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // 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 @@ -148,7 +152,7 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/jaegertracing/jaeger-idl v0.5.0 // indirect - github.com/jessevdk/go-flags v1.5.0 // indirect + github.com/jessevdk/go-flags v1.6.1 // indirect github.com/jmespath-community/go-jmespath v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect @@ -186,6 +190,7 @@ 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/nikunjy/rules v1.5.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/oklog/run v1.1.0 // indirect @@ -220,6 +225,7 @@ require ( github.com/spf13/pflag v1.0.7 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tetratelabs/wazero v1.8.2 // indirect + github.com/thomaspoignant/go-feature-flag v1.42.0 // indirect github.com/tjhop/slog-gokit v0.1.3 // indirect github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect github.com/unknwon/com v1.0.1 // indirect @@ -227,7 +233,7 @@ require ( github.com/urfave/cli v1.22.16 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.mongodb.org/mongo-driver v1.16.1 // indirect + go.mongodb.org/mongo-driver v1.17.3 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 // indirect diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 778e8e0f26b..1547cfd7ddf 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -210,6 +210,8 @@ github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1c github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= 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/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= +github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -220,6 +222,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blevesearch/bleve/v2 v2.5.0 h1:HzYqBy/5/M9Ul9ESEmXzN/3Jl7YpmWBdHM/+zzv/3k4= @@ -354,6 +358,8 @@ github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fp github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= +github.com/diegoholiveira/jsonlogic/v3 v3.7.4 h1:92HSmB9bwM/o0ZvrCpcvTP2EsPXSkKtAniIr2W/dcIM= +github.com/diegoholiveira/jsonlogic/v3 v3.7.4/go.mod h1:OYRb6FSTVmMM+MNQ7ElmMsczyNSepw+OU4Z8emDSi4w= github.com/dlmiddlecote/sqlstats v1.0.2 h1:gSU11YN23D/iY50A2zVYwgXgy072khatTsIW6UPjUtI= github.com/dlmiddlecote/sqlstats v1.0.2/go.mod h1:0CWaIh/Th+z2aI6Q9Jpfg/o21zmGxWhbByHgQSCUQvY= github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= @@ -776,8 +782,8 @@ github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80s github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE= github.com/jaegertracing/jaeger-idl v0.5.0/go.mod h1:ON90zFo9eoyXrt9F/KN8YeF3zxcnujaisMweFY/rg5k= -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/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= 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-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4= @@ -938,6 +944,8 @@ 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/nikunjy/rules v1.5.0 h1:KJDSLOsFhwt7kcXUyZqwkgrQg5YoUwj+TVu6ItCQShw= +github.com/nikunjy/rules v1.5.0/go.mod h1:TlZtZdBChrkqi8Lr2AXocme8Z7EsbxtFdDoKeI6neBQ= 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-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= @@ -1125,6 +1133,10 @@ github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ7 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/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM= +github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8= +github.com/thomaspoignant/go-feature-flag v1.42.0 h1:C7embmOTzaLyRki+OoU2RvtVjJE9IrvgBA2C1mRN1lc= +github.com/thomaspoignant/go-feature-flag v1.42.0/go.mod h1:y0QiWH7chHWhGATb/+XqwAwErORmPSH2MUsQlCmmWlM= 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= @@ -1180,8 +1192,8 @@ go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/ go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8= -go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 9e0d9cbf6d8..7ec24ff7bb8 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -23,6 +23,7 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow-go/v18 v18.3.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/aws/aws-sdk-go-v2 v1.36.5 // indirect @@ -33,7 +34,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect github.com/aws/smithy-go v1.22.4 // indirect + github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/bluele/gcache v0.0.2 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect @@ -45,6 +48,7 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect github.com/elazarl/goproxy v1.7.2 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect @@ -132,6 +136,7 @@ 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/nikunjy/rules v1.5.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/oklog/run v1.1.0 // indirect @@ -157,6 +162,7 @@ require ( github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/spf13/pflag v1.0.7 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/thomaspoignant/go-feature-flag v1.42.0 // indirect github.com/tjhop/slog-gokit v0.1.3 // indirect github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect github.com/unknwon/com v1.0.1 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index d6f3b9daad3..5e621d587ec 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -49,6 +49,8 @@ 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.3.0 h1:Xq4A6dZj9Nu33sqZibzn012LNnewkTUlfKVUFD/RX/I= github.com/apache/arrow-go/v18 v18.3.0/go.mod h1:eEM1DnUTHhgGAjf/ChvOAQbUQ+EPohtDrArffvUjPg8= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -79,6 +81,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1 github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w= github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw= github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= +github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -86,6 +90,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= @@ -143,6 +149,8 @@ github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zA github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= +github.com/diegoholiveira/jsonlogic/v3 v3.7.4 h1:92HSmB9bwM/o0ZvrCpcvTP2EsPXSkKtAniIr2W/dcIM= +github.com/diegoholiveira/jsonlogic/v3 v3.7.4/go.mod h1:OYRb6FSTVmMM+MNQ7ElmMsczyNSepw+OU4Z8emDSi4w= github.com/dlmiddlecote/sqlstats v1.0.2 h1:gSU11YN23D/iY50A2zVYwgXgy072khatTsIW6UPjUtI= github.com/dlmiddlecote/sqlstats v1.0.2/go.mod h1:0CWaIh/Th+z2aI6Q9Jpfg/o21zmGxWhbByHgQSCUQvY= github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= @@ -405,8 +413,8 @@ github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80s github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE= github.com/jaegertracing/jaeger-idl v0.5.0/go.mod h1:ON90zFo9eoyXrt9F/KN8YeF3zxcnujaisMweFY/rg5k= -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/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= 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-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4= @@ -532,6 +540,8 @@ 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/nikunjy/rules v1.5.0 h1:KJDSLOsFhwt7kcXUyZqwkgrQg5YoUwj+TVu6ItCQShw= +github.com/nikunjy/rules v1.5.0/go.mod h1:TlZtZdBChrkqi8Lr2AXocme8Z7EsbxtFdDoKeI6neBQ= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= @@ -563,6 +573,7 @@ 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 v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 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= @@ -671,6 +682,10 @@ github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ7 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/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM= +github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8= +github.com/thomaspoignant/go-feature-flag v1.42.0 h1:C7embmOTzaLyRki+OoU2RvtVjJE9IrvgBA2C1mRN1lc= +github.com/thomaspoignant/go-feature-flag v1.42.0/go.mod h1:y0QiWH7chHWhGATb/+XqwAwErORmPSH2MUsQlCmmWlM= 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= @@ -701,8 +716,8 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8= -go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/go.mod b/go.mod index dfddf213482..b59ccb1643d 100644 --- a/go.mod +++ b/go.mod @@ -175,7 +175,7 @@ require ( github.com/urfave/cli/v3 v3.3.3 // @grafana/grafana-backend-group github.com/wk8/go-ordered-map v1.0.0 // @grafana/grafana-backend-group github.com/xlab/treeprint v1.2.0 // @grafana/observability-traces-and-profiling - github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // @grafana/grafana-operator-experience-squad + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // @grafana/grafana-operator-experience-squad github.com/yudai/gojsondiff v1.0.0 // @grafana/grafana-backend-group go.opencensus.io v0.24.0 // @grafana/grafana-backend-group go.opentelemetry.io/collector/pdata v1.30.0 // @grafana/grafana-backend-group @@ -245,6 +245,7 @@ require ( // Check go.work file for details github.com/grafana/grafana/pkg/promlib v0.0.8 // @grafana/oss-big-tent github.com/grafana/grafana/pkg/semconv v0.0.0-20250627191313-2f1a6ae1712b // @grafana/grafana-app-platform-squad + github.com/thomaspoignant/go-feature-flag v1.42.0 // @grafana/grafana-backend-group ) require ( @@ -311,8 +312,10 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.22.0 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect github.com/blevesearch/geo v0.1.20 // indirect github.com/blevesearch/go-faiss v1.0.25 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect @@ -354,6 +357,7 @@ require ( github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // 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-20250327004329-6799764f2dad // indirect @@ -433,7 +437,7 @@ require ( github.com/jcmturner/goidentity/v6 v6.0.1 // indirect github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect - github.com/jessevdk/go-flags v1.5.0 // indirect + github.com/jessevdk/go-flags v1.6.1 // indirect github.com/jhump/protoreflect v1.15.1 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -479,6 +483,7 @@ require ( 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/nikunjy/rules v1.5.0 // indirect github.com/oapi-codegen/runtime v1.0.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect @@ -549,7 +554,7 @@ require ( go.etcd.io/etcd/api/v3 v3.5.21 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect go.etcd.io/etcd/client/v3 v3.5.21 // indirect - go.mongodb.org/mongo-driver v1.16.1 // indirect + go.mongodb.org/mongo-driver v1.17.3 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect diff --git a/go.sum b/go.sum index 827f20c85c3..936fd459dcc 100644 --- a/go.sum +++ b/go.sum @@ -896,6 +896,8 @@ github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1c github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= 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/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= +github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= 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/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= @@ -914,6 +916,8 @@ github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blevesearch/bleve/v2 v2.5.0 h1:HzYqBy/5/M9Ul9ESEmXzN/3Jl7YpmWBdHM/+zzv/3k4= @@ -1083,6 +1087,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= +github.com/diegoholiveira/jsonlogic/v3 v3.7.4 h1:92HSmB9bwM/o0ZvrCpcvTP2EsPXSkKtAniIr2W/dcIM= +github.com/diegoholiveira/jsonlogic/v3 v3.7.4/go.mod h1:OYRb6FSTVmMM+MNQ7ElmMsczyNSepw+OU4Z8emDSi4w= github.com/digitalocean/godo v1.136.0 h1:DTxugljFJSMBPfEGq4KeXpnKeAHicggNqogcrw/YdZw= github.com/digitalocean/godo v1.136.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -1822,8 +1828,9 @@ 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/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= @@ -2081,6 +2088,8 @@ github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nikunjy/rules v1.5.0 h1:KJDSLOsFhwt7kcXUyZqwkgrQg5YoUwj+TVu6ItCQShw= +github.com/nikunjy/rules v1.5.0/go.mod h1:TlZtZdBChrkqi8Lr2AXocme8Z7EsbxtFdDoKeI6neBQ= 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= @@ -2429,6 +2438,10 @@ github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLY 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/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM= +github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8= +github.com/thomaspoignant/go-feature-flag v1.42.0 h1:C7embmOTzaLyRki+OoU2RvtVjJE9IrvgBA2C1mRN1lc= +github.com/thomaspoignant/go-feature-flag v1.42.0/go.mod h1:y0QiWH7chHWhGATb/+XqwAwErORmPSH2MUsQlCmmWlM= 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= @@ -2487,8 +2500,9 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= @@ -2539,8 +2553,8 @@ go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qL go.mongodb.org/mongo-driver v1.0.4/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= -go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8= -go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= diff --git a/go.work.sum b/go.work.sum index 6d9b5c84368..3eb2ae972fc 100644 --- a/go.work.sum +++ b/go.work.sum @@ -563,6 +563,8 @@ 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.7.1 h1:QcG/0iCOLChjfUweIMC3YL5Xy9C3VBeNmCZHrZfJMBw= github.com/KimMachineGun/automemlimit v0.7.1/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= @@ -621,8 +623,14 @@ github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhi 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/atc0005/go-teams-notify/v2 v2.13.0 h1:nbDeHy89NjYlF/PEfLVF6lsserY9O5SnN1iOIw3AxXw= +github.com/atc0005/go-teams-notify/v2 v2.13.0/go.mod h1:WSv9moolRsBcpZbwEf6gZxj7h0uJlJskJq5zkEWKO8Y= +github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI= +github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1 h1:nMp7diZObd4XEVUR0pEvn7/E13JIgManMX79Q6quV6E= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1/go.mod h1:MVYeeOhILFFemC/XlYTClvBjYZrg/EPd3ts885KrNTI= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.33.0 h1:JPXkrQk5OS/+Q81fKH97Ll/Vmmy0p9vwHhxw+V+tVjg= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.33.0/go.mod h1:dJngkoVMrq0K7QvRkdRZYM4NUp6cdWa2GBdpm8zoY8U= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4 h1:NgRFYyFpiMD62y4VPXh4DosPFbZd4vdMVBWKk0VmWXc= @@ -641,6 +649,8 @@ github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4 h1:hgSBvRT7JEWx2+vEGI9/Ld5rZtl7 github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4/go.mod h1:v7NIzEFIHBiicOMaMTuEmbnzGnqW0d+6ulNALul6fYE= github.com/aws/aws-sdk-go-v2/service/ssm v1.58.0 h1:zQz6Q5uaC8s9734DV9UDAm2q1TEEfOvEejDBSulOapI= github.com/aws/aws-sdk-go-v2/service/ssm v1.58.0/go.mod h1:PUWUl5MDiYNQkUHN9Pyd9kgtA/YhbxnSnHP+yQqzrM8= +github.com/awslabs/aws-lambda-go-api-proxy v0.16.2 h1:CJyGEyO1CIwOnXTU40urf0mchf6t3voxpvUDikOU9LY= +github.com/awslabs/aws-lambda-go-api-proxy v0.16.2/go.mod h1:vxxjwBHe/KbgFeNlAP/Tvp4SsVRL3WQamcWRxqVh0z0= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/baidubce/bce-sdk-go v0.9.188 h1:8MA7ewe4VpX01uYl7Kic6ZvfIReUFdSKbY46ZqlQM7U= @@ -706,6 +716,8 @@ github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARu github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/containerd v1.6.18 h1:qZbsLvmyu+Vlty0/Ex5xc0z2YtKpIsb5n45mAMI+2Ns= github.com/containerd/containerd v1.6.18/go.mod h1:1RdCUu95+gc2v9t3IL+zIlpClSmew7/0YS8O5eQZrOw= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= @@ -827,6 +839,8 @@ 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/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8= +github.com/fsouza/fake-gcs-server v1.52.2 h1:j6ne83nqHrlX5EEor7WWVIKdBsztGtwJ1J2mL+k+iio= +github.com/fsouza/fake-gcs-server v1.52.2/go.mod h1:47HKyIkz6oLTes1R8vEaHLwXfzYsGfmDUk1ViHHAUsA= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= @@ -1036,6 +1050,7 @@ github.com/jaegertracing/jaeger v1.67.0 h1:t0BiJZVW9D3Z16y3uHqKzV9bKFTusooTH1Kgr github.com/jaegertracing/jaeger v1.67.0/go.mod h1:tE/FEQfybCSdUbBgel51YaCSkc58O+Njih8oTl6j8vw= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -1048,6 +1063,8 @@ github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a/go.mod h1:dNKs71rs2V github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o= github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0= +github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= +github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d h1:c93kUJDtVAXFEhsCh5jSxyOJmFHuzcihnslQiX8Urwo= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= @@ -1071,6 +1088,7 @@ github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3ro github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= 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.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= @@ -1079,8 +1097,20 @@ github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU= +github.com/knadh/koanf/parsers/json v0.1.0/go.mod h1:ll2/MlXcZ2BfXD6YJcjVFzhG9P0TdJ207aIBKQhV2hY= +github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= +github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0= +github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= +github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= +github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= +github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= @@ -1088,10 +1118,16 @@ github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/pty v1.1.5 h1:hyz3dwM5QLc1Rfoz4FuWJQG5BN7tc6K1MndAUnGpQr4= github.com/kshvakov/clickhouse v1.3.5 h1:PDTYk9VYgbjPAWry3AoDREeMgOVUFij6bh6IjlloHL0= +github.com/labstack/echo-contrib v0.17.2 h1:K1zivqmtcC70X9VdBFdLomjPDEVHlrcAObqmuFj1c6w= +github.com/labstack/echo-contrib v0.17.2/go.mod h1:NeDh3PX7j/u+jR4iuDt1zHmWZSCz9c/p9mxXcDpyS8E= github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-syslog/v4 v4.1.0 h1:Wsl194qyWXr7V6DrGWC3xmxA9Ra6XgWO+toNt2fmCaI= github.com/leodido/go-syslog/v4 v4.1.0/go.mod h1:eJ8rUfDN5OS6dOkCOBYlg2a+hbAg6pJa99QXXgMrd98= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -1102,6 +1138,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8 github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/logrusorgru/aurora/v4 v4.0.0 h1:sRjfPpun/63iADiSvGGjgA1cAYegEWMPCJdUpJYn9JA= github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= +github.com/luci/go-render v0.0.0-20160219211803-9a04cc21af0f h1:WVPqVsbUsrzAebTEgWRAZMdDOfkFx06iyhbIoyMgtkE= +github.com/luci/go-render v0.0.0-20160219211803-9a04cc21af0f/go.mod h1:aS446i8akEg0DAtNKTVYpNpLPMc0SzsZ0RtGhjl0uFM= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= @@ -1204,6 +1242,8 @@ github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= +github.com/pablor21/echo-etag/v4 v4.0.3 h1:o49j5NmxbqWIMfKHtzJan33PW12LQnORDFlM6qMaMqw= +github.com/pablor21/echo-etag/v4 v4.0.3/go.mod h1:cKXqBSw57xk+jT68+CR9HZTD4yWgbmGjvDdQcxRWdY0= github.com/parquet-go/parquet-go v0.25.1-0.20250428214007-401fed3de956 h1:EqOiLPZlZ3UfC9d51fuYWP13FxwSzUCTArR0wg1LlxM= github.com/parquet-go/parquet-go v0.25.1-0.20250428214007-401fed3de956/go.mod h1:OqBBRGBl7+llplCvDMql8dEKaDqjaFA/VAPw+OJiNiw= github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= @@ -1221,6 +1261,8 @@ github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM= github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY= +github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= +github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= @@ -1235,6 +1277,8 @@ github.com/prometheus/statsd_exporter v0.26.1 h1:ucbIAdPmwAUcA+dU+Opok8Qt81Aw8Ha github.com/prometheus/statsd_exporter v0.26.1/go.mod h1:XlDdjAmRmx3JVvPPYuFNUg+Ynyb5kR69iPPkQjxXFMk= github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= +github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= +github.com/r3labs/diff/v3 v3.0.1/go.mod h1:f1S9bourRbiM66NskseyUdo0fTmEE0qKrikYJX63dgo= 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= @@ -1253,6 +1297,12 @@ github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNl github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ= +github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk= +github.com/samber/slog-zap/v2 v2.6.2 h1:IPHgVQjBfEwqu7fBxSxvvl+/E4b7TqAu/eispdQdv9M= +github.com/samber/slog-zap/v2 v2.6.2/go.mod h1:bMOphuaRcThr+2X7vE4kFaqyr1lqGkc9Js95n9X6xaU= github.com/samuel/go-zookeeper v0.0.0-20190810000440-0ceca61e4d75 h1:cA+Ubq9qEVIQhIWvP2kNuSZ2CmnfBJFSRq+kO1pu2cc= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= @@ -1282,12 +1332,26 @@ github.com/substrait-io/substrait v0.66.1-0.20250205013839-a30b3e2d7ec6 h1:Xqtxw github.com/substrait-io/substrait v0.66.1-0.20250205013839-a30b3e2d7ec6/go.mod h1:MPFNw6sToJgpD5Z2rj0rQrdP/Oq8HG7Z2t3CAEHtkHw= github.com/substrait-io/substrait-go/v3 v3.9.1 h1:2yfHDHpK6KMcvLd0bJVzUJoeXO+K98yS+ciBruxD9po= github.com/substrait-io/substrait-go/v3 v3.9.1/go.mod h1:VG7jCqtUm28bSngHwq86FywtU74knJ25LNX63SZ53+E= +github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= +github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= github.com/tdewolff/minify/v2 v2.12.8 h1:Q2BqOTmlMjoutkuD/OPCnJUpIqrzT3nRPkw+q+KpXS0= github.com/tdewolff/minify/v2 v2.12.8/go.mod h1:YRgk7CC21LZnbuke2fmYnCTq+zhCgpb0yJACOTUNJ1E= github.com/tdewolff/parse/v2 v2.6.7 h1:WrFllrqmzAcrKHzoYgMupqgUBIfBVOb0yscFzDf8bBg= github.com/tdewolff/parse/v2 v2.6.7/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM= github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo= github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4= +github.com/testcontainers/testcontainers-go/modules/azurite v0.35.0 h1:gUZ25e1DVE/0+ZZ0nupsIo+C1j7UNloN7Pkg3w6tceI= +github.com/testcontainers/testcontainers-go/modules/azurite v0.35.0/go.mod h1:2Fc67EpyOEexLAF99zhSuzu9H22zd83pkjxEHHTtHf4= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.35.0 h1:i1Kh9fmXgHG9z3uzJv5Arz7pDKVaaNpLrqyd+0xhYMA= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.35.0/go.mod h1:SD8nVMK1m7b/K2YJqYjYNzfHmZfqHtqNOlI44nfxjdg= +github.com/testcontainers/testcontainers-go/modules/redis v0.35.0 h1:RBgVefU5j5IWapp3TNKqMTYX+M22OSjtuORjPd4+g08= +github.com/testcontainers/testcontainers-go/modules/redis v0.35.0/go.mod h1:UgghVXQ0//D3MjC8X71Bpb/lUCChidjNCRILD+btqfU= +github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM= +github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -1332,6 +1396,8 @@ github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3R github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE= @@ -1352,6 +1418,10 @@ 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/xitongsys/parquet-go v1.6.2 h1:MhCaXii4eqceKPu9BwrjLqyK10oX9WF+xGhwvwbw7xM= +github.com/xitongsys/parquet-go v1.6.2/go.mod h1:IulAQyalCm0rPiZVNnCgm/PCL64X2tdSVGMQ/UeKqWA= +github.com/xitongsys/parquet-go-source v0.0.0-20230830030807-0dd610dbff1d h1:VVWj8KWdzpebBaXpTVpOaQW32y2UCWy3JXJ5lVDa/e8= +github.com/xitongsys/parquet-go-source v0.0.0-20230830030807-0dd610dbff1d/go.mod h1:HaLl1OAA7RAuQURU3Enxn7aRAI9yezsPPaxiGrbzxW4= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= @@ -1361,6 +1431,7 @@ github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1 h1:ixAiqjj2S/dNuJqrz4AxSqgw2P5OBM github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1/go.mod h1:l5sSv153E18VvYcsmr51hok9Sjc16tEC8AXGbwrk+ho= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -1494,6 +1565,8 @@ go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0F go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.59.0 h1:I8k9HW4yl8SRYNmECKKtjhcOvq9lAP9riqYPixBU3qw= +go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.59.0/go.mod h1:/vTiuiSKBQAerQeMB3CsVJbXd+cvTbhcdOk5AV5Z5R0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= @@ -1587,6 +1660,7 @@ 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/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1601,6 +1675,7 @@ golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -1648,6 +1723,7 @@ 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/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 4839a4f96d5..b30b3016532 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -101,7 +101,7 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.mongodb.org/mongo-driver v1.16.1 // indirect + go.mongodb.org/mongo-driver v1.17.3 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 47b36a87ee2..e348ab465d9 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -310,8 +310,8 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8= -go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= diff --git a/pkg/registry/apis/ofrep/proxy.go b/pkg/registry/apis/ofrep/proxy.go index 793a099339e..efb5cc51930 100644 --- a/pkg/registry/apis/ofrep/proxy.go +++ b/pkg/registry/apis/ofrep/proxy.go @@ -14,8 +14,9 @@ import ( "strconv" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/util/proxyutil" + + goffmodel "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model" ) func (b *APIBuilder) proxyAllFlagReq(isAuthedUser bool, w http.ResponseWriter, r *http.Request) { @@ -27,13 +28,13 @@ func (b *APIBuilder) proxyAllFlagReq(isAuthedUser bool, w http.ResponseWriter, r proxy.ModifyResponse = func(resp *http.Response) error { if resp.StatusCode == http.StatusOK && !isAuthedUser { - var result featuremgmt.OFREPBulkResponse + var result goffmodel.OFREPBulkEvaluateSuccessResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return err } _ = resp.Body.Close() - var filteredFlags []featuremgmt.OFREPFlag + var filteredFlags []goffmodel.OFREPFlagBulkEvaluateSuccessResponse for _, f := range result.Flags { if isPublicFlag(f.Key) { filteredFlags = append(filteredFlags, f) diff --git a/pkg/registry/apis/ofrep/static.go b/pkg/registry/apis/ofrep/static.go index 538e8d7e3da..8d56104649d 100644 --- a/pkg/registry/apis/ofrep/static.go +++ b/pkg/registry/apis/ofrep/static.go @@ -3,7 +3,7 @@ package ofrep import ( "net/http" - "github.com/grafana/grafana/pkg/services/featuremgmt" + goffmodel "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model" ) func (b *APIBuilder) evalAllFlagsStatic(isAuthedUser bool, w http.ResponseWriter, r *http.Request) { @@ -15,7 +15,7 @@ func (b *APIBuilder) evalAllFlagsStatic(isAuthedUser bool, w http.ResponseWriter } if !isAuthedUser { - var publicOnly []featuremgmt.OFREPFlag + var publicOnly []goffmodel.OFREPFlagBulkEvaluateSuccessResponse for _, flag := range result.Flags { if isPublicFlag(flag.Key) { diff --git a/pkg/services/featuremgmt/static_evaluator.go b/pkg/services/featuremgmt/static_evaluator.go index 1d4b05d9ece..760c48c4f66 100644 --- a/pkg/services/featuremgmt/static_evaluator.go +++ b/pkg/services/featuremgmt/static_evaluator.go @@ -7,13 +7,14 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/setting" "github.com/open-feature/go-sdk/openfeature" + goffmodel "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model" ) // StaticFlagEvaluator provides methods for evaluating static feature flags // it is only used when static provider is configured type StaticFlagEvaluator interface { - EvalFlag(ctx context.Context, flagKey string) (openfeature.BooleanEvaluationDetails, error) - EvalAllFlags(ctx context.Context) (OFREPBulkResponse, error) + EvalFlag(ctx context.Context, flagKey string) (goffmodel.OFREPEvaluateSuccessResponse, error) + EvalAllFlags(ctx context.Context) (goffmodel.OFREPBulkEvaluateSuccessResponse, error) } // CreateStaticEvaluator is a dependency for ofrep APIBuilder @@ -53,22 +54,29 @@ type staticEvaluator struct { log log.Logger } -func (s *staticEvaluator) EvalFlag(ctx context.Context, flagKey string) (openfeature.BooleanEvaluationDetails, error) { +func (s *staticEvaluator) EvalFlag(ctx context.Context, flagKey string) (goffmodel.OFREPEvaluateSuccessResponse, error) { result, err := s.client.BooleanValueDetails(ctx, flagKey, false, openfeature.TransactionContext(ctx)) if err != nil { - return openfeature.BooleanEvaluationDetails{}, fmt.Errorf("failed to evaluate flag %s: %w", flagKey, err) + return goffmodel.OFREPEvaluateSuccessResponse{}, fmt.Errorf("failed to evaluate flag %s: %w", flagKey, err) + } + resp := goffmodel.OFREPEvaluateSuccessResponse{ + Key: flagKey, + Value: result.Value, + Reason: "static provider evaluation result", + Variant: result.Variant, + Metadata: result.FlagMetadata, } - return result, nil + return resp, nil } -func (s *staticEvaluator) EvalAllFlags(ctx context.Context) (OFREPBulkResponse, error) { +func (s *staticEvaluator) EvalAllFlags(ctx context.Context) (goffmodel.OFREPBulkEvaluateSuccessResponse, error) { flags, err := s.provider.ListFlags() if err != nil { - return OFREPBulkResponse{}, fmt.Errorf("static provider failed to list all flags: %w", err) + return goffmodel.OFREPBulkEvaluateSuccessResponse{}, fmt.Errorf("static provider failed to list all flags: %w", err) } - allFlags := make([]OFREPFlag, 0, len(flags)) + allFlags := make([]goffmodel.OFREPFlagBulkEvaluateSuccessResponse, 0, len(flags)) for _, flagKey := range flags { result, err := s.client.BooleanValueDetails(ctx, flagKey, false, openfeature.TransactionContext(ctx)) if err != nil { @@ -76,32 +84,18 @@ func (s *staticEvaluator) EvalAllFlags(ctx context.Context) (OFREPBulkResponse, continue } - allFlags = append(allFlags, OFREPFlag{ - Key: flagKey, - Value: result.Value, - Reason: "static provider evaluation result", - Variant: result.Variant, + allFlags = append(allFlags, goffmodel.OFREPFlagBulkEvaluateSuccessResponse{ + OFREPEvaluateSuccessResponse: goffmodel.OFREPEvaluateSuccessResponse{ + Key: flagKey, + Value: result.Value, + Reason: "static provider evaluation result", + Variant: result.Variant, + Metadata: result.FlagMetadata, + }, ErrorCode: string(result.ErrorCode), ErrorDetails: result.ErrorMessage, }) } - return OFREPBulkResponse{Flags: allFlags}, nil -} - -// OFREPBulkResponse represents the response for bulk flag evaluation -type OFREPBulkResponse struct { - Flags []OFREPFlag `json:"flags"` - Metadata map[string]any `json:"metadata,omitempty"` -} - -// OFREPFlag represents a single flag in the bulk response -type OFREPFlag struct { - Key string `json:"key"` - Value any `json:"value"` - Reason string `json:"reason"` - Variant string `json:"variant,omitempty"` - Metadata map[string]any `json:"metadata,omitempty"` - ErrorCode string `json:"errorCode,omitempty"` - ErrorDetails string `json:"errorDetails,omitempty"` + return goffmodel.OFREPBulkEvaluateSuccessResponse{Flags: allFlags}, nil } diff --git a/pkg/tests/apis/features/features_test.go b/pkg/tests/apis/features/features_test.go index 1a4b05a898d..706984e9edf 100644 --- a/pkg/tests/apis/features/features_test.go +++ b/pkg/tests/apis/features/features_test.go @@ -40,14 +40,9 @@ func TestIntegrationFeatures(t *testing.T) { require.Equal(t, 200, rsp.Response.StatusCode) require.JSONEq(t, `{ - "Value": true, - "FlagKey": "`+flag+`", - "FlagType": 0, - "Variant": "enabled", - "Reason": "STATIC", - "ErrorCode": "", - "ErrorMessage": "", - "FlagMetadata": {} - }`, string(rsp.Body)) + "value": true, + "key":"`+flag+`", + "reason":"static provider evaluation result", + "variant":"enabled"}`, string(rsp.Body)) }) } From 2bd023edc0e3f1fa8172610c2cbea6a7754f3cfc Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 29 Jul 2025 16:49:51 +0300 Subject: [PATCH 008/126] Provisioning: Enable navigating to previous step (#108834) * Provisioning: Enable navigation to previous step in Wizard * Show confirmation modal * Show success message * Extract --- .../provisioning/Job/FinishedJobStatus.tsx | 7 +- .../provisioning/Shared/ProvisioningAlert.tsx | 25 ++-- .../Wizard/ProvisioningWizard.test.tsx | 6 +- .../Wizard/ProvisioningWizard.tsx | 113 ++++++++++++++---- .../app/features/provisioning/Wizard/types.ts | 9 +- public/app/features/provisioning/types.ts | 2 +- public/locales/en-US/grafana.json | 9 ++ 7 files changed, 131 insertions(+), 40 deletions(-) diff --git a/public/app/features/provisioning/Job/FinishedJobStatus.tsx b/public/app/features/provisioning/Job/FinishedJobStatus.tsx index 9f633447433..05ed8c1a13c 100644 --- a/public/app/features/provisioning/Job/FinishedJobStatus.tsx +++ b/public/app/features/provisioning/Job/FinishedJobStatus.tsx @@ -47,7 +47,12 @@ export function FinishedJobStatus({ jobUid, repositoryName }: FinishedJobProps) }, }); } else if (state === 'success') { - setStepStatusInfo({ status: 'success' }); + setStepStatusInfo({ + status: 'success', + success: { + title: t('provisioning.job-status.status.title-success-running-job', 'Job completed successfully'), + }, + }); } else if (state === 'warning') { setStepStatusInfo({ status: 'warning', diff --git a/public/app/features/provisioning/Shared/ProvisioningAlert.tsx b/public/app/features/provisioning/Shared/ProvisioningAlert.tsx index 94fb45423cc..6060e297633 100644 --- a/public/app/features/provisioning/Shared/ProvisioningAlert.tsx +++ b/public/app/features/provisioning/Shared/ProvisioningAlert.tsx @@ -1,28 +1,31 @@ import { t } from '@grafana/i18n'; import { Alert } from '@grafana/ui'; -import { ProvisioningErrorInfo } from '../types'; +import { StatusInfo } from '../types'; import { MessageList } from './MessageList'; interface ProvisioningAlertProps { - error?: string | ProvisioningErrorInfo; - warning?: string | ProvisioningErrorInfo; + error?: string | StatusInfo; + warning?: string | StatusInfo; + success?: string | StatusInfo; } -const getTitle = (alert: string | ProvisioningErrorInfo, isWarning = false) => { +const getTitle = (alert: string | StatusInfo, type: 'error' | 'warning' | 'success' = 'error') => { if (typeof alert === 'string') { return alert; } - if (isWarning) { + if (type === 'warning') { return alert.title || t('provisioning.warning-title-default', 'Warning'); + } else if (type === 'success') { + return alert.title || t('provisioning.success-title-default', 'Success'); } else { return alert.title || t('provisioning.error-title-default', 'Error'); } }; -const getMessage = (alert: string | ProvisioningErrorInfo) => { +const getMessage = (alert: string | StatusInfo) => { if (typeof alert === 'string' || !alert.message) { return null; } @@ -34,17 +37,17 @@ const getMessage = (alert: string | ProvisioningErrorInfo) => { return alert.message; }; -export function ProvisioningAlert({ error, warning }: ProvisioningAlertProps) { - const alertData = error || warning; - const isWarning = Boolean(warning); - const severity = isWarning ? 'warning' : 'error'; +export function ProvisioningAlert({ error, warning, success }: ProvisioningAlertProps) { + const alertData = error || warning || success; + const type = error ? 'error' : warning ? 'warning' : 'success'; + const severity = type === 'success' ? 'success' : type; if (!alertData) { return null; } return ( - + {getMessage(alertData)} ); diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx index f9c293137d9..965dd3b8b8f 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx @@ -406,7 +406,7 @@ describe('ProvisioningWizard', () => { expect(mockNavigate).toHaveBeenCalledWith('/admin/provisioning'); }); - it('should handle cancel on subsequent steps with repository deletion', async () => { + it('should handle going back to previous step', async () => { const { user } = setup(); await fillConnectionForm(user, 'github', { @@ -422,10 +422,10 @@ describe('ProvisioningWizard', () => { expect(screen.getByRole('heading', { name: /2\. Choose what to synchronize/i })).toBeInTheDocument(); }); - await user.click(screen.getByRole('button', { name: /Cancel/i })); + await user.click(screen.getByRole('button', { name: /Previous/i })); await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith('/admin/provisioning'); + expect(screen.getByRole('heading', { name: /1\. Connect to external storage/i })).toBeInTheDocument(); }); }); diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index 6c9ef49e917..8776c3b7313 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents, GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getAppEvents, isFetchError } from '@grafana/runtime'; -import { Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; +import { Box, Button, ConfirmModal, Stack, Text, useStyles2 } from '@grafana/ui'; import { useDeleteRepositoryMutation, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1'; import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; @@ -64,10 +64,16 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { const [isSubmitting, setIsSubmitting] = useState(false); const [isCancelling, setIsCancelling] = useState(false); + const [showCancelConfirmation, setShowCancelConfirmation] = useState(false); const { stepStatusInfo, setStepStatusInfo, isStepSuccess, isStepRunning, hasStepError, hasStepWarning } = useStepStatus(); + const isSyncCompleted = activeStep === 'synchronize' && (isStepSuccess || hasStepWarning || hasStepError); + const isFinishWithSyncCompleted = + activeStep === 'finish' && (isStepSuccess || completedSteps.includes('synchronize')); + const shouldUseCancelBehavior = activeStep === 'connection' || isSyncCompleted || isFinishWithSyncCompleted; + const { data } = useGetFrontendSettingsQuery(); const isLegacyStorage = Boolean(data?.legacyStorage); const navigate = useNavigate(); @@ -98,7 +104,11 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { const [repoName = '', repoType] = watch(['repositoryName', 'repository.type']); const [submitData] = useCreateOrUpdateRepository(repoName); const [deleteRepository] = useDeleteRepositoryMutation(); - const { shouldSkipSync, requiresMigration } = useResourceStats(repoName, isLegacyStorage); + const { + shouldSkipSync, + requiresMigration, + isLoading: isResourceStatsLoading, + } = useResourceStats(repoName, isLegacyStorage); const { createSyncJob, isLoading: isCreatingSkipJob } = useCreateSyncJob({ repoName: repoName, requiresMigration, @@ -110,6 +120,8 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { const currentStepIndex = steps.findIndex((s) => s.id === activeStep); const currentStepConfig = steps[currentStepIndex]; + const canSkipSync = repoName && !isResourceStatsLoading && shouldSkipSync; + // A different repository is marked with instance target -- nothing will succeed useEffect(() => { if (data?.items.some((item) => item.target === 'instance' && item.name !== repoName)) { @@ -125,6 +137,7 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { }, [navigate, repoName, data?.items]); const handleRepositoryDeletion = async (name: string) => { + setIsCancelling(true); try { await deleteRepository({ name }); // Wait before redirecting to ensure deletion is processed @@ -136,13 +149,44 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { } }; - const handleCancel = async () => { - // For the first step, do not delete anything — just go back. - if (activeStep === 'connection' || !repoName) { - navigate(PROVISIONING_URL); + const handleBack = () => { + const currentStepIndex = steps.findIndex((s) => s.id === activeStep); + + if (currentStepIndex > 0) { + let previousStepIndex = currentStepIndex - 1; + + // Handle special case: if we're on finish step and sync was skipped + if (activeStep === 'finish' && canSkipSync) { + previousStepIndex = currentStepIndex - 2; // Go back to bootstrap + } + + if (previousStepIndex >= 0) { + const previousStep = steps[previousStepIndex]; + setActiveStep(previousStep.id); + // Remove current step from completed steps when going back + setCompletedSteps((prev) => prev.filter((step) => step !== activeStep)); + setStepStatusInfo({ status: 'idle' }); + } + } + }; + + const handlePrevious = async () => { + // For cancel actions, show confirmation modal + if (shouldUseCancelBehavior) { + if (!repoName) { + navigate(PROVISIONING_URL); + return; + } + setShowCancelConfirmation(true); return; } - setIsCancelling(true); + + // For other steps, go back one step + handleBack(); + }; + + const handleConfirmCancel = () => { + setShowCancelConfirmation(false); handleRepositoryDeletion(repoName); }; @@ -157,7 +201,7 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { } // If on bootstrap step and should skip sync, show finish step name - if (currentStep === 'bootstrap' && shouldSkipSync) { + if (currentStep === 'bootstrap' && canSkipSync) { const finishStepIndex = stepIndex + 2; if (finishStepIndex < steps.length) { return steps[finishStepIndex].name; @@ -167,9 +211,22 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { return steps[stepIndex + 1].name; }, - [steps, shouldSkipSync] + [steps, canSkipSync] ); + // Calculate previous/cancel button text based on current state + const getPreviousButtonText = useCallback(() => { + if (isCancelling) { + return t('provisioning.wizard-content.button-cancelling', 'Cancelling...'); + } + + if (shouldUseCancelBehavior) { + return t('provisioning.wizard-content.button-cancel', 'Cancel'); + } + + return t('provisioning.wizard-content.button-previous', 'Previous'); + }, [isCancelling, shouldUseCancelBehavior]); + const handleNext = async () => { const isLastStep = currentStepIndex === steps.length - 1; @@ -180,15 +237,13 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { let nextStepIndex = currentStepIndex + 1; // Skip synchronize step if no sync is needed - if (activeStep === 'bootstrap' && shouldSkipSync) { + if (activeStep === 'bootstrap' && canSkipSync) { nextStepIndex = currentStepIndex + 2; // Skip to finish step // Create a pull job to initialize the repository - if (repoName) { - const job = await createSyncJob(); - if (!job) { - return; // Don't proceed if job creation fails - } + const job = await createSyncJob(); + if (!job) { + return; // Don't proceed if job creation fails } } @@ -277,7 +332,10 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
- + @@ -287,6 +345,7 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { {hasStepError && 'error' in stepStatusInfo && } {hasStepWarning && 'warning' in stepStatusInfo && } + {isStepSuccess && 'success' in stepStatusInfo && }
{activeStep === 'connection' && } @@ -296,10 +355,12 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
- + ))} +
+ )} {!hasOneClickLink && !hasOneClickAction && annotate != null && ( -
+
@@ -106,11 +123,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ flex: 1, padding: theme.spacing(0), }), - dataLinks: css({ - borderTop: `1px solid ${theme.colors.border.medium}`, - padding: theme.spacing(1), - }), - addAnnotations: css({ + footerSection: css({ borderTop: `1px solid ${theme.colors.border.medium}`, 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 e19a6230cec..703b6a85ed1 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -7,6 +7,7 @@ import uPlot from 'uplot'; import { GrafanaTheme2, LinkModel } from '@grafana/data'; import { DashboardCursorSync } from '@grafana/schema'; +import { AdHocFilterModel } from '../../../internal'; import { useStyles2 } from '../../../themes/ThemeContext'; import { RangeSelection1D, RangeSelection2D, OnSelectRangeCallback } from '../../PanelChrome'; import { getPortalContainer } from '../../Portal/Portal'; @@ -28,6 +29,7 @@ export const enum TooltipHoverMode { } type GetDataLinksCallback = (seriesIdx: number, dataIdx: number) => LinkModel[]; +type GetAdHocFiltersCallback = (seriesIdx: number, dataIdx: number) => AdHocFilterModel[]; interface TooltipPlugin2Props { config: UPlotConfigBuilder; @@ -43,6 +45,7 @@ interface TooltipPlugin2Props { onSelectRange?: OnSelectRangeCallback; getDataLinks?: GetDataLinksCallback; + getAdHocFilters?: GetAdHocFiltersCallback; render: ( u: uPlot, @@ -53,7 +56,8 @@ interface TooltipPlugin2Props { // selected time range (for annotation triggering) timeRange: TimeRange2 | null, viaSync: boolean, - dataLinks: LinkModel[] + dataLinks: LinkModel[], + adHocFilters: AdHocFilterModel[] ) => React.ReactNode; maxWidth?: number; @@ -107,6 +111,7 @@ const MIN_ZOOM_DIST = 5; const maybeZoomAction = (e?: MouseEvent | null) => e != null && !e.ctrlKey && !e.metaKey; const getDataLinksFallback: GetDataLinksCallback = () => []; +const getAdHocFiltersFallback: GetAdHocFiltersCallback = () => []; const userAgentIsMobile = /Android|iPhone|iPad/i.test(navigator.userAgent); @@ -124,6 +129,7 @@ export const TooltipPlugin2 = ({ syncMode = DashboardCursorSync.Off, syncScope = 'global', // eventsScope getDataLinks = getDataLinksFallback, + getAdHocFilters = getAdHocFiltersFallback, }: TooltipPlugin2Props) => { const domRef = useRef(null); const portalRoot = useRef(null); @@ -143,6 +149,9 @@ export const TooltipPlugin2 = ({ const getLinksRef = useRef(getDataLinks); getLinksRef.current = getDataLinks; + const getAdHocFiltersRef = useRef(getAdHocFilters); + getAdHocFiltersRef.current = getAdHocFilters; + useLayoutEffect(() => { sizeRef.current?.observer.disconnect(); @@ -202,6 +211,7 @@ export const TooltipPlugin2 = ({ let closestSeriesIdx: number | null = null; let viaSync = false; let dataLinks: LinkModel[] = []; + let adHocFilters: AdHocFilterModel[] = []; // for onceClick link rendering during mousemoves we use these pre-generated first links or actions // these will be wrong if the titles have interpolation using the hovered *value* @@ -273,7 +283,8 @@ export const TooltipPlugin2 = ({ dismiss, selectedRange, viaSync, - _isPinned ? dataLinks : closestSeriesIdx != null ? persistentLinks[closestSeriesIdx] : [] + _isPinned ? dataLinks : closestSeriesIdx != null ? persistentLinks[closestSeriesIdx] : [], + _isPinned ? adHocFilters : [] ) : null, dismiss, @@ -293,6 +304,7 @@ export const TooltipPlugin2 = ({ _isHovering = false; _plot!.setCursor({ left: -10, top: -10 }); dataLinks = []; + adHocFilters = []; scheduleRender(prevIsPinned); }; @@ -351,6 +363,7 @@ export const TooltipPlugin2 = ({ // if tooltip visible, not pinned, and within proximity to a series/point else if (_isHovering && !_isPinned && closestSeriesIdx != null) { dataLinks = getLinksRef.current(closestSeriesIdx, seriesIdxs[closestSeriesIdx]!); + adHocFilters = getAdHocFiltersRef.current(closestSeriesIdx, seriesIdxs[closestSeriesIdx]!); const oneClickLink = dataLinks.find((dataLink) => dataLink.oneClick === true); if (oneClickLink != null) { diff --git a/packages/grafana-ui/src/internal/index.ts b/packages/grafana-ui/src/internal/index.ts index 54bf72d16b0..0a549922564 100644 --- a/packages/grafana-ui/src/internal/index.ts +++ b/packages/grafana-ui/src/internal/index.ts @@ -83,7 +83,7 @@ export { ConfirmContent, type ConfirmContentProps } from '../components/ConfirmM export { EmotionPerfTest } from '../components/ThemeDemos/EmotionPerfTest'; export { VizTooltipContent } from '../components/VizTooltip/VizTooltipContent'; -export { VizTooltipFooter } from '../components/VizTooltip/VizTooltipFooter'; +export { VizTooltipFooter, type AdHocFilterModel } from '../components/VizTooltip/VizTooltipFooter'; export { VizTooltipHeader } from '../components/VizTooltip/VizTooltipHeader'; export { VizTooltipWrapper } from '../components/VizTooltip/VizTooltipWrapper'; export { VizTooltipRow } from '../components/VizTooltip/VizTooltipRow'; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index b7ee60b6e55..bef148ef73c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1868,6 +1868,13 @@ var ( HideFromDocs: true, Expression: "false", }, + { + Name: "adhocFiltersInTooltips", + Description: "Enable adhoc filter buttons in visualization tooltips", + Stage: FeatureStageExperimental, + Owner: grafanaDataProSquad, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d22d7e5b728..720d9a00434 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -241,3 +241,4 @@ pluginAssetProvider,experimental,@grafana/plugins-platform-backend,false,true,fa unifiedStorageSearchDualReaderEnabled,experimental,@grafana/search-and-storage,false,false,false dashboardDsAdHocFiltering,experimental,@grafana/datapro,false,false,true alertmanagerRemoteSecondaryWithRemoteState,experimental,@grafana/alerting-squad,false,false,false +adhocFiltersInTooltips,experimental,@grafana/datapro,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 7def9808abb..6f82a45302d 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -974,4 +974,8 @@ const ( // FlagAlertmanagerRemoteSecondaryWithRemoteState // Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications. FlagAlertmanagerRemoteSecondaryWithRemoteState = "alertmanagerRemoteSecondaryWithRemoteState" + + // FlagAdhocFiltersInTooltips + // Enable adhoc filter buttons in visualization tooltips + FlagAdhocFiltersInTooltips = "adhocFiltersInTooltips" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 6d738596653..ac1e306cd71 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -17,6 +17,19 @@ "expression": "true" } }, + { + "metadata": { + "name": "adhocFiltersInTooltips", + "resourceVersion": "1753712755564", + "creationTimestamp": "2025-07-28T14:25:55Z" + }, + "spec": { + "description": "Enable adhoc filter buttons in visualization tooltips", + "stage": "experimental", + "codeowner": "@grafana/datapro", + "frontend": true + } + }, { "metadata": { "name": "aiGeneratedDashboardChanges", diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index fdb05575741..3b54eed78d4 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -1,18 +1,19 @@ import { useMemo } from 'react'; import { PanelProps, VizOrientation } from '@grafana/data'; -import { PanelDataErrorView } from '@grafana/runtime'; +import { config, PanelDataErrorView } from '@grafana/runtime'; import { + AdHocFilterItem, TooltipDisplayMode, TooltipPlugin2, UPLOT_AXIS_FONT_SIZE, UPlotChart, VizLayout, measureText, - // usePanelContext, + usePanelContext, useTheme2, } from '@grafana/ui'; -import { TooltipHoverMode } from '@grafana/ui/internal'; +import { AdHocFilterModel, FILTER_FOR_OPERATOR, TooltipHoverMode } from '@grafana/ui/internal'; import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip'; @@ -30,6 +31,7 @@ export const BarChartPanel = (props: PanelProps) => { // const { dataLinkPostProcessor } = usePanelContext(); const theme = useTheme2(); + const { onAddAdHocFilter } = usePanelContext(); const { barWidth, @@ -160,7 +162,40 @@ export const BarChartPanel = (props: PanelProps) => { getDataLinks={(seriesIdx, dataIdx) => vizSeries[0].fields[seriesIdx].getLinks?.({ valueRowIndex: dataIdx }) ?? [] } - render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync, dataLinks) => { + getAdHocFilters={(_seriesIdx, dataIdx) => { + const xField = vizSeries[0].fields[0]; + + // Check if the field supports filtering + // We only show filters on filterable fields (xField.config.filterable). + // Fields will have been marked as filterable by the data source if that data source supports adhoc filtering + // (eg. Prom or Loki) and the field types support adhoc filtering (eg. string or number - depending on the data source). + // Fields may later be marked as not filterable. For example, fields created from Grafana Transforms that + // are derived from a data source, but are not present in the data source. + // We choose `xField` here because it contains the label-value pair, rather than `field` which is the numeric Value. + if ( + config.featureToggles.adhocFiltersInTooltips && + xField.config.filterable && + onAddAdHocFilter != null + ) { + const adHocFilterItem: AdHocFilterItem = { + key: xField.name, + operator: FILTER_FOR_OPERATOR, + value: String(xField.values[dataIdx]), + }; + + const adHocFilters: AdHocFilterModel[] = [ + { + ...adHocFilterItem, + onClick: () => onAddAdHocFilter(adHocFilterItem), + }, + ]; + + return adHocFilters; + } + + return []; + }} + render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync, dataLinks, adHocFilters) => { return ( ) => { maxHeight={options.tooltip.maxHeight} replaceVariables={replaceVariables} dataLinks={dataLinks} + adHocFilters={adHocFilters} hideZeros={options.tooltip.hideZeros} /> ); diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index 48d7cc1665a..36cf73bb58a 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -9,6 +9,7 @@ import { VizTooltipWrapper, getContentItems, VizTooltipItem, + AdHocFilterModel, } from '@grafana/ui/internal'; import { getFieldActions } from '../status-history/utils'; @@ -41,6 +42,7 @@ export interface TimeSeriesTooltipProps { replaceVariables?: InterpolateFunction; dataLinks: LinkModel[]; hideZeros?: boolean; + adHocFilters?: AdHocFilterModel[]; } export const TimeSeriesTooltip = ({ @@ -56,6 +58,7 @@ export const TimeSeriesTooltip = ({ replaceVariables = (str) => str, dataLinks, hideZeros, + adHocFilters, }: TimeSeriesTooltipProps) => { const xField = series.fields[0]; const xVal = formattedValueToString(xField.display!(xField.values[dataIdxs[0]!])); @@ -90,7 +93,9 @@ export const TimeSeriesTooltip = ({ const dataIdx = dataIdxs[seriesIdx]!; const actions = getFieldActions(series, field, replaceVariables, dataIdx); - footer = ; + footer = ( + + ); } } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a2596532803..c3763c9a59a 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -8729,6 +8729,7 @@ "footer-add-annotation": "Add annotation", "footer-click-to-action": "Click to {{actionTitle}}", "footer-click-to-navigate": "Click to open {{linkTitle}}", + "footer-filter-for-value": "Filter for '{{value}}'", "timestamp": "Timestamp" }, "week-start-picker": { From 2b9a6465999d8a08c5e5cd5dbee9b8c5c8b0d76d Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Tue, 29 Jul 2025 14:29:22 -0400 Subject: [PATCH 019/126] Git Sync: Bulk move resource (#108818) * Bulk move for provisioning resources --- .../BrowseActions/BrowseActions.tsx | 39 +++ .../BulkActions/BulkActionPostSubmitStep.tsx | 54 ++++ .../BulkActions/BulkActionProgress.test.tsx | 11 +- .../BulkActions/BulkActionProgress.tsx | 24 +- .../BulkDeleteProvisionedResource.tsx | 116 ++----- .../BulkMoveProvisionedResource.tsx | 306 ++++++++++++++++++ .../BulkActions/useBulkActionRequest.ts | 39 +++ .../components/BulkActions/utils.ts | 50 +++ .../settings/MoveProvisionedDashboardForm.tsx | 4 +- public/locales/en-US/grafana.json | 26 +- 10 files changed, 564 insertions(+), 105 deletions(-) create mode 100644 public/app/features/browse-dashboards/components/BulkActions/BulkActionPostSubmitStep.tsx create mode 100644 public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx create mode 100644 public/app/features/browse-dashboards/components/BulkActions/useBulkActionRequest.ts create mode 100644 public/app/features/browse-dashboards/components/BulkActions/utils.ts diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx index 85b50db08c0..6bc4095ff27 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx @@ -15,6 +15,7 @@ import { useActionSelectionState } from '../../state/hooks'; import { setAllSelection } from '../../state/slice'; import { DashboardTreeSelection } from '../../types'; import { BulkDeleteProvisionedResource } from '../BulkActions/BulkDeleteProvisionedResource'; +import { BulkMoveProvisionedResource } from '../BulkActions/BulkMoveProvisionedResource'; import { DeleteModal } from './DeleteModal'; import { MoveModal } from './MoveModal'; @@ -27,6 +28,7 @@ export interface Props { export function BrowseActions({ folderDTO }: Props) { const [showBulkDeleteProvisionedResource, setShowBulkDeleteProvisionedResource] = useState(false); + const [showBulkMoveProvisionedResource, setShowBulkMoveProvisionedResource] = useState(false); const dispatch = useDispatch(); const selectedItems = useActionSelectionState(); @@ -70,6 +72,24 @@ export function BrowseActions({ folderDTO }: Props) { }; const showMoveModal = () => { + if (provisioningEnabled && hasProvisioned && hasNonProvisioned) { + // Mixed selection + appEvents.publish( + new ShowModalReactEvent({ + component: SelectedMixResourcesMsgModal, + props: {}, + }) + ); + return; + } + + if (provisioningEnabled && hasProvisioned) { + // Only provisioned items + setShowBulkMoveProvisionedResource(true); + return; + } + + // only non-provisioned items appEvents.publish( new ShowModalReactEvent({ component: MoveModal, @@ -128,6 +148,7 @@ export function BrowseActions({ folderDTO }: Props) { Delete + {/* bulk delete */} {showBulkDeleteProvisionedResource && ( )} + + {/* bulk move */} + {showBulkMoveProvisionedResource && ( + setShowBulkMoveProvisionedResource(false)} + size="md" + > + { + setShowBulkMoveProvisionedResource(false); + onActionComplete(); + }} + /> + + )} ); } diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkActionPostSubmitStep.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkActionPostSubmitStep.tsx new file mode 100644 index 00000000000..c3a03aac1c9 --- /dev/null +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkActionPostSubmitStep.tsx @@ -0,0 +1,54 @@ +import { t, Trans } from '@grafana/i18n'; +import { Alert, Button, Stack } from '@grafana/ui'; + +import { BulkActionFailureBanner, MoveResultFailed } from './BulkActionFailureBanner'; +import { BulkActionProgress, ProgressState } from './BulkActionProgress'; +import { MoveResultSuccessState } from './utils'; + +interface Props { + action: 'move' | 'delete'; + progress: ProgressState | null; + successState: MoveResultSuccessState; + failureResults: MoveResultFailed[] | undefined; + handleSuccess: () => void; + setFailureResults: (results: MoveResultFailed[] | undefined) => void; +} + +export function BulkActionPostSubmitStep({ + action, + progress, + successState, + failureResults, + handleSuccess, + setFailureResults, +}: Props) { + if (progress) { + return ; + } + + if (successState.allSuccess) { + return ( + <> + + {action === 'move' + ? t('browse-dashboards.bulk-action-resources-form.all-moved', 'All resources have been moved successfully') + : t( + 'browse-dashboards.bulk-action-resources-form.all-deleted', + 'All resources have been deleted successfully' + )} + + + + + + ); + } + + if (failureResults) { + return setFailureResults(undefined)} />; + } + + return null; +} diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx index e1dcbc41a94..47a6fabfb6a 100644 --- a/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import { BulkActionProgress, ProgressState } from './BulkActionProgress'; -const setup = (progressOverrides: Partial = {}) => { +const setup = (progressOverrides: Partial = {}, action: 'delete' | 'move' = 'delete') => { const defaultProgress: ProgressState = { current: 5, total: 10, @@ -15,7 +15,7 @@ const setup = (progressOverrides: Partial = {}) => { }; return { - ...render(), + ...render(), props, }; }; @@ -59,4 +59,11 @@ describe('BulkActionProgress', () => { expect(screen.getByText(/Deleting:/)).toBeInTheDocument(); expect(screen.getByText(/Complex Dashboard Name/)).toBeInTheDocument(); }); + + it('should render moving action text when action is move', () => { + setup({ current: 2, total: 4, item: 'Moving Dashboard' }, 'move'); + + expect(screen.getByText(/Moving:/)).toBeInTheDocument(); + expect(screen.getByText(/Moving Dashboard/)).toBeInTheDocument(); + }); }); diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.tsx index fd8ac5196a8..f022554d52a 100644 --- a/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.tsx +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.tsx @@ -2,30 +2,38 @@ import { Trans } from '@grafana/i18n'; import { Box, Text, Stack, Spinner } from '@grafana/ui'; import ProgressBar from 'app/features/provisioning/Shared/ProgressBar'; -export interface ProgressState { +export type ProgressState = { current: number; total: number; item: string; +}; + +interface Props { + progress: ProgressState; + action: 'move' | 'delete'; } -export function BulkActionProgress({ progress }: { progress: ProgressState }) { +export function BulkActionProgress({ progress, action }: Props) { const progressPercentage = Math.round((progress.current / progress.total) * 100); return ( - + + Progress: {{ current: progress.current }} of {{ total: progress.total }} + - {progress.item} + {action === 'move' ? ( + Moving + ) : ( + Deleting + )} + : {progress.item} ); diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx index d5fd35ff1bd..14ea787fc81 100644 --- a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx @@ -1,9 +1,8 @@ import { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { useNavigate } from 'react-router-dom-v5-compat'; import { Trans, t } from '@grafana/i18n'; -import { Alert, Box, Button, Stack } from '@grafana/ui'; +import { Box, Button, Stack } from '@grafana/ui'; import { DeleteRepositoryFilesWithPathApiArg, DeleteRepositoryFilesWithPathApiResponse, @@ -15,50 +14,32 @@ import { AnnoKeySourcePath } from 'app/features/apiserver/types'; import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields'; import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/dashboard-scene/saving/provisioned/defaults'; import { generateTimestamp } from 'app/features/dashboard-scene/saving/provisioned/utils/timestamp'; -import { buildResourceBranchRedirectUrl } from 'app/features/dashboard-scene/settings/utils'; import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView'; -import { WorkflowOption } from 'app/features/provisioning/types'; import { useSelector } from 'app/types/store'; import { useChildrenByParentUIDState, rootItemsSelector } from '../../state/hooks'; import { findItem } from '../../state/utils'; -import { DashboardTreeSelection } from '../../types'; import { DescendantCount } from '../BrowseActions/DescendantCount'; import { collectSelectedItems, fetchProvisionedDashboardPath } from '../utils'; -import { BulkActionFailureBanner, MoveResultFailed } from './BulkActionFailureBanner'; -import { BulkActionProgress, ProgressState } from './BulkActionProgress'; +import { MoveResultFailed } from './BulkActionFailureBanner'; +import { BulkActionPostSubmitStep } from './BulkActionPostSubmitStep'; +import { ProgressState } from './BulkActionProgress'; +import { useBulkActionRequest } from './useBulkActionRequest'; +import { + BulkActionFormData, + BulkActionProvisionResourceProps, + BulkSuccessResponse, + MoveResultSuccessState, +} from './utils'; -interface BulkDeleteFormData { - comment: string; - ref: string; - workflow?: WorkflowOption; -} - -interface FormProps extends BulkDeleteProvisionResourceProps { - initialValues: BulkDeleteFormData; +interface FormProps extends BulkActionProvisionResourceProps { + initialValues: BulkActionFormData; repository: RepositoryView; workflowOptions: Array<{ label: string; value: string }>; folderPath?: string; } -interface BulkDeleteProvisionResourceProps { - folderUid?: string; - selectedItems: Omit; - onDismiss?: () => void; -} - -type BulkSuccessResponse = Array<{ - index: number; - item: DeleteRepositoryFilesWithPathApiArg; - data: DeleteRepositoryFilesWithPathApiResponse; -}>; - -type MoveResultSuccessState = { - allSuccess: boolean; - repoUrl?: string; -}; - function FormContent({ initialValues, selectedItems, repository, workflowOptions, folderPath, onDismiss }: FormProps) { // States const [progress, setProgress] = useState(null); @@ -71,12 +52,12 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions // Hooks const [deleteRepoFile, request] = useDeleteRepositoryFilesWithPathMutation(); - const methods = useForm({ defaultValues: initialValues }); + const methods = useForm({ defaultValues: initialValues }); const childrenByParentUID = useChildrenByParentUIDState(); const rootItems = useSelector(rootItemsSelector); const { handleSubmit, watch } = methods; const workflow = watch('workflow'); - const navigate = useNavigate(); + const { handleSuccess } = useBulkActionRequest({ workflow, repository, successState, onDismiss }); const getResourcePath = async (uid: string, isFolder: boolean): Promise => { const item = findItem(rootItems?.items || [], childrenByParentUID, uid); @@ -86,27 +67,7 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions return isFolder ? `${folderPath}/${item.title}/` : fetchProvisionedDashboardPath(uid); }; - const handleSuccess = () => { - if (workflow === 'branch') { - onDismiss?.(); - if (successState.repoUrl) { - const url = buildResourceBranchRedirectUrl({ - paramName: 'repo_url', - paramValue: successState.repoUrl, - repoType: repository.type, - }); - - navigate(url); - return; - } - window.location.reload(); - } else { - onDismiss?.(); - window.location.reload(); - } - }; - - const handleSubmitForm = async (data: BulkDeleteFormData) => { + const handleSubmitForm = async (data: BulkActionFormData) => { setFailureResults(undefined); setHasSubmitted(true); @@ -120,7 +81,10 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions }); } - const successes: BulkSuccessResponse = []; + const successes: BulkSuccessResponse< + DeleteRepositoryFilesWithPathApiArg, + DeleteRepositoryFilesWithPathApiResponse + > = []; const failures: MoveResultFailed[] = []; // Iterate through each selected item and delete it @@ -177,40 +141,13 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions // handleSuccess(successes); setSuccessState({ allSuccess: true, - repoUrl: successes[0].data.urls?.repositoryURL, + repoUrl: successes[0].data.urls?.newPullRequestURL, }); } else if (failures.length > 0) { setFailureResults(failures); } }; - const getPostSubmitContent = () => { - if (progress) { - return ; - } - - if (successState.allSuccess) { - return ( - <> - - - All resources have been deleted successfully. - - - - - - - ); - } else if (failureResults) { - return setFailureResults(undefined)} />; - } - - return null; - }; - return (
@@ -223,7 +160,14 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions {hasSubmitted ? ( - getPostSubmitContent() + ) : ( <> ; + folderPath?: string; +} + +function FormContent({ initialValues, selectedItems, repository, workflowOptions, folderPath, onDismiss }: FormProps) { + // States + const [targetFolderUID, setTargetFolderUID] = useState(undefined); + const [progress, setProgress] = useState(null); + const [failureResults, setFailureResults] = useState(); + const [successState, setSuccessState] = useState({ + allSuccess: false, + repoUrl: '', + }); + const [hasSubmitted, setHasSubmitted] = useState(false); + + // Hooks + const [moveFile, moveRequest] = useCreateRepositoryFilesWithPathMutation(); + const methods = useForm({ defaultValues: initialValues }); + const childrenByParentUID = useChildrenByParentUIDState(); + const rootItems = useSelector(rootItemsSelector); + const { handleSubmit, watch } = methods; + const workflow = watch('workflow'); + const { handleSuccess } = useBulkActionRequest({ workflow, repository, successState, onDismiss }); + + // Get target folder data + const { data: targetFolder } = useGetFolderQuery(targetFolderUID ? { name: targetFolderUID } : skipToken); + + const getResourceCurrentPath = async (uid: string, isFolder: boolean): Promise => { + const item = findItem(rootItems?.items || [], childrenByParentUID, uid); + if (!item) { + return undefined; + } + return isFolder ? `${folderPath}/${item.title}/` : fetchProvisionedDashboardPath(uid); + }; + + const getDashboardBody = async (currentPath: string) => { + const repositoryClient = new ScopedResourceClient({ + group: 'provisioning.grafana.app', + version: 'v0alpha1', + resource: 'repositories', + }); + const fileResponse = await repositoryClient.subresource(repository.name, `files/${currentPath}`); + return fileResponse.resource?.file; + }; + + const setupMoveOperation = () => { + const targetFolderPathInRepo = getTargetFolderPathInRepo({ targetFolder }); + const targets = collectSelectedItems(selectedItems, childrenByParentUID, rootItems?.items || []); + + if (targets.length > 0) { + setProgress({ + current: 0, + total: targets.length, + item: targets[0].displayName || 'Unknown', + }); + } + + return { targetFolderPathInRepo, targets }; + }; + + const createFileBody = async (isFolder: boolean, displayName: string, currentPath: string) => { + if (isFolder) { + return { + title: displayName, + type: 'folder', + }; + } + + const fileBody = await getDashboardBody(currentPath); + if (!fileBody) { + throw new Error( + t('browse-dashboards.bulk-move-resources-form.error-file-content-not-found', 'File content not found') + ); + } + + return fileBody; + }; + + const handleSubmitForm = async (data: BulkActionFormData) => { + setFailureResults(undefined); + setHasSubmitted(true); + + // 1. Validate + if (!targetFolder) { + setFailureResults([ + { + status: 'failed', + title: t('browse-dashboards.bulk-move-resources-form.error-title', 'Target Folder Error'), + }, + ]); + return; + } + + // 2. Setup + const { targetFolderPathInRepo, targets } = setupMoveOperation(); + + // 3. Process items + const successes: BulkSuccessResponse< + CreateRepositoryFilesWithPathApiArg, + CreateRepositoryFilesWithPathApiResponse + > = []; + const failures: MoveResultFailed[] = []; + + // Iterate through each selected item and move it + // We want sequential processing to avoid overwhelming the API + for (let i = 0; i < targets.length; i++) { + const { uid, isFolder, displayName } = targets[i]; + setProgress({ + current: i, + total: targets.length, + item: displayName, + }); + + try { + // 1. Get source path in repository + const currentPath = await getResourceCurrentPath(uid, isFolder); + if (!currentPath) { + failures.push({ + status: 'failed', + title: `${isFolder ? 'Folder' : 'Dashboard'}: ${displayName}`, + errorMessage: t('browse-dashboards.bulk-move-resources-form.error-path-not-found', 'Path not found'), + }); + continue; + } + + if (!targetFolderPathInRepo) { + failures.push({ + status: 'failed', + title: `${isFolder ? 'Folder' : 'Dashboard'}: ${displayName}`, + errorMessage: t( + 'browse-dashboards.bulk-move-resources-form.error-target-folder-path-missing', + 'Target folder path is missing' + ), + }); + continue; + } + + const newPath = getResourceTargetPath(currentPath, targetFolderPathInRepo); + const fileBody = await createFileBody(isFolder, displayName, currentPath); + + // Build move parameters + const moveParams: CreateRepositoryFilesWithPathApiArg = { + name: repository.name, + path: newPath, // NEW target path + ref: workflow === 'write' ? undefined : data.ref, + message: data.comment || `Move resource ${displayName}`, + originalPath: currentPath, // CURRENT path (source) + body: fileBody, // File content + }; + + // Call endpoint to move resource + const response = await moveFile(moveParams).unwrap(); + successes.push({ index: i, item: moveParams, data: response }); + } catch (error: unknown) { + failures.push({ + status: 'failed', + title: `${isFolder ? 'Folder' : 'Dashboard'}: ${displayName}`, + errorMessage: extractErrorMessage(error), + }); + } + + setProgress({ + current: i + 1, + total: targets.length, + item: targets[i + 1]?.displayName, + }); + } + + setProgress(null); + + if (successes.length > 0 && failures.length === 0) { + // handleSuccess(successes); + setSuccessState({ + allSuccess: true, + repoUrl: successes[0].data.urls?.newPullRequestURL, + }); + } else if (failures.length > 0) { + setFailureResults(failures); + } + }; + + return ( + + + + + + This will move selected folders and their descendants. In total, this will affect: + + + + + {hasSubmitted ? ( + + ) : ( + <> + {/* Target folder selection */} + + + + + + + + + + + )} + + + + ); +} + +export function BulkMoveProvisionedResource({ folderUid, selectedItems, onDismiss }: BulkActionProvisionResourceProps) { + const { repository, folder } = useGetResourceRepositoryView({ folderName: folderUid }); + + const workflowOptions = getWorkflowOptions(repository); + const folderPath = folder?.metadata?.annotations?.[AnnoKeySourcePath] || ''; + const timestamp = generateTimestamp(); + + const initialValues = { + comment: '', + ref: `bulk-move/${timestamp}`, + workflow: getDefaultWorkflow(repository), + }; + + if (!repository) { + return null; + } + + return ( + + ); +} diff --git a/public/app/features/browse-dashboards/components/BulkActions/useBulkActionRequest.ts b/public/app/features/browse-dashboards/components/BulkActions/useBulkActionRequest.ts new file mode 100644 index 00000000000..0e0ddfcaf2c --- /dev/null +++ b/public/app/features/browse-dashboards/components/BulkActions/useBulkActionRequest.ts @@ -0,0 +1,39 @@ +import { useNavigate } from 'react-router-dom-v5-compat'; + +import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1'; +import { buildResourceBranchRedirectUrl } from 'app/features/dashboard-scene/settings/utils'; + +import { MoveResultSuccessState } from './utils'; + +interface Props { + workflow?: 'branch' | 'write'; + repository: RepositoryView; + successState: MoveResultSuccessState; + onDismiss?: () => void; +} +export function useBulkActionRequest({ workflow, repository, successState, onDismiss }: Props) { + const navigate = useNavigate(); + + const handleSuccess = () => { + if (workflow === 'branch') { + onDismiss?.(); + if (successState.repoUrl) { + const url = buildResourceBranchRedirectUrl({ + paramName: 'repo_url', + paramValue: successState.repoUrl, + repoType: repository.type, + }); + + navigate(url); + return; + } + window.location.reload(); + } else { + onDismiss?.(); + window.location.reload(); + } + }; + return { + handleSuccess, + }; +} diff --git a/public/app/features/browse-dashboards/components/BulkActions/utils.ts b/public/app/features/browse-dashboards/components/BulkActions/utils.ts new file mode 100644 index 00000000000..48fc4892c73 --- /dev/null +++ b/public/app/features/browse-dashboards/components/BulkActions/utils.ts @@ -0,0 +1,50 @@ +import { Folder } from 'app/api/clients/folder/v1beta1'; +import { AnnoKeySourcePath } from 'app/features/apiserver/types'; +import { WorkflowOption } from 'app/features/provisioning/types'; + +import { DashboardTreeSelection } from '../../types'; + +export type BulkActionFormData = { + comment: string; + ref: string; + workflow?: WorkflowOption; +}; + +export interface BulkActionProvisionResourceProps { + folderUid?: string; + selectedItems: Omit; + onDismiss?: () => void; +} + +export type BulkSuccessResponse = Array<{ + index: number; + item: T; + data: K; +}>; + +export type MoveResultSuccessState = { + allSuccess: boolean; + repoUrl?: string; +}; + +export function getTargetFolderPathInRepo({ targetFolder }: { targetFolder?: Folder }): string | undefined { + if (!targetFolder) { + return undefined; + } + const folderAnnotations = targetFolder.metadata.annotations || {}; + return folderAnnotations[AnnoKeySourcePath] || targetFolder.metadata.name || ''; +} + +export function getResourceTargetPath(currentPath: string, targetFolderPath: string): string { + // Handle folder paths that end with '/' + const cleanCurrentPath = currentPath.replace(/\/$/, ''); // Remove trailing slash + const filename = cleanCurrentPath.split('/').pop(); + + if (!filename) { + throw new Error(`Invalid path: ${currentPath}`); + } + + // For folders, add back the trailing slash + const isFolder = currentPath.endsWith('/'); + return isFolder ? `${targetFolderPath}/${filename}/` : `${targetFolderPath}/${filename}`; +} diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx index 5a2a07cc478..8d431f3f6a1 100644 --- a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx @@ -13,6 +13,7 @@ import { useGetRepositoryFilesWithPathQuery, } from 'app/api/clients/provisioning/v0alpha1'; import { AnnoKeySourcePath } from 'app/features/apiserver/types'; +import { getTargetFolderPathInRepo } from 'app/features/browse-dashboards/components/BulkActions/utils'; import { ResourceEditFormSharedFields } from '../components/Provisioned/ResourceEditFormSharedFields'; import { ProvisionedDashboardFormData } from '../saving/shared'; @@ -74,8 +75,7 @@ export function MoveProvisionedDashboardForm({ return; } - const folderAnnotations = targetFolder.metadata.annotations || {}; - const targetFolderPath = folderAnnotations[AnnoKeySourcePath] || targetFolderTitle; + const targetFolderPath = getTargetFolderPathInRepo({ targetFolder }); const filename = currentSourcePath.split('/').pop(); const newPath = `${targetFolderPath}/${filename}`; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c3763c9a59a..a36e09b69c4 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3482,6 +3482,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "Bulk Delete Provisioned Resources", + "bulk-move-provisioned-resources": "Bulk Move Provisioned Resources", "cancel-button": "Cancel", "cannot-move-folders": "Folders cannot be moved", "confirmation-text": "Delete", @@ -3515,22 +3516,33 @@ "this-folder-is-empty": "This folder is empty" }, "bulk-action-resources-form": { + "all-deleted": "All resources have been deleted successfully", + "all-moved": "All resources have been moved successfully", + "button-done": "Done", "failed-alert_one": "{{count}} items failed", - "failed-alert_other": "{{count}} items failed" + "failed-alert_other": "{{count}} items failed", + "progress-title": "Success" }, "bulk-delete-resources-form": { "button-cancel": "Cancel", "button-delete": "Delete", "button-deleting": "Deleting...", - "button-done": "Done", "delete-warning": "This will delete selected folders and their descendants. In total, this will affect:", - "error-path-not-found": "Path not found", - "progress-title": "Success", - "success-message": "All resources have been deleted successfully." + "error-path-not-found": "Path not found" }, "bulk-move-resources-form": { - "deleting": "Deleting:", - "progress": "Progress: {{current}} of {{total}}" + "button-cancel": "Cancel", + "button-move": "Move", + "button-moving": "Moving...", + "deleting": "Deleting", + "error-file-content-not-found": "File content not found", + "error-path-not-found": "Path not found", + "error-target-folder-path-missing": "Target folder path is missing", + "error-title": "Target Folder Error", + "move-warning": "This will move selected folders and their descendants. In total, this will affect:", + "moving": "Moving", + "progress": "Progress: {{current}} of {{total}}", + "target-folder": "Target Folder" }, "counts": { "alertRule_one": "{{count}} alert rule", From 69dc5a0b88e3317f692c7beb0a426724edba4d18 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Tue, 29 Jul 2025 21:11:06 +0200 Subject: [PATCH 020/126] `grafana-iam`: Add resolver for `permissions:type:delegate` (#108789) * `grafana-iam`: Add resolver for `permissions:type:delegate` * roles create -> write --- pkg/services/authz/rbac/mapper.go | 16 +++++++++++++++- pkg/services/authz/rbac/resolver.go | 12 ++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pkg/services/authz/rbac/mapper.go b/pkg/services/authz/rbac/mapper.go index ded19301c83..28537ad638d 100644 --- a/pkg/services/authz/rbac/mapper.go +++ b/pkg/services/authz/rbac/mapper.go @@ -105,7 +105,21 @@ func NewMapperRegistry() MapperRegistry { // Teams is a special case. We translate user permissions from id to uid based. "teams": newResourceTranslation("teams", "uid", false), "coreroles": newResourceTranslation("roles", "uid", false), - "roles": newResourceTranslation("roles", "uid", false), + "roles": translation{ + resource: "roles", + attribute: "uid", + verbMapping: map[string]string{ + utils.VerbCreate: "roles:write", + utils.VerbGet: "roles:read", + utils.VerbUpdate: "roles:write", + utils.VerbPatch: "roles:write", + utils.VerbDelete: "roles:delete", + utils.VerbDeleteCollection: "roles:delete", + utils.VerbList: "roles:read", + utils.VerbWatch: "roles:read", + }, + folderSupport: false, + }, }, "secret.grafana.app": { "securevalues": newResourceTranslation("secret.securevalues", "uid", false), diff --git a/pkg/services/authz/rbac/resolver.go b/pkg/services/authz/rbac/resolver.go index f8978302c6c..0a774844c29 100644 --- a/pkg/services/authz/rbac/resolver.go +++ b/pkg/services/authz/rbac/resolver.go @@ -81,10 +81,22 @@ func (s *Service) newTeamNameResolver(ctx context.Context, ns types.NamespaceInf }, nil } +func permissionsDelegateResolverFunc(scope string) (string, error) { + if strings.TrimPrefix(scope, "permissions:type:") == "delegate" { + // The permissions:type:delegate scope does not have any discriminating value, + // so we return a wildcard to indicate that it applies to all roles. + return "*", nil + } + return "", fmt.Errorf("unsupported scope: %s", scope) +} + func (s *Service) nameResolver(ctx context.Context, ns types.NamespaceInfo, scopePrefix string) (ScopeResolverFunc, error) { if scopePrefix == "teams:id:" { return s.newTeamNameResolver(ctx, ns) } + if scopePrefix == "permissions:type:" { + return permissionsDelegateResolverFunc, nil + } // No resolver found for the given scope prefix. return nil, nil } From 412704c9de6a05f41d6478a513076fede4774e07 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 29 Jul 2025 21:53:24 +0200 Subject: [PATCH 021/126] Alerting: Document Mimir compatibility for Prometheus conversion API endpoints (#108741) Document Mimir compatibility for Prometheus conversion API endpoints --- .../alerting-rules/alerting-migration.md | 55 +++++++++++++++---- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/alerting-migration.md b/docs/sources/alerting/alerting-rules/alerting-migration.md index f07c874902d..328ca0eefb7 100644 --- a/docs/sources/alerting/alerting-rules/alerting-migration.md +++ b/docs/sources/alerting/alerting-rules/alerting-migration.md @@ -270,20 +270,51 @@ When you set `X-Grafana-Alerting-Notification-Settings`, the header value must b ### Compatible endpoints -The API endpoints listed in this section are supported in Grafana and are used by mimirtool and cortextool, as shown earlier. +The API endpoints listed in this section are supported in Grafana and are used by `mimirtool` and `cortextool`, as shown earlier. These endpoints are compatible with [Mimir HTTP API](/docs/mimir/latest/references/http-api/). -The `POST` endpoints can be used to import data source–managed alert rules. +In these endpoints, a "namespace" corresponds to a folder title in Grafana. -| Endpoint | Method | Summary | -| -------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| POST | /convert/prometheus/config/v1/rules | Create or update multiple rule groups across multiple namespaces. Requires [`X-Grafana-Alerting-Datasource-UID`](#x-grafana-alerting-datasource-uid). | -| POST | /convert/prometheus/config/v1/rules/:namespaceTitle | Create or update a single rule group in a namespace. Requires [`X-Grafana-Alerting-Datasource-UID`](#x-grafana-alerting-datasource-uid). | +The `POST` endpoints can be used to import data source–managed alert rules. They accept requests in both YAML and JSON. If no media type is specified, YAML is assumed. + +| Endpoint | Method | Summary | Mimir equivalent | +| -------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| POST | `/convert/prometheus/config/v1/rules` | [Create or update multiple rule groups](#create-or-update-multiple-rule-groups) across multiple namespaces. Requires [`X-Grafana-Alerting-Datasource-UID`](#x-grafana-alerting-datasource-uid). | None | +| POST | `/convert/prometheus/config/v1/rules/:namespaceTitle` | Create or update a single rule group in a namespace. Requires [`X-Grafana-Alerting-Datasource-UID`](#x-grafana-alerting-datasource-uid). | [Set rule group](/docs/mimir/latest/references/http-api/#set-rule-group) | The `GET` and `DELETE` endpoints work only with provisioned and imported alert rules. -| Endpoint | Method | Summary | -| -------- | ---------------------------------------------------------- | --------------------------------------------------- | -| GET | /convert/prometheus/config/v1/rules | Get all imported rule groups across all namespaces. | -| GET | /convert/prometheus/config/v1/rules/:namespaceTitle | Get imported rule groups in a specific namespace. | -| DELETE | /convert/prometheus/config/v1/rules/:namespaceTitle | Delete all imported alert rules in a namespace. | -| DELETE | /convert/prometheus/config/v1/rules/:namespaceTitle/:group | Delete a specific imported rule group. | +| Endpoint | Method | Summary | Mimir equivalent | +| -------- | ------------------------------------------------------------ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| GET | `/convert/prometheus/config/v1/rules` | Get all imported rule groups across all namespaces. | [List rule groups](/docs/mimir/latest/references/http-api/#list-rule-groups) | +| GET | `/convert/prometheus/config/v1/rules/:namespaceTitle` | Get imported rule groups in a specific namespace. | [Get rule groups by namespace](/docs/mimir/latest/references/http-api/#get-rule-groups-by-namespace) | +| DELETE | `/convert/prometheus/config/v1/rules/:namespaceTitle` | Delete all imported alert rules in a namespace. | [Delete namespace](/docs/mimir/latest/references/http-api/#delete-namespace) | +| DELETE | `/convert/prometheus/config/v1/rules/:namespaceTitle/:group` | Delete a specific imported rule group. | [Delete rule group](/docs/mimir/latest/references/http-api/#delete-rule-group) | + +#### Create or update multiple rule groups + +``` +POST /convert/prometheus/config/v1/rules +``` + +Creates or updates multiple rule groups across multiple namespaces. This endpoint expects a request with a map of namespace titles to arrays of rule groups, and returns `202` on success. + +This endpoint has no Mimir equivalent and is Grafana-specific for bulk operations. + +##### Example request body + +```yaml +namespace1: + - name: MyGroupName1 + rules: + - alert: MyAlertName1 + expr: up == 0 + labels: + severity: warning +namespace2: + - name: MyGroupName2 + rules: + - alert: MyAlertName2 + expr: rate(http_requests_total[5m]) > 0.1 + labels: + severity: critical +``` From c827ddf790bb2f866f0c06c5002f2b5212d90809 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 29 Jul 2025 21:54:44 +0200 Subject: [PATCH 022/126] Alerting: Add meta-monitoring documentation for GRAFANA_ALERTS (#108785) --- docs/sources/alerting/set-up/meta-monitoring.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sources/alerting/set-up/meta-monitoring.md b/docs/sources/alerting/set-up/meta-monitoring.md index 2467afa1eea..250487b1816 100644 --- a/docs/sources/alerting/set-up/meta-monitoring.md +++ b/docs/sources/alerting/set-up/meta-monitoring.md @@ -128,6 +128,12 @@ This metric is a gauge that shows you the number of seconds that the scheduler i This metric is a histogram that shows you the number of seconds taken to send notifications for firing and resolved alerts. This metric lets you observe slow or over-utilized integrations, such as an SMTP server that is being given emails faster than it can send them. +#### grafana_alerting_state_history_writes_failed_total + +This metric is a counter that shows you the number of failed writes to the configured alert state history backend. It includes a `backend` label to distinguish between different backends (such as `loki` or `prometheus`). + +For example, you might want to create an alert that fires when `grafana_alerting_state_history_writes_failed_total{backend="prometheus"}` is greater than 0 to detect when Prometheus remote write is failing. + ## Logs for Grafana-managed alerts If you have configured [Loki for alert state history](/docs/grafana//alerting/set-up/configure-alert-state-history/), logs related to state changes in Grafana-managed alerts are stored in the Loki data source. From e3cb84bef8579db2cead8398b751c5d2c9d563f0 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:40:30 -0500 Subject: [PATCH 023/126] CI: specify what matrix jobs set the --verified flag (#108880) specify what matrix jobs set the --verified flag --- .github/workflows/release-build.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index fda8533dc0e..231bb0395c2 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -133,22 +133,31 @@ jobs: include: - name: linux-amd64 artifacts: targz:grafana:linux/amd64,deb:grafana:linux/amd64,rpm:grafana:linux/amd64,docker:grafana:linux/amd64,docker:grafana:linux/amd64:ubuntu,npm:grafana,storybook + verify: true - name: linux-arm64 artifacts: targz:grafana:linux/arm64,deb:grafana:linux/arm64,rpm:grafana:linux/arm64,docker:grafana:linux/arm64,docker:grafana:linux/arm64:ubuntu + verify: false - name: linux-s390x artifacts: targz:grafana:linux/s390x,deb:grafana:linux/s390x,rpm:grafana:linux/s390x,docker:grafana:linux/s390x,docker:grafana:linux/s390x:ubuntu + verify: true - name: linux-armv7 artifacts: targz:grafana:linux/arm/v7,deb:grafana:linux/arm/v7,docker:grafana:linux/arm/v7,docker:grafana:linux/arm/v7:ubuntu + verify: true - name: linux-armv6 artifacts: targz:grafana:linux/arm/v6,deb:grafana:linux/arm/v6 + verify: true - name: windows-amd64 artifacts: targz:grafana:windows/amd64,zip:grafana:windows/amd64,msi:grafana:windows/amd64 + verify: true - name: windows-arm64 artifacts: targz:grafana:windows/arm64,zip:grafana:windows/arm64 + verify: true - name: darwin-amd64 artifacts: targz:grafana:darwin/amd64 + verify: true - name: darwin-arm64 artifacts: targz:grafana:darwin/arm64 + verify: true steps: - uses: grafana/shared-workflows/actions/dockerhub-login@main - uses: actions/checkout@v4 @@ -167,7 +176,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} version: ${{ needs.setup.outputs.version }} output: artifacts-${{ matrix.name }}.txt - verify: true + verify: ${{ matrix.verify }} build-id: ${{ github.run_id }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: From 1f025fe1a38de911c555038f94450b35fecce19f Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 29 Jul 2025 16:52:57 -0500 Subject: [PATCH 024/126] K8s: Remove kubernetesClientDashboardsFolders feature flag (#108626) --- .../feature-toggles/index.md | 1 - e2e/dashboards-search-suite/mode0.ini | 1 - e2e/dashboards-search-suite/mode1.ini | 1 - .../mode2-legacy-search-api.ini | 1 - e2e/dashboards-search-suite/mode2.ini | 1 - e2e/dashboards-search-suite/mode3.ini | 1 - e2e/dashboards-search-suite/mode4.ini | 1 - e2e/dashboards-search-suite/mode5.ini | 1 - .../src/types/featureToggles.gen.ts | 5 - pkg/api/dashboard.go | 2 +- pkg/api/dashboard_test.go | 120 +- pkg/api/folder.go | 51 - pkg/api/folder_test.go | 100 +- .../cloudmigrationimpl/cloudmigration_test.go | 8 +- .../snapshot_mgmt_alerts_test.go | 18 + .../dashboardimport/service/service.go | 8 - .../dashboardimport/service/service_test.go | 6 - pkg/services/dashboards/dashboard.go | 9 - pkg/services/dashboards/database/database.go | 126 -- .../database/database_provisioning_test.go | 36 - .../dashboards/database/database_test.go | 189 +- .../dashboards/service/dashboard_service.go | 727 +++---- .../service/dashboard_service_test.go | 1739 ++++++----------- pkg/services/dashboards/store_mock.go | 122 -- .../service/service_test.go | 82 - .../dashboardsnapshots/service_test.go | 75 + .../dashboardversion/dashverimpl/dashver.go | 43 +- .../dashverimpl/dashver_test.go | 109 +- 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 | 13 - pkg/services/folder/folderimpl/folder.go | 119 +- .../folderimpl/folder_unifiedstorage_test.go | 10 +- pkg/services/folder/foldertest/foldertest.go | 60 +- pkg/services/libraryelements/database.go | 35 +- .../libraryelements_create_test.go | 12 +- .../libraryelements_delete_test.go | 41 +- .../libraryelements_get_all_test.go | 29 +- .../libraryelements_get_test.go | 41 +- .../libraryelements_patch_test.go | 42 +- .../libraryelements/libraryelements_test.go | 211 +- .../librarypanels/librarypanels_test.go | 164 +- .../api/api_convert_prometheus_test.go | 4 + .../ngalert/api/api_provisioning_test.go | 102 +- pkg/services/ngalert/store/alert_rule_test.go | 69 +- pkg/services/ngalert/store/namespace_test.go | 17 +- pkg/services/ngalert/tests/util.go | 8 +- pkg/services/quota/quotaimpl/quota_test.go | 4 +- .../sqlstore/permissions/dashboard_test.go | 56 +- pkg/services/stats/statsimpl/stats.go | 5 - pkg/storage/unified/README.md | 4 - .../api/dashboards/api_dashboards_test.go | 26 +- pkg/tests/api/folders/api_folder_test.go | 22 +- pkg/tests/api/folders/api_folders_test.go | 1 - pkg/tests/apis/dashboard/dashboards_test.go | 7 +- .../integration/api_validation_test.go | 4 - .../library_panels_api_validation_test.go | 3 - pkg/tests/apis/folder/folders_test.go | 19 +- pkg/tests/apis/openapi_test.go | 3 +- pkg/tests/apis/provisioning/helper_test.go | 1 - .../dashboard-scene/scene/DashboardScene.tsx | 9 +- .../settings/VersionsEditView.test.tsx | 10 +- .../settings/VersionsEditView.tsx | 21 +- .../VersionsSettings.test.tsx | 12 +- .../DashboardSettings/VersionsSettings.tsx | 26 +- .../VersionHistory/useDashboardRestore.tsx | 8 +- .../provisioning/GettingStarted/features.ts | 6 +- 69 files changed, 1585 insertions(+), 3235 deletions(-) create mode 100644 pkg/services/dashboardsnapshots/service_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 cc448ec4ba3..fd567340110 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -43,7 +43,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `dashgpt` | Enable AI powered features in dashboards | Yes | | `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes | | `formatString` | Enable format string transformer | Yes | -| `kubernetesClientDashboardsFolders` | Route the folder and dashboard service requests to k8s | Yes | | `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation | Yes | | `annotationPermissionUpdate` | Change the way annotation permissions work by scoping them to folders and dashboards. | Yes | | `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | Yes | diff --git a/e2e/dashboards-search-suite/mode0.ini b/e2e/dashboards-search-suite/mode0.ini index 752638f24f2..1e0c6c55b0a 100644 --- a/e2e/dashboards-search-suite/mode0.ini +++ b/e2e/dashboards-search-suite/mode0.ini @@ -10,7 +10,6 @@ kubernetesCliDashboards = true unifiedStorageSearchSprinkles = true kubernetesFoldersServiceV2 = true unifiedStorageSearchPermissionFiltering = true -kubernetesClientDashboardsFolders = true [unified_storage.folders.folder.grafana.app] dualWriterMode = 0 diff --git a/e2e/dashboards-search-suite/mode1.ini b/e2e/dashboards-search-suite/mode1.ini index 8f8f80e8e30..56e16cb4cff 100644 --- a/e2e/dashboards-search-suite/mode1.ini +++ b/e2e/dashboards-search-suite/mode1.ini @@ -10,7 +10,6 @@ kubernetesCliDashboards = true unifiedStorageSearchSprinkles = true kubernetesFoldersServiceV2 = true unifiedStorageSearchPermissionFiltering = true -kubernetesClientDashboardsFolders = true [unified_storage.folders.folder.grafana.app] dualWriterMode = 1 diff --git a/e2e/dashboards-search-suite/mode2-legacy-search-api.ini b/e2e/dashboards-search-suite/mode2-legacy-search-api.ini index 38590b40a67..4b671c8b78b 100644 --- a/e2e/dashboards-search-suite/mode2-legacy-search-api.ini +++ b/e2e/dashboards-search-suite/mode2-legacy-search-api.ini @@ -10,7 +10,6 @@ kubernetesCliDashboards = true unifiedStorageSearchSprinkles = true kubernetesFoldersServiceV2 = true unifiedStorageSearchPermissionFiltering = true -kubernetesClientDashboardsFolders = true [unified_storage.folders.folder.grafana.app] dualWriterMode = 2 diff --git a/e2e/dashboards-search-suite/mode2.ini b/e2e/dashboards-search-suite/mode2.ini index c1d5890d6f4..af96c552def 100644 --- a/e2e/dashboards-search-suite/mode2.ini +++ b/e2e/dashboards-search-suite/mode2.ini @@ -10,7 +10,6 @@ kubernetesCliDashboards = true unifiedStorageSearchSprinkles = true kubernetesFoldersServiceV2 = true unifiedStorageSearchPermissionFiltering = true -kubernetesClientDashboardsFolders = true [unified_storage.folders.folder.grafana.app] dualWriterMode = 2 diff --git a/e2e/dashboards-search-suite/mode3.ini b/e2e/dashboards-search-suite/mode3.ini index e027f7e429f..a4a56f9d80c 100644 --- a/e2e/dashboards-search-suite/mode3.ini +++ b/e2e/dashboards-search-suite/mode3.ini @@ -10,7 +10,6 @@ kubernetesCliDashboards = true unifiedStorageSearchSprinkles = true kubernetesFoldersServiceV2 = true unifiedStorageSearchPermissionFiltering = true -kubernetesClientDashboardsFolders = true [unified_storage.folders.folder.grafana.app] dualWriterMode = 3 diff --git a/e2e/dashboards-search-suite/mode4.ini b/e2e/dashboards-search-suite/mode4.ini index 4c00fd10c47..dd774cb2f14 100644 --- a/e2e/dashboards-search-suite/mode4.ini +++ b/e2e/dashboards-search-suite/mode4.ini @@ -10,7 +10,6 @@ kubernetesCliDashboards = true unifiedStorageSearchSprinkles = true kubernetesFoldersServiceV2 = true unifiedStorageSearchPermissionFiltering = true -kubernetesClientDashboardsFolders = true [unified_storage.folders.folder.grafana.app] dualWriterMode = 4 diff --git a/e2e/dashboards-search-suite/mode5.ini b/e2e/dashboards-search-suite/mode5.ini index 277d4cdf1c1..83437bda185 100644 --- a/e2e/dashboards-search-suite/mode5.ini +++ b/e2e/dashboards-search-suite/mode5.ini @@ -10,7 +10,6 @@ kubernetesCliDashboards = true unifiedStorageSearchSprinkles = true kubernetesFoldersServiceV2 = true unifiedStorageSearchPermissionFiltering = true -kubernetesClientDashboardsFolders = true [unified_storage.folders.folder.grafana.app] dualWriterMode = 5 diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 6f908a114a4..835ff2230f2 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -290,11 +290,6 @@ export interface FeatureToggles { */ kubernetesDashboards?: boolean; /** - * Route the folder and dashboard service requests to k8s - * @default true - */ - kubernetesClientDashboardsFolders?: boolean; - /** * Disable schema validation for dashboards/v1 */ dashboardDisableSchemaValidationV1?: boolean; diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index bff3ffadc7a..bde32d4ad3f 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -204,7 +204,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response } metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetDashboard).Inc() // lookup folder title & url - if dash.FolderUID != "" && hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { + if dash.FolderUID != "" { queryResult, err := hs.folderService.Get(ctx, &folder.GetFolderQuery{ OrgID: c.GetOrgID(), UID: &dash.FolderUID, diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index e3159225c30..5e8167fa532 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -19,32 +19,23 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" - "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/db/dbtest" - "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "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" - "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" - "github.com/grafana/grafana/pkg/services/dashboards/service" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/services/dashboardversion/dashvertest" "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" libraryelementsfake "github.com/grafana/grafana/pkg/services/libraryelements/fake" "github.com/grafana/grafana/pkg/services/librarypanels" @@ -59,14 +50,10 @@ import ( "github.com/grafana/grafana/pkg/services/publicdashboards/api" publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/quota/quotatest" - "github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/star/startest" - "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" - "github.com/grafana/grafana/pkg/services/tag/tagimpl" "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/legacysql/dualwrite" "github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web/webtest" ) @@ -712,25 +699,42 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { t.Run("Given provisioned dashboard", func(t *testing.T) { mockSQLStore := dbtest.NewFakeDB() - dashboardStore := dashboards.NewFakeDashboardStore(t) - dashboardStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(&dashboards.DashboardProvisioningSearchResults{ExternalID: "/dashboard1.json"}, nil).Once() - dashboardService := dashboards.NewFakeDashboardService(t) + dashboardProvisioningService := dashboards.NewFakeDashboardProvisioning(t) dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "editable": true, "style": "dark"}`)) require.NoError(t, err) qResult := &dashboards.Dashboard{ID: 1, Data: dataValue} dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) - + dashboardProvisioningService.On("GetProvisionedDashboardDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(&dashboards.DashboardProvisioning{ExternalID: "/dashboard1.json"}, nil) loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", org.RoleEditor, func(sc *scenarioContext) { fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background()) fakeProvisioningService.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "/tmp/grafana/dashboards" } - dash := getDashboardShouldReturn200WithConfig(t, sc, fakeProvisioningService, dashboardStore, dashboardService, nil) + hs := &HTTPServer{ + Cfg: setting.NewCfg(), + ProvisioningService: fakeProvisioningService, + LibraryPanelService: &mockLibraryPanelService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, + dashboardProvisioningService: dashboardProvisioningService, + SQLStore: mockSQLStore, + AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, + DashboardService: dashboardService, + Features: featuremgmt.WithFeatures(), + starService: startest.NewStarServiceFake(), + tracer: tracing.InitializeTracerForTest(), + } + hs.callGetDashboard(sc) - assert.Equal(t, "../../../dashboard1.json", dash.Meta.ProvisionedExternalId, mockSQLStore) + assert.Equal(t, http.StatusOK, sc.resp.Code) + + dash := dtos.DashboardFullWithMeta{} + err := json.NewDecoder(sc.resp.Body).Decode(&dash) + require.NoError(t, err) + + assert.Equal(t, "../../../dashboard1.json", dash.Meta.ProvisionedExternalId) }, mockSQLStore) loggedInUserScenarioWithRole(t, "When allowUiUpdates is true and calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", org.RoleEditor, func(sc *scenarioContext) { @@ -748,7 +752,7 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { ProvisioningService: fakeProvisioningService, LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, - dashboardProvisioningService: mockDashboardProvisioningService{}, + dashboardProvisioningService: dashboardProvisioningService, SQLStore: mockSQLStore, AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, DashboardService: dashboardService, @@ -771,6 +775,7 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { t.Run("v2 dashboards should not be returned in api", func(t *testing.T) { mockSQLStore := dbtest.NewFakeDB() dashboardService := dashboards.NewFakeDashboardService(t) + dashboardProvisioningService := dashboards.NewFakeDashboardProvisioning(t) dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "apiVersion": "v2"}`)) require.NoError(t, err) @@ -794,7 +799,7 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { Features: featuremgmt.WithFeatures(), starService: startest.NewStarServiceFake(), tracer: tracing.InitializeTracerForTest(), - dashboardProvisioningService: mockDashboardProvisioningService{}, + dashboardProvisioningService: dashboardProvisioningService, folderService: foldertest.NewFakeService(), log: log.New("test"), namespacer: func(orgID int64) string { return strconv.FormatInt(orgID, 10) }, @@ -912,79 +917,6 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { }, mockSQLStore) } -func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, provisioningService provisioning.ProvisioningService, dashboardStore dashboards.Store, dashboardService dashboards.DashboardService, folderStore folder.FolderStore) dtos.DashboardFullWithMeta { - t.Helper() - - if provisioningService == nil { - provisioningService = provisioning.NewProvisioningServiceMock(context.Background()) - } - - features := featuremgmt.WithFeatures() - var err error - if dashboardStore == nil { - sql, cfg := db.InitTestDBWithCfg(t) - dashboardStore, err = database.ProvideDashboardStore(sql, cfg, features, tagimpl.ProvideService(sql)) - require.NoError(t, err) - } - - libraryPanelsService := mockLibraryPanelService{} - libraryElementsService := libraryelementsfake.LibraryElementService{} - cfg := setting.NewCfg() - ac := accesscontrolmock.New() - folderPermissions := accesscontrolmock.NewMockedPermissionsService() - dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() - - db := db.InitTestDB(t) - fStore := folderimpl.ProvideStore(db) - 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, - dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - if dashboardService == nil { - dashboardService, err = service.ProvideDashboardServiceImpl( - cfg, dashboardStore, folderStore, features, folderPermissions, - ac, actest.FakeService{}, folderSvc, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, - dualwrite.ProvideTestService(), sort.ProvideService(), - serverlock.ProvideService(db, tracing.InitializeTracerForTest()), kvstore.NewFakeKVStore(), - ) - require.NoError(t, err) - dashboardService.(dashboards.PermissionsRegistrationService).RegisterDashboardPermissions(dashboardPermissions) - } - - dashboardProvisioningService, err := service.ProvideDashboardServiceImpl( - cfg, dashboardStore, folderStore, features, folderPermissions, - ac, actest.FakeService{}, folderSvc, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, - dualwrite.ProvideTestService(), sort.ProvideService(), - serverlock.ProvideService(db, tracing.InitializeTracerForTest()), kvstore.NewFakeKVStore(), - ) - require.NoError(t, err) - - hs := &HTTPServer{ - Cfg: cfg, - LibraryPanelService: &libraryPanelsService, - LibraryElementService: &libraryElementsService, - SQLStore: sc.sqlStore, - ProvisioningService: provisioningService, - AccessControl: accesscontrolmock.New(), - dashboardProvisioningService: dashboardProvisioningService, - DashboardService: dashboardService, - Features: featuremgmt.WithFeatures(), - starService: startest.NewStarServiceFake(), - tracer: tracing.InitializeTracerForTest(), - } - - hs.callGetDashboard(sc) - - require.Equal(sc.t, 200, sc.resp.Code) - - dash := dtos.DashboardFullWithMeta{} - err = json.NewDecoder(sc.resp.Body).Decode(&dash) - require.NoError(sc.t, err) - - return dash -} - func (hs *HTTPServer) callGetDashboard(sc *scenarioContext) { sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 3bc0b5994fe..82ccca6c8f1 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -1,17 +1,14 @@ package api import ( - "context" "errors" "net/http" "strconv" - claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -20,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "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/services/search" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -198,13 +194,6 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response return apierrors.ToFolderErrorResponse(err) } - // Only set default permissions if the Folder API Server is disabled. - if !hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - if err := hs.setDefaultFolderPermissions(c.Req.Context(), cmd.OrgID, cmd.SignedInUser, folder); err != nil { - hs.log.Error("Could not set the default folder permissions", "folder", folder.Title, "user", cmd.SignedInUser, "error", err) - } - } - folderDTO, err := hs.newToFolderDto(c, folder) if err != nil { return response.Err(err) @@ -214,46 +203,6 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response return response.JSON(http.StatusOK, folderDTO) } -func (hs *HTTPServer) setDefaultFolderPermissions(ctx context.Context, orgID int64, user identity.Requester, folder *folder.Folder) error { - if !hs.Cfg.RBAC.PermissionsOnCreation("folder") { - return nil - } - - var permissions []accesscontrol.SetResourcePermissionCommand - - if user.IsIdentityType(claims.TypeUser, claims.TypeServiceAccount) { - userID, err := user.GetInternalID() - if err != nil { - return err - } - - permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ - UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(), - }) - } - - isNested := folder.ParentUID != "" - if !isNested || !hs.Features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { - permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ - {BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()}, - {BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()}, - }...) - } - - _, err := hs.folderPermissionsService.SetPermissions(ctx, orgID, folder.UID, permissions...) - if err != nil { - return err - } - - if user.IsIdentityType(claims.TypeUser, claims.TypeServiceAccount) { - // Clear permission cache for the user who's created the folder, so that new permissions are fetched for their next call - // Required for cases when caller wants to immediately interact with the newly created object - hs.accesscontrolService.ClearUserPermissionCache(user) - } - - return nil -} - // swagger:route POST /folders/{folder_uid}/move folders moveFolder // // Move folder. diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 9a4d42274bc..c02cad02628 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -620,9 +620,6 @@ func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) { } featuresArr := []any{featuremgmt.FlagNestedFolders} - if tc.unifiedStorageEnabled { - featuresArr = append(featuresArr, featuremgmt.FlagKubernetesClientDashboardsFolders) - } server := SetupAPITestServer(t, func(hs *HTTPServer) { hs.Cfg = cfg @@ -676,39 +673,6 @@ func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) { } func TestSetDefaultPermissionsWhenCreatingFolder(t *testing.T) { - folderService := &foldertest.FakeService{} - folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}" - - type testCase struct { - description string - expectedCallsToSetPermissions int - expectedCode int - expectedFolder *folder.Folder - permissions []accesscontrol.Permission - featuresArr []any - input string - } - - tcs := []testCase{ - { - description: "folder creation succeeds, via legacy storage", - expectedCallsToSetPermissions: 1, - input: folderWithoutParentInput, - expectedCode: http.StatusOK, - expectedFolder: &folder.Folder{UID: "uid", Title: "Folder"}, - permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}}, - }, - { - description: "folder creation succeeds, via API Server", - expectedCallsToSetPermissions: 0, - input: folderWithoutParentInput, - expectedCode: http.StatusOK, - expectedFolder: &folder.Folder{UID: "uid", Title: "Folder"}, - permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}}, - featuresArr: []any{featuremgmt.FlagKubernetesClientDashboardsFolders}, - }, - } - // we need to save these values because they are defined at `setting` package level // and modified when we invoke setting.NewCfgFromINIFile prevCookieSameSiteDisabled := setting.CookieSameSiteDisabled @@ -725,42 +689,32 @@ func TestSetDefaultPermissionsWhenCreatingFolder(t *testing.T) { setting.CookieSameSiteDisabled = prevCookieSameSiteDisabled setting.CookieSameSiteMode = prevCookieSameSiteMode - for _, tc := range tcs { - t.Run(tc.description, func(t *testing.T) { - folderService.ExpectedFolder = tc.expectedFolder - folderPermService := acmock.NewMockedPermissionsService() - folderPermService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - - srv := SetupAPITestServer(t, func(hs *HTTPServer) { - hs.Cfg = cfg - - featuresArr := append(tc.featuresArr, featuremgmt.FlagNestedFolders) - hs.Features = featuremgmt.WithFeatures( - featuresArr..., - ) - hs.folderService = folderService - hs.folderPermissionsService = folderPermService - hs.accesscontrolService = actest.FakeService{} - }) - - input := strings.NewReader(tc.input) - req := srv.NewPostRequest("/api/folders", input) - req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, tc.permissions)) - resp, err := srv.SendJSON(req) - require.NoError(t, err) - require.Equal(t, tc.expectedCode, resp.StatusCode) - - folder := dtos.Folder{} - err = json.NewDecoder(resp.Body).Decode(&folder) - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) - - folderPermService.AssertNumberOfCalls(t, "SetPermissions", tc.expectedCallsToSetPermissions) - - if tc.expectedCode == http.StatusOK { - assert.Equal(t, "uid", folder.UID) - assert.Equal(t, "Folder", folder.Title) - } - }) + folderService := &foldertest.FakeService{ + ExpectedFolder: &folder.Folder{UID: "uid", Title: "Folder"}, } + folderPermService := acmock.NewMockedPermissionsService() + folderPermService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) + + srv := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders) + hs.folderService = folderService + hs.folderPermissionsService = folderPermService + hs.accesscontrolService = actest.FakeService{} + }) + + input := strings.NewReader("{ \"uid\": \"uid\", \"title\": \"Folder\"}") + req := srv.NewPostRequest("/api/folders", input) + req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}})) + resp, err := srv.SendJSON(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + folder := dtos.Folder{} + err = json.NewDecoder(resp.Body).Decode(&folder) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + assert.Equal(t, "uid", folder.UID) + assert.Equal(t, "Folder", folder.Title) } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 3bf16b66ef9..c8867722f5e 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -872,11 +872,9 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf rr := routing.NewRouteRegister() tracer := tracing.InitializeTracerForTest() - fakeFolder := &folder.Folder{UID: "folderUID", Title: "Folder"} - mockFolder := &foldertest.FakeService{ - ExpectedFolders: []*folder.Folder{fakeFolder}, - ExpectedFolder: fakeFolder, - } + fakeFolder := &folder.Folder{UID: "folderUID", Title: "Folder", Fullpath: "Folder"} + mockFolder := foldertest.NewFakeService() + mockFolder.AddFolder(fakeFolder) cfg := setting.NewCfg() section, err := cfg.Raw.NewSection("cloud_migration") diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go index f6c43b28196..b382035c150 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -3,6 +3,7 @@ package cloudmigrationimpl import ( "context" "encoding/json" + "errors" "fmt" "net/http" "testing" @@ -18,6 +19,7 @@ import ( "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/folder" ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -408,6 +410,9 @@ func updateNotificationPolicyTree(t *testing.T, ctx context.Context, service *Se func createAlertRule(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, isPaused bool, ruleGroup string) models.AlertRule { t.Helper() + // Ensure the folder exists before creating alert rules + createFolder(t, ctx, service, user, "folderUID", "Test Folder") + rule := models.AlertRule{ OrgID: user.GetOrgID(), Title: fmt.Sprintf("Alert Rule SLO (Paused: %v) - %v", isPaused, ruleGroup), @@ -437,6 +442,19 @@ func createAlertRule(t *testing.T, ctx context.Context, service *Service, user * return createdRule } +func createFolder(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, uid, title string) { + t.Helper() + _, err := service.folderService.Create(ctx, &folder.CreateFolderCommand{ + OrgID: user.GetOrgID(), + UID: uid, + Title: title, + SignedInUser: user, + }) + if err != nil && !errors.Is(err, dashboards.ErrFolderWithSameUIDExists) { + require.NoError(t, err) + } +} + func createAlertRuleGroup(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, title string, rules []models.AlertRule) models.AlertRuleGroup { t.Helper() diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index c16801e55fa..fac501f9361 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -146,14 +146,6 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb } metrics.MFolderIDsServiceCount.WithLabelValues(metrics.DashboardImport).Inc() - // in the k8s flow, we connect the library panels in pkg/registry/apis/dashboard/legacy/sql_dashboards.go - if !s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - err = s.libraryPanelService.ConnectLibraryPanelsForDashboard(ctx, req.User, savedDashboard) - if err != nil { - return nil, err - } - } - revision := savedDashboard.Data.Get("revision").MustInt64(0) metrics.MFolderIDsServiceCount.WithLabelValues(metrics.DashboardImport).Inc() return &dashboardimport.ImportDashboardResponse{ diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index a4a1af485a2..16ee483fc1b 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -46,16 +46,11 @@ func TestImportDashboardService(t *testing.T) { } importLibraryPanelsForDashboard := false - connectLibraryPanelsForDashboardCalled := false libraryPanelService := &libraryPanelServiceMock{ importLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { importLibraryPanelsForDashboard = true return nil }, - connectLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error { - connectLibraryPanelsForDashboardCalled = true - return nil - }, } folderService := &foldertest.FakeService{ ExpectedFolder: &folder.Folder{ @@ -98,7 +93,6 @@ func TestImportDashboardService(t *testing.T) { require.Equal(t, "prom", panel.Get("datasource").MustString()) require.True(t, importLibraryPanelsForDashboard) - require.True(t, connectLibraryPanelsForDashboardCalled) }) t.Run("When importing a non-plugin dashboard should save dashboard and sync library panels", func(t *testing.T) { diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 6d9bbb0e5f8..131b24f8d67 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" @@ -77,12 +76,8 @@ type DashboardProvisioningService interface { type Store interface { DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) error CleanupAfterDelete(ctx context.Context, cmd *DeleteDashboardCommand) error - DeleteAllDashboards(ctx context.Context, orgID int64) error - DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) - GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) - GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) // GetDashboardsByPluginID retrieves dashboards identified by plugin. GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) ([]*Dashboard, error) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) @@ -97,11 +92,7 @@ type Store interface { // ValidateDashboardBeforeSave validates a dashboard before save. ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) - Count(context.Context, *quota.ScopeParameters) (*quota.Map, error) CountInOrg(ctx context.Context, orgID int64, isFolder bool) (int64, error) - // CountDashboardsInFolder returns the number of dashboards associated with - // the given parent folder ID. - CountDashboardsInFolders(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) DeleteDashboardsInFolders(ctx context.Context, request *DeleteDashboardsInFolderRequest) error GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 1062568a706..13e874e4fad 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -2,10 +2,8 @@ package database import ( "context" - "errors" "fmt" "strconv" - "strings" "time" "go.opentelemetry.io/otel" @@ -25,7 +23,6 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" - "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/tag" "github.com/grafana/grafana/pkg/setting" @@ -352,34 +349,6 @@ func (d *dashboardStore) UnprovisionDashboard(ctx context.Context, id int64) err }) } -func (d *dashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error { - ctx, span := tracer.Start(ctx, "dashboards.database.DeleteOrphanedProvisionedDashboards") - defer span.End() - - return d.store.WithDbSession(ctx, func(sess *db.Session) error { - var result []*dashboards.DashboardProvisioning - - convertedReaderNames := make([]any, len(cmd.ReaderNames)) - for index, readerName := range cmd.ReaderNames { - convertedReaderNames[index] = readerName - } - - err := sess.NotIn("name", convertedReaderNames...).Find(&result) - if err != nil { - return err - } - - for _, deleteDashCommand := range result { - err := d.DeleteDashboard(ctx, &dashboards.DeleteDashboardCommand{ID: deleteDashCommand.DashboardID}) - if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { - return err - } - } - - return nil - }) -} - func (d *dashboardStore) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) { ctx, span := tracer.Start(ctx, "dashboards.database.Count") defer span.End() @@ -690,16 +659,6 @@ func (d *dashboardStore) CleanupAfterDelete(ctx context.Context, cmd *dashboards return err } -func (d *dashboardStore) DeleteAllDashboards(ctx context.Context, orgID int64) error { - ctx, span := tracer.Start(ctx, "dashboards.database.DeleteAllDashboards") - defer span.End() - - return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - _, err := sess.Where("org_id = ?", orgID).Delete(&dashboards.Dashboard{}) - return err - }) -} - // FIXME: Remove me and handle nested deletions in the service with the DashboardPermissionsService func (d *dashboardStore) deleteResourcePermissions(sess *db.Session, orgID int64, resourceScope string) error { // retrieve all permissions for the resource scope and org id @@ -821,58 +780,6 @@ func (d *dashboardStore) GetDashboard(ctx context.Context, query *dashboards.Get return queryResult, err } -func (d *dashboardStore) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) (*dashboards.DashboardRef, error) { - ctx, span := tracer.Start(ctx, "dashboards.database.GetDashboardUIDByID") - defer span.End() - - us := &dashboards.DashboardRef{} - err := d.store.WithDbSession(ctx, func(sess *db.Session) error { - var rawSQL = `SELECT uid, slug, folder_uid from dashboard WHERE Id=?` - exists, err := sess.SQL(rawSQL, query.ID).Get(us) - if err != nil { - return err - } else if !exists { - return dashboards.ErrDashboardNotFound - } - return nil - }) - if err != nil { - return nil, err - } - return us, nil -} - -func (d *dashboardStore) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) { - ctx, span := tracer.Start(ctx, "dashboards.database.GetDashboards") - defer span.End() - - var dashboards = make([]*dashboards.Dashboard, 0) - err := d.store.WithDbSession(ctx, func(sess *db.Session) error { - if len(query.DashboardIDs) == 0 && len(query.DashboardUIDs) == 0 { - return star.ErrCommandValidationFailed - } - - // remove soft deleted dashboards from the response - sess.Where("deleted IS NULL") - - if len(query.DashboardIDs) > 0 { - sess.In("id", query.DashboardIDs) - } else { - sess.In("uid", query.DashboardUIDs) - } - if query.OrgID > 0 { - sess.Where("org_id = ?", query.OrgID) - } - - err := sess.Find(&dashboards) - return err - }) - if err != nil { - return nil, err - } - return dashboards, nil -} - func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { ctx, span := tracer.Start(ctx, "dashboards.database.FindDashboards") defer span.End() @@ -1025,39 +932,6 @@ func (d *dashboardStore) GetDashboardTags(ctx context.Context, query *dashboards return queryResult, nil } -// CountDashboardsInFolder returns a count of all dashboards associated with the -// given parent folder ID. -func (d *dashboardStore) CountDashboardsInFolders( - ctx context.Context, req *dashboards.CountDashboardsInFolderRequest) (int64, error) { - ctx, span := tracer.Start(ctx, "dashboards.database.CountDashboardsInFolders") - defer span.End() - - if len(req.FolderUIDs) == 0 { - return 0, nil - } - var count int64 - err := d.store.WithDbSession(ctx, func(sess *db.Session) error { - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - s := strings.Builder{} - args := make([]any, 0, 3) - s.WriteString("SELECT COUNT(*) FROM dashboard WHERE ") - if len(req.FolderUIDs) == 1 && req.FolderUIDs[0] == "" { - s.WriteString("folder_uid IS NULL") - } else { - s.WriteString(fmt.Sprintf("folder_uid IN (%s)", strings.Repeat("?,", len(req.FolderUIDs)-1)+"?")) - for _, folderUID := range req.FolderUIDs { - args = append(args, folderUID) - } - } - s.WriteString(" AND org_id = ? AND is_folder = ? AND deleted IS NULL") - args = append(args, req.OrgID, d.store.GetDialect().BooleanValue(false)) - sql := s.String() - _, err := sess.SQL(sql, args...).Get(&count) - return err - }) - return count, err -} - func (d *dashboardStore) DeleteDashboardsInFolders( ctx context.Context, req *dashboards.DeleteDashboardsInFolderRequest) error { ctx, span := tracer.Start(ctx, "dashboards.database.DeleteDashboardsInFolders") diff --git a/pkg/services/dashboards/database/database_provisioning_test.go b/pkg/services/dashboards/database/database_provisioning_test.go index ec82181c4d3..b14100f5c6e 100644 --- a/pkg/services/dashboards/database/database_provisioning_test.go +++ b/pkg/services/dashboards/database/database_provisioning_test.go @@ -59,42 +59,6 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { require.NotEqual(t, 0, dash.ID) dashId := dash.ID - t.Run("Deleting orphaned provisioned dashboards", func(t *testing.T) { - saveCmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - IsFolder: false, - FolderUID: dash.UID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": "another_dashboard", - }), - } - - provisioning := &dashboards.DashboardProvisioning{ - Name: "another_reader", - ExternalID: "/var/grafana.json", - Updated: now.Unix(), - } - - anotherDash, err := dashboardStore.SaveProvisionedDashboard(context.Background(), saveCmd, provisioning) - require.Nil(t, err) - - query := &dashboards.GetDashboardsQuery{DashboardIDs: []int64{anotherDash.ID}} - queryResult, err := dashboardStore.GetDashboards(context.Background(), query) - require.Nil(t, err) - require.NotNil(t, queryResult) - - deleteCmd := &dashboards.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}} - require.Nil(t, dashboardStore.DeleteOrphanedProvisionedDashboards(context.Background(), deleteCmd)) - - query = &dashboards.GetDashboardsQuery{DashboardIDs: []int64{dash.ID, anotherDash.ID}} - queryResult, err = dashboardStore.GetDashboards(context.Background(), query) - require.Nil(t, err) - - require.Equal(t, 1, len(queryResult)) - require.Equal(t, dashId, queryResult[0].ID) - }) - t.Run("Can query for provisioned dashboards", func(t *testing.T) { rslt, err := dashboardStore.GetProvisionedDashboardData(context.Background(), "default") require.Nil(t, err) diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index f45a3affda2..a940e873e0b 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -10,27 +10,19 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" - "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/folder/folderimpl" libmodel "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/search/model" - "github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" - "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) @@ -185,15 +177,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.False(t, queryResult.IsFolder) }) - t.Run("Should be able to get a dashboard UID by ID", func(t *testing.T) { - setup() - query := dashboards.GetDashboardRefByIDQuery{ID: savedDash.ID} - queryResult, err := dashboardStore.GetDashboardUIDByID(context.Background(), &query) - require.NoError(t, err) - require.Equal(t, queryResult.UID, savedDash.UID) - require.Equal(t, queryResult.FolderUID, savedFolder.UID) - }) - t.Run("Shouldn't be able to get a dashboard with just an OrgID", func(t *testing.T) { setup() query := dashboards.GetDashboardQuery{ @@ -216,19 +199,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Error(t, err, dashboards.ErrDashboardNotFound) }) - t.Run("Should be able to get dashboards by IDs & UIDs", func(t *testing.T) { - setup() - query := dashboards.GetDashboardsQuery{DashboardIDs: []int64{savedDash.ID, savedDash2.ID}} - queryResult, err := dashboardStore.GetDashboards(context.Background(), &query) - require.NoError(t, err) - assert.Equal(t, len(queryResult), 2) - - query = dashboards.GetDashboardsQuery{DashboardUIDs: []string{savedDash.UID, savedDash2.UID}} - queryResult, err = dashboardStore.GetDashboards(context.Background(), &query) - require.NoError(t, err) - assert.Equal(t, len(queryResult), 2) - }) - t.Run("Should be able to delete dashboard and associated tags", func(t *testing.T) { setup() dash := insertTestDashboard(t, dashboardStore, "delete me", 1, 0, "", false, "delete this") @@ -330,32 +300,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Len(t, res, 0) }) - t.Run("Should be able to delete all dashboards for an org", func(t *testing.T) { - setup() - dash1 := insertTestDashboard(t, dashboardStore, "delete me", 1, 0, "", false, "delete this") - dash2 := insertTestDashboard(t, dashboardStore, "delete me2", 1, 0, "", false, "delete this2") - dash3 := insertTestDashboard(t, dashboardStore, "dont delete me", 2, 0, "", false, "dont delete me") - - err := dashboardStore.DeleteAllDashboards(context.Background(), 1) - require.NoError(t, err) - - // no dashboards should exist for org 1 - queryResult, err := dashboardStore.GetDashboards(context.Background(), &dashboards.GetDashboardsQuery{ - OrgID: 1, - DashboardUIDs: []string{dash1.UID, dash2.UID}, - }) - require.NoError(t, err) - assert.Equal(t, len(queryResult), 0) - - // but we should still have one for org 2 - queryResult, err = dashboardStore.GetDashboards(context.Background(), &dashboards.GetDashboardsQuery{ - OrgID: 2, - DashboardUIDs: []string{dash3.UID}, - }) - require.NoError(t, err) - assert.Equal(t, len(queryResult), 1) - }) - t.Run("Should be able to get all dashboards for an org", func(t *testing.T) { setup() dash1 := insertTestDashboard(t, dashboardStore, "org3test1", 3, 0, "", false, "org 1 test 1") @@ -679,22 +623,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Equal(t, len(hit2.Tags), 1) }) - t.Run("Can count dashboards by parent folder", func(t *testing.T) { - setup() - // setup() saves one dashboard in the general folder and two in the "savedFolder". - count, err := dashboardStore.CountDashboardsInFolders( - context.Background(), - &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{""}, OrgID: 1}) - require.NoError(t, err) - require.Equal(t, int64(1), count) - - count, err = dashboardStore.CountDashboardsInFolders( - context.Background(), - &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1}) - require.NoError(t, err) - require.Equal(t, int64(2), count) - }) - t.Run("Can delete dashboards in folder", func(t *testing.T) { setup() folder := insertTestDashboard(t, dashboardStore, "dash folder", 1, 0, "", true, "prod", "webapp") @@ -703,10 +631,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { err := dashboardStore.DeleteDashboardsInFolders(context.Background(), &dashboards.DeleteDashboardsInFolderRequest{OrgID: folder.OrgID, FolderUIDs: []string{folder.UID}}) require.NoError(t, err) - - count, err := dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{folder.UID}, OrgID: 1}) - require.NoError(t, err) - require.Equal(t, count, int64(0)) }) } @@ -830,6 +754,59 @@ func TestIntegrationDashboard_Filter(t *testing.T) { assert.Equal(t, dashB.ID, results[0].ID) } +// has to be added to both the folder & the dashboard table +func insertTestFolder(t *testing.T, dashboardStore dashboards.Store, sqlStore db.DB, title string, orgId int64, parentUID string, tags ...interface{}) *dashboards.Dashboard { + t.Helper() + + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgId, + FolderUID: parentUID, + IsFolder: true, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + "tags": tags, + }), + } + dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) + require.NoError(t, err) + require.NotNil(t, dash) + dash.Data.Set("id", dash.ID) + dash.Data.Set("uid", dash.UID) + + err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + type folder struct { + ID int64 `xorm:"pk autoincr 'id'"` + OrgID int64 `xorm:"org_id"` + UID string `xorm:"uid"` + ParentUID *string `xorm:"parent_uid"` + Title string `xorm:"title"` + Description string `xorm:"description"` + Created time.Time `xorm:"created"` + Updated time.Time `xorm:"updated"` + } + + f := &folder{ + OrgID: orgId, + UID: dash.UID, + Title: title, + Description: "", + Created: time.Now(), + Updated: time.Now(), + } + + if parentUID != "" { + f.ParentUID = &parentUID + } + + _, err := sess.Insert(f) + return err + }) + require.NoError(t, err) + + return dash +} + func TestIntegrationFindDashboardsByTitle(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") @@ -844,13 +821,6 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) { orgID := int64(1) insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false, []string{"tag1", "tag2"}) - ac := acimpl.ProvideAccessControl(features) - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - 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, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - user := &user.SignedInUser{ OrgID: 1, Permissions: map[int64]map[string][]string{ @@ -863,23 +833,10 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) { }, } - f0, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{ - OrgID: orgID, - Title: "f0", - SignedInUser: user, - }) - require.NoError(t, err) - + f0 := insertTestFolder(t, dashboardStore, sqlStore, "f0", orgID, "") insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, 0, f0.UID, false, []string{"tag3"}) - subfolder, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{ - OrgID: orgID, - Title: "subfolder", - ParentUID: f0.UID, - SignedInUser: user, - }) - require.NoError(t, err) - + subfolder := insertTestFolder(t, dashboardStore, sqlStore, "subfolder", orgID, f0.UID) insertTestDashboard(t, dashboardStore, "dashboard under subfolder", orgID, 0, subfolder.UID, false) type res struct { @@ -981,14 +938,6 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) { orgID := int64(1) insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false) - ac := acimpl.ProvideAccessControl(features) - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - 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, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - user := &user.SignedInUser{ OrgID: 1, Permissions: map[int64]map[string][]string{ @@ -1001,31 +950,13 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) { }, } - f0, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{ - OrgID: orgID, - Title: "f0", - SignedInUser: user, - }) - require.NoError(t, err) - // nolint:staticcheck - insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, f0.ID, f0.UID, false) + f0 := insertTestFolder(t, dashboardStore, sqlStore, "f0", orgID, "") + insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, 0, f0.UID, false) - f1, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{ - OrgID: orgID, - Title: "f1", - SignedInUser: user, - }) - require.NoError(t, err) - // nolint:staticcheck - insertTestDashboard(t, dashboardStore, "dashboard under f1", orgID, f1.ID, f1.UID, false) + f1 := insertTestFolder(t, dashboardStore, sqlStore, "f1", orgID, "") + insertTestDashboard(t, dashboardStore, "dashboard under f1", orgID, 0, f1.UID, false) - subfolder, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{ - OrgID: orgID, - Title: "subfolder", - ParentUID: f0.UID, - SignedInUser: user, - }) - require.NoError(t, err) + subfolder := insertTestFolder(t, dashboardStore, sqlStore, "subfolder", orgID, f0.UID) type res struct { title string diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index de7624545c2..043438db828 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -150,10 +150,6 @@ func (dr *DashboardServiceImpl) cleanupK8sDashboardResources(ctx context.Context ctx, span := tracer.Start(ctx, "dashboards.service.cleanupK8sDashboardResources") defer span.End() - if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return nil - } - readingFromLegacy := dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, dr.dual) if readingFromLegacy { // Legacy does its own cleanup @@ -444,45 +440,41 @@ func (dr *DashboardServiceImpl) getPermissionsService(isFolder bool) accesscontr } func (dr *DashboardServiceImpl) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - u := "a.Map{} - orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) - if err != nil { - return u, err - } + u := "a.Map{} + orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) + if err != nil { + return u, err + } - total := int64(0) - for _, org := range orgs { - ctx, _ := identity.WithServiceIdentity(ctx, org.ID) - orgDashboards, err := dr.CountDashboardsInOrg(ctx, org.ID) - if err != nil { - return nil, err - } - total += orgDashboards - - if scopeParams != nil && scopeParams.OrgID == org.ID { - tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope) - if err != nil { - return nil, err - } - u.Set(tag, orgDashboards) - } - } - - tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope) + total := int64(0) + for _, org := range orgs { + ctx, _ := identity.WithServiceIdentity(ctx, org.ID) + orgDashboards, err := dr.CountDashboardsInOrg(ctx, org.ID) if err != nil { return nil, err } - u.Set(tag, total) + total += orgDashboards - return u, nil + if scopeParams != nil && scopeParams.OrgID == org.ID { + tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope) + if err != nil { + return nil, err + } + u.Set(tag, orgDashboards) + } } - return dr.dashboardStore.Count(ctx, scopeParams) + tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope) + if err != nil { + return nil, err + } + u.Set(tag, total) + + return u, nil } func (dr *DashboardServiceImpl) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*dashboards.DashboardRef, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) && dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesLibraryPanelConnections) { + if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesLibraryPanelConnections) { res, err := dr.k8sclient.Search(ctx, orgID, &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ Fields: []*resourcepb.Requirement{ @@ -519,20 +511,16 @@ func (dr *DashboardServiceImpl) GetDashboardsByLibraryPanelUID(ctx context.Conte } func (dr *DashboardServiceImpl) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - resp, err := dr.k8sclient.GetStats(ctx, orgID) - if err != nil { - return 0, err - } - - if len(resp.Stats) != 1 { - return 0, fmt.Errorf("expected 1 stat, got %d", len(resp.Stats)) - } - - return resp.Stats[0].Count, nil + resp, err := dr.k8sclient.GetStats(ctx, orgID) + if err != nil { + return 0, err } - return dr.dashboardStore.CountInOrg(ctx, orgID, false) + if len(resp.Stats) != 1 { + return 0, fmt.Errorf("expected 1 stat, got %d", len(resp.Stats)) + } + + return resp.Stats[0].Count, nil } func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) { @@ -557,93 +545,46 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) { } func (dr *DashboardServiceImpl) GetProvisionedDashboardData(ctx context.Context, name string) ([]*dashboards.DashboardProvisioning, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) - if err != nil { - return nil, err - } - - results := []*dashboards.DashboardProvisioning{} - for _, org := range orgs { - res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ - ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck - ManagerIdentity: name, - OrgId: org.ID, - }) - if err != nil { - return nil, err - } - - for _, r := range res { - results = append(results, &r.DashboardProvisioning) - } - } - - return results, nil - } - - return dr.dashboardStore.GetProvisionedDashboardData(ctx, name) -} - -func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*dashboards.DashboardProvisioning, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - // if dashboard id is 0, it is a new dashboard - if dashboardID == 0 { - return nil, nil - } - - orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) - if err != nil { - return nil, err - } - - for _, org := range orgs { - res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ - ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck - OrgId: org.ID, - DashboardIds: []int64{dashboardID}, - }) - if err != nil { - return nil, err - } - - if len(res) == 1 { - return &res[0].DashboardProvisioning, nil - } else if len(res) > 1 { - return nil, fmt.Errorf("found more than one provisioned dashboard with ID %d", dashboardID) - } - } - - return nil, nil - } - - data, err := dr.dashboardStore.GetProvisionedDataByDashboardID(ctx, dashboardID) + orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) if err != nil { return nil, err } - if data == nil { + + results := []*dashboards.DashboardProvisioning{} + for _, org := range orgs { + res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ + ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck + ManagerIdentity: name, + OrgId: org.ID, + }) + if err != nil { + return nil, err + } + + for _, r := range res { + results = append(results, &r.DashboardProvisioning) + } + } + + return results, nil +} + +func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*dashboards.DashboardProvisioning, error) { + // if dashboard id is 0, it is a new dashboard + if dashboardID == 0 { return nil, nil } - return &dashboards.DashboardProvisioning{ - DashboardID: data.Dashboard.ID, - Name: data.Provisioner, - ExternalID: data.ExternalID, - CheckSum: data.CheckSum, - Updated: data.ProvisionUpdate, - }, nil -} - -func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*dashboards.DashboardProvisioning, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - if dashboardUID == "" { - return nil, nil - } + orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) + if err != nil { + return nil, err + } + for _, org := range orgs { res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ - ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck - OrgId: orgID, - DashboardUIDs: []string{dashboardUID}, + ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck + OrgId: org.ID, + DashboardIds: []int64{dashboardID}, }) if err != nil { return nil, err @@ -652,27 +593,34 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx co if len(res) == 1 { return &res[0].DashboardProvisioning, nil } else if len(res) > 1 { - return nil, fmt.Errorf("found more than one provisioned dashboard with UID %s", dashboardUID) + return nil, fmt.Errorf("found more than one provisioned dashboard with ID %d", dashboardID) } + } + return nil, nil +} + +func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*dashboards.DashboardProvisioning, error) { + if dashboardUID == "" { return nil, nil } - data, err := dr.dashboardStore.GetProvisionedDataByDashboardUID(ctx, orgID, dashboardUID) + res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ + ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck + OrgId: orgID, + DashboardUIDs: []string{dashboardUID}, + }) if err != nil { return nil, err } - if data == nil { - return nil, nil + + if len(res) == 1 { + return &res[0].DashboardProvisioning, nil + } else if len(res) > 1 { + return nil, fmt.Errorf("found more than one provisioned dashboard with UID %s", dashboardUID) } - return &dashboards.DashboardProvisioning{ - DashboardID: data.Dashboard.ID, - Name: data.Provisioner, - ExternalID: data.ExternalID, - CheckSum: data.CheckSum, - Updated: data.ProvisionUpdate, - }, nil + return nil, nil } func (dr *DashboardServiceImpl) ValidateBasicDashboardProperties(title string, uid string, message string) error { @@ -730,7 +678,7 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } // Validate folder - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) && (dash.FolderID != 0 || dash.FolderUID != "") { // nolint:staticcheck + if dash.FolderID != 0 || dash.FolderUID != "" { // nolint:staticcheck folder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{ OrgID: dash.OrgID, UID: &dash.FolderUID, @@ -744,22 +692,6 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d // nolint:staticcheck dash.FolderID = folder.ID dash.FolderUID = folder.UID - } else if dash.FolderUID != "" { - folder, err := dr.folderStore.GetFolderByUID(ctx, dash.OrgID, dash.FolderUID) - if err != nil { - return nil, err - } - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - dash.FolderID = folder.ID - } else if dash.FolderID != 0 { // nolint:staticcheck - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - folder, err := dr.folderStore.GetFolderByID(ctx, dash.OrgID, dash.FolderID) - if err != nil { - return nil, err - } - dash.FolderUID = folder.UID } isParentFolderChanged, err := dr.ValidateDashboardBeforeSave(ctx, dash, dto.Overwrite) @@ -944,46 +876,42 @@ func (dr *DashboardServiceImpl) waitForSearchQuery(ctx context.Context, query *d } func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - // check each org for orphaned provisioned dashboards - orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) + // check each org for orphaned provisioned dashboards + orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) + if err != nil { + return err + } + + 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{ + ManagedBy: utils.ManagerKindClassicFP, //nolint:staticcheck + ManagerIdentityNotIn: cmd.ReaderNames, + OrgId: org.ID, + }) if err != nil { return err } + dr.log.Debug("Found dashboards to be deleted", "orgId", org.ID, "count", len(foundDashs)) - 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{ - ManagedBy: utils.ManagerKindClassicFP, //nolint:staticcheck - ManagerIdentityNotIn: cmd.ReaderNames, - OrgId: org.ID, - }) + // delete them + var deletedUids []string + for _, foundDash := range foundDashs { + if err = dr.deleteDashboard(ctx, foundDash.DashboardID, foundDash.DashboardUID, org.ID, false); err != nil { + return err + } + deletedUids = append(deletedUids, foundDash.DashboardUID) + } + if len(deletedUids) > 0 { + // wait for deleted dashboards to be removed from the index + err = dr.waitForSearchQuery(ctx, &dashboards.FindPersistedDashboardsQuery{OrgId: org.ID, DashboardUIDs: deletedUids}, 5, 0) if err != nil { return err } - dr.log.Debug("Found dashboards to be deleted", "orgId", org.ID, "count", len(foundDashs)) - - // delete them - var deletedUids []string - for _, foundDash := range foundDashs { - if err = dr.deleteDashboard(ctx, foundDash.DashboardID, foundDash.DashboardUID, org.ID, false); err != nil { - return err - } - deletedUids = append(deletedUids, foundDash.DashboardUID) - } - if len(deletedUids) > 0 { - // wait for deleted dashboards to be removed from the index - err = dr.waitForSearchQuery(ctx, &dashboards.FindPersistedDashboardsQuery{OrgId: org.ID, DashboardUIDs: deletedUids}, 5, 0) - if err != nil { - return err - } - } } - return nil } - - return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd) + return nil } func (dr *DashboardServiceImpl) ValidateDashboardRefreshInterval(minRefreshInterval string, targetRefreshInterval string) error { @@ -1034,13 +962,7 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt return nil, fmt.Errorf("failed to build save dashboard command. cmd is nil") } - var dash *dashboards.Dashboard - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - dash, err = dr.saveProvisionedDashboardThroughK8s(ctx, cmd, provisioning, false) - } else { - dash, err = dr.dashboardStore.SaveProvisionedDashboard(ctx, *cmd, provisioning) - } - + dash, err := dr.saveProvisionedDashboardThroughK8s(ctx, cmd, provisioning, false) if err != nil { return nil, err } @@ -1065,10 +987,6 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C return nil, err } - // Only set default permissions if the Folder API Server is disabled. - if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - dr.setDefaultFolderPermissions(ctx, dto, f) - } return f, nil } @@ -1103,11 +1021,7 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar } func (dr *DashboardServiceImpl) saveDashboard(ctx context.Context, cmd *dashboards.SaveDashboardCommand) (*dashboards.Dashboard, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return dr.saveDashboardThroughK8s(ctx, cmd, cmd.OrgID) - } - - return dr.dashboardStore.SaveDashboard(ctx, *cmd) + return dr.saveDashboardThroughK8s(ctx, cmd, cmd.OrgID) } // DeleteDashboard removes dashboard from the DB. Errors out if the dashboard was provisioned. Should be used for @@ -1118,11 +1032,7 @@ func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId // DeleteAllDashboards will delete all dashboards within a given org. func (dr *DashboardServiceImpl) DeleteAllDashboards(ctx context.Context, orgId int64) error { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return dr.deleteAllDashboardThroughK8s(ctx, orgId) - } - - return dr.dashboardStore.DeleteAllDashboards(ctx, orgId) + return dr.deleteAllDashboardThroughK8s(ctx, orgId) } func (dr *DashboardServiceImpl) GetDashboardByPublicUid(ctx context.Context, dashboardPublicUid string) (*dashboards.Dashboard, error) { @@ -1141,28 +1051,7 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId, UID: dashboardUID} - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return dr.deleteDashboardThroughK8s(ctx, cmd, validateProvisionedDashboard) - } - - if validateProvisionedDashboard { - provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dashboardId) - if err != nil { - return fmt.Errorf("%v: %w", "failed to check if dashboard is provisioned", err) - } - - if provisionedData != nil { - return dashboards.ErrDashboardCannotDeleteProvisionedDashboard - } - } - - // deletes all related public dashboard entities - err := dr.publicDashboardService.DeleteByDashboardUIDs(ctx, orgId, []string{dashboardUID}) - if err != nil { - return err - } - - return dr.dashboardStore.DeleteDashboard(ctx, cmd) + return dr.deleteDashboardThroughK8s(ctx, cmd, validateProvisionedDashboard) } func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) ( @@ -1195,65 +1084,58 @@ func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashbo // UnprovisionDashboard removes info about dashboard being provisioned. Used after provisioning configs are changed // and provisioned dashboards are left behind but not deleted. func (dr *DashboardServiceImpl) UnprovisionDashboard(ctx context.Context, dashboardId int64) error { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) + orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) + if err != nil { + return err + } + + for _, org := range orgs { + ctx, _ = identity.WithServiceIdentity(ctx, org.ID) + dash, err := dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: org.ID, ID: dashboardId}) + if err != nil { + // if we can't find it in this org, try the next one + continue + } + + _, err = dr.saveProvisionedDashboardThroughK8s(ctx, &dashboards.SaveDashboardCommand{ + OrgID: org.ID, + PluginID: dash.PluginID, + FolderUID: dash.FolderUID, + FolderID: dash.FolderID, // nolint:staticcheck + UpdatedAt: time.Now(), + Dashboard: dash.Data, + }, nil, true) if err != nil { return err } - for _, org := range orgs { - ctx, _ = identity.WithServiceIdentity(ctx, org.ID) - dash, err := dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: org.ID, ID: dashboardId}) - if err != nil { - // if we can't find it in this org, try the next one - continue - } - - _, err = dr.saveProvisionedDashboardThroughK8s(ctx, &dashboards.SaveDashboardCommand{ - OrgID: org.ID, - PluginID: dash.PluginID, - FolderUID: dash.FolderUID, - FolderID: dash.FolderID, // nolint:staticcheck - UpdatedAt: time.Now(), - Dashboard: dash.Data, - }, nil, true) - if err != nil { - return err - } - - return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId) - } - - return dashboards.ErrDashboardNotFound + return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId) } - return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId) + return dashboards.ErrDashboardNotFound } func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) ([]*dashboards.Dashboard, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ - OrgId: query.OrgID, - ManagedBy: utils.ManagerKindPlugin, - ManagerIdentity: query.PluginID, - }) + dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ + OrgId: query.OrgID, + ManagedBy: utils.ManagerKindPlugin, + ManagerIdentity: query.PluginID, + }) + if err != nil { + return nil, err + } + + // search only returns the metadata, need to get the dashboard.Data too + results := make([]*dashboards.Dashboard, len(dashs)) + for i, d := range dashs { + dash, err := dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{OrgID: d.OrgID, UID: d.UID}) if err != nil { return nil, err } - - // search only returns the metadata, need to get the dashboard.Data too - results := make([]*dashboards.Dashboard, len(dashs)) - for i, d := range dashs { - dash, err := dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{OrgID: d.OrgID, UID: d.UID}) - if err != nil { - return nil, err - } - results[i] = dash - } - - return results, nil + results[i] = dash } - return dr.dashboardStore.GetDashboardsByPluginID(ctx, query) + + return results, nil } // (sometimes) called by the k8s storage engine after creating an object @@ -1361,92 +1243,62 @@ func (dr *DashboardServiceImpl) SetDefaultPermissions(ctx context.Context, dto * } } -func (dr *DashboardServiceImpl) setDefaultFolderPermissions(ctx context.Context, cmd *folder.CreateFolderCommand, f *folder.Folder) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) || !dr.cfg.RBAC.PermissionsOnCreation("folder") || f.ParentUID != "" { - return - } - - ctx, span := tracer.Start(ctx, "dashboards.service.setDefaultFolderPermissions") - defer span.End() - - permissions := []accesscontrol.SetResourcePermissionCommand{ - {BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()}, - {BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()}, - } - - if _, err := dr.folderPermissions.SetPermissions(ctx, cmd.OrgID, f.UID, permissions...); err != nil { - dr.log.Error("Could not set default folder permissions", "folder", f.Title, "error", err) - } -} - func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return dr.getDashboardThroughK8s(ctx, query) - } - - return dr.dashboardStore.GetDashboard(ctx, query) + return dr.getDashboardThroughK8s(ctx, query) } func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) (*dashboards.DashboardRef, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - requester, err := identity.GetRequester(ctx) - if err != nil { - return nil, err - } - result, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ - OrgId: requester.GetOrgID(), - DashboardIds: []int64{query.ID}, - }) - if err != nil { - return nil, err - } - - 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)) - } - - return &dashboards.DashboardRef{UID: result[0].UID, Slug: result[0].Slug, FolderUID: result[0].FolderUID}, nil + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + result, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ + OrgId: requester.GetOrgID(), + DashboardIds: []int64{query.ID}, + }) + if err != nil { + return nil, err } - return dr.dashboardStore.GetDashboardUIDByID(ctx, query) + 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)) + } + + return &dashboards.DashboardRef{UID: result[0].UID, Slug: result[0].Slug, FolderUID: result[0].FolderUID}, nil } // expensive query in new flow !! use sparingly - only if you truly need dashboard.Data func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - if query.OrgID == 0 { - requester, err := identity.GetRequester(ctx) - if err != nil { - return nil, err - } - query.OrgID = requester.GetOrgID() - } - - dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ - DashboardIds: query.DashboardIDs, - OrgId: query.OrgID, - DashboardUIDs: query.DashboardUIDs, - }) + if query.OrgID == 0 { + requester, err := identity.GetRequester(ctx) if err != nil { return nil, err } - - // search only returns the metadata, need to get the dashboard.Data too - results := make([]*dashboards.Dashboard, len(dashs)) - for i, d := range dashs { - dash, err := dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{OrgID: d.OrgID, UID: d.UID}) - if err != nil { - return nil, err - } - results[i] = dash - } - - return results, nil + query.OrgID = requester.GetOrgID() } - return dr.dashboardStore.GetDashboards(ctx, query) + dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ + DashboardIds: query.DashboardIDs, + OrgId: query.OrgID, + DashboardUIDs: query.DashboardUIDs, + }) + if err != nil { + return nil, err + } + + // search only returns the metadata, need to get the dashboard.Data too + results := make([]*dashboards.Dashboard, len(dashs)) + for i, d := range dashs { + dash, err := dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{OrgID: d.OrgID, UID: d.UID}) + if err != nil { + return nil, err + } + results[i] = dash + } + + return results, nil } func (dr *DashboardServiceImpl) getDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*dashboards.DashboardRef, error) { @@ -1468,21 +1320,10 @@ func (dr *DashboardServiceImpl) getDashboardsSharedWithUser(ctx context.Context, return []*dashboards.DashboardRef{}, nil } - dashboardsQuery := &dashboards.GetDashboardsQuery{ + dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ DashboardUIDs: dashboardUids, - OrgID: user.GetOrgID(), - } - - var err error - var dashs []*dashboards.Dashboard - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - dashs, err = dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ - DashboardUIDs: dashboardUids, - OrgId: user.GetOrgID(), - }) - } else { - dashs, err = dr.dashboardStore.GetDashboards(ctx, dashboardsQuery) - } + OrgId: user.GetOrgID(), + }) if err != nil { return nil, err } @@ -1573,67 +1414,63 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb }(time.Now()) } - if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesClientDashboardsFolders) { - if query.OrgId == 0 { - requester, err := identity.GetRequester(ctx) + if query.OrgId == 0 { + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + query.OrgId = requester.GetOrgID() + } + + response, err := dr.searchDashboardsThroughK8sRaw(ctx, query) + if err != nil { + return nil, err + } + + folderNames, err := dr.fetchFolderNames(ctx, query, response.Hits) + if err != nil { + return nil, err + } + + finalResults := make([]dashboards.DashboardSearchProjection, len(response.Hits)) + for i, hit := range response.Hits { + folderTitle := "" + folderID := int64(0) + if f, ok := folderNames[hit.Folder]; ok { + folderTitle = f.Title + folderID = f.ID + } + + result := dashboards.DashboardSearchProjection{ + ID: hit.Field.GetNestedInt64(resource.SEARCH_FIELD_LEGACY_ID), + UID: hit.Name, + OrgID: query.OrgId, + Title: hit.Title, + Slug: slugify.Slugify(hit.Title), + IsFolder: false, + FolderUID: hit.Folder, + FolderTitle: folderTitle, + FolderID: folderID, + FolderSlug: slugify.Slugify(folderTitle), + Tags: hit.Tags, + } + + if hit.Field != nil && query.Sort.Name != "" { + fieldName, _, err := legacysearcher.ParseSortName(query.Sort.Name) if err != nil { return nil, err } - query.OrgId = requester.GetOrgID() + result.SortMeta = hit.Field.GetNestedInt64(fieldName) } - response, err := dr.searchDashboardsThroughK8sRaw(ctx, query) - if err != nil { - return nil, err + if hit.Resource == folderv1.RESOURCE { + result.IsFolder = true } - folderNames, err := dr.fetchFolderNames(ctx, query, response.Hits) - if err != nil { - return nil, err - } - - finalResults := make([]dashboards.DashboardSearchProjection, len(response.Hits)) - for i, hit := range response.Hits { - folderTitle := "" - folderID := int64(0) - if f, ok := folderNames[hit.Folder]; ok { - folderTitle = f.Title - folderID = f.ID - } - - result := dashboards.DashboardSearchProjection{ - ID: hit.Field.GetNestedInt64(resource.SEARCH_FIELD_LEGACY_ID), - UID: hit.Name, - OrgID: query.OrgId, - Title: hit.Title, - Slug: slugify.Slugify(hit.Title), - IsFolder: false, - FolderUID: hit.Folder, - FolderTitle: folderTitle, - FolderID: folderID, - FolderSlug: slugify.Slugify(folderTitle), - Tags: hit.Tags, - } - - if hit.Field != nil && query.Sort.Name != "" { - fieldName, _, err := legacysearcher.ParseSortName(query.Sort.Name) - if err != nil { - return nil, err - } - result.SortMeta = hit.Field.GetNestedInt64(fieldName) - } - - if hit.Resource == folderv1.RESOURCE { - result.IsFolder = true - } - - finalResults[i] = result - } - - return finalResults, nil + finalResults[i] = result } - return dr.dashboardStore.FindDashboards(ctx, query) + return finalResults, nil } type folderRes struct { @@ -1680,11 +1517,7 @@ func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *das } func (dr *DashboardServiceImpl) GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return dr.listDashboardsThroughK8s(ctx, orgID) - } - - return dr.dashboardStore.GetAllDashboardsByOrgId(ctx, orgID) + return dr.listDashboardsThroughK8s(ctx, orgID) } func getHitType(item dashboards.DashboardSearchProjection) model.HitType { @@ -1743,51 +1576,43 @@ func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashb } func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) ([]*dashboards.DashboardTagCloudItem, error) { - if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesClientDashboardsFolders) { - res, err := dr.k8sclient.Search(ctx, query.OrgID, &resourcepb.ResourceSearchRequest{ - Facet: map[string]*resourcepb.ResourceSearchRequest_Facet{ - "tags": { - Field: "tags", - Limit: 100000, - }, + res, err := dr.k8sclient.Search(ctx, query.OrgID, &resourcepb.ResourceSearchRequest{ + Facet: map[string]*resourcepb.ResourceSearchRequest_Facet{ + "tags": { + Field: "tags", + Limit: 100000, }, - Limit: 100000}) - if err != nil { - return nil, err - } - facet, ok := res.Facet["tags"] - if !ok { - return []*dashboards.DashboardTagCloudItem{}, nil - } - - results := make([]*dashboards.DashboardTagCloudItem, len(facet.Terms)) - for i, item := range facet.Terms { - results[i] = &dashboards.DashboardTagCloudItem{ - Term: item.Term, - Count: int(item.Count), - } - } - - return results, nil + }, + Limit: 100000}) + if err != nil { + return nil, err + } + facet, ok := res.Facet["tags"] + if !ok { + return []*dashboards.DashboardTagCloudItem{}, nil } - return dr.dashboardStore.GetDashboardTags(ctx, query) + results := make([]*dashboards.DashboardTagCloudItem, len(facet.Terms)) + for i, item := range facet.Terms { + results[i] = &dashboards.DashboardTagCloudItem{ + Term: item.Term, + Count: int(item.Count), + } + } + + return results, nil } func (dr DashboardServiceImpl) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ - OrgId: orgID, - FolderUIDs: folderUIDs, - }) - if err != nil { - return 0, err - } - - return int64(len(dashs)), nil + dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ + OrgId: orgID, + FolderUIDs: folderUIDs, + }) + if err != nil { + return 0, err } - return dr.dashboardStore.CountDashboardsInFolders(ctx, &dashboards.CountDashboardsInFolderRequest{FolderUIDs: folderUIDs, OrgID: orgID}) + return int64(len(dashs)), nil } func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) error { diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 34a9279bec4..95e5a0a9023 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -54,216 +54,94 @@ func TestMain(m *testing.M) { testsuite.Run(m) } -func TestDashboardService(t *testing.T) { - t.Run("Dashboard service tests", func(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) - defer fakeStore.AssertExpectations(t) +func TestDashboardServiceValidation(t *testing.T) { + fakeStore := dashboards.FakeDashboardStore{} + fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) + defer fakeStore.AssertExpectations(t) - folderSvc := foldertest.NewFakeService() - service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - log: log.New("test.logger"), - dashboardStore: &fakeStore, - folderService: folderSvc, - ac: actest.FakeAccessControl{ExpectedEvaluate: true}, - features: featuremgmt.WithFeatures(), - publicDashboardService: fakePublicDashboardService, - } - folderStore := foldertest.FakeFolderStore{} - folderStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(nil, dashboards.ErrFolderNotFound).Once() - service.folderStore = &folderStore + service := &DashboardServiceImpl{ + cfg: setting.NewCfg(), + log: log.New("test.logger"), + dashboardStore: &fakeStore, + folderService: foldertest.NewFakeService(), + ac: actest.FakeAccessControl{ExpectedEvaluate: true}, + features: featuremgmt.WithFeatures(), + publicDashboardService: fakePublicDashboardService, + } - t.Run("Save dashboard validation", func(t *testing.T) { - dto := &dashboards.SaveDashboardDTO{} + ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") + k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) - t.Run("When saving a dashboard with empty title it should return error", func(t *testing.T) { - titles := []string{"", " ", " \t "} + t.Run("Save dashboard validation", func(t *testing.T) { + dto := &dashboards.SaveDashboardDTO{} - for _, title := range titles { - dto.Dashboard = dashboards.NewDashboard(title) - _, err := service.SaveDashboard(context.Background(), dto, false) - require.Equal(t, err, dashboards.ErrDashboardTitleEmpty) + t.Run("When saving a dashboard with empty title it should return error", func(t *testing.T) { + titles := []string{"", " ", " \t "} + + for _, title := range titles { + dto.Dashboard = dashboards.NewDashboard(title) + _, err := service.SaveDashboard(ctx, dto, false) + require.Equal(t, err, dashboards.ErrDashboardTitleEmpty) + } + }) + + 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 + 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 + 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(ctx, dto, false) + require.Equal(t, err, dashboards.ErrDashboardMessageTooLong) + + // set to a shorter message for the rest of the tests + dto.Message = `message` + }) + + t.Run("Should return validation error if folder is named General", func(t *testing.T) { + dto.Dashboard = dashboards.NewDashboardFolder("General") + _, err := service.SaveDashboard(ctx, dto, false) + require.Equal(t, err, dashboards.ErrDashboardFolderNameExists) + }) + + t.Run("When saving a dashboard should validate uid", func(t *testing.T) { + testCases := []struct { + Uid string + Error error + }{ + {Uid: "", Error: nil}, + {Uid: " ", Error: nil}, + {Uid: " \t ", Error: nil}, + {Uid: "asdf90_-", Error: nil}, + {Uid: "asdf/90", Error: dashboards.ErrDashboardInvalidUid}, + {Uid: " asdfghjklqwertyuiopzxcvbnmasdfghjklqwer ", Error: nil}, + {Uid: "asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm", Error: dashboards.ErrDashboardUidTooLong}, + } + + for _, tc := range testCases { + dto.Dashboard = dashboards.NewDashboard("title") + dto.Dashboard.SetUID(tc.Uid) + dto.User = &user.SignedInUser{} + + if tc.Error == nil { + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once() } - }) - - 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 - 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 - 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) - require.Equal(t, err, dashboards.ErrDashboardMessageTooLong) - - // set to a shorter message for the rest of the tests - dto.Message = `message` - }) - - t.Run("Should return validation error if folder is named General", func(t *testing.T) { - dto.Dashboard = dashboards.NewDashboardFolder("General") - _, err := service.SaveDashboard(context.Background(), dto, false) - require.Equal(t, err, dashboards.ErrDashboardFolderNameExists) - }) - - t.Run("When saving a dashboard should validate uid", func(t *testing.T) { - testCases := []struct { - Uid string - Error error - }{ - {Uid: "", Error: nil}, - {Uid: " ", Error: nil}, - {Uid: " \t ", Error: nil}, - {Uid: "asdf90_-", Error: nil}, - {Uid: "asdf/90", Error: dashboards.ErrDashboardInvalidUid}, - {Uid: " asdfghjklqwertyuiopzxcvbnmasdfghjklqwer ", Error: nil}, - {Uid: "asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm", Error: dashboards.ErrDashboardUidTooLong}, - } - - for _, tc := range testCases { - dto.Dashboard = dashboards.NewDashboard("title") - dto.Dashboard.SetUID(tc.Uid) - dto.User = &user.SignedInUser{} - - if tc.Error == nil { - fakeStore.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() - } - _, err := service.BuildSaveDashboardCommand(context.Background(), dto, false) - require.Equal(t, err, tc.Error) - } - }) - - t.Run("Should return validation error if a folder that is specified can't be found", func(t *testing.T) { - dto.Dashboard = dashboards.NewDashboard("Dash") - dto.Dashboard.FolderUID = "non-existing-folder" - folderSvc := foldertest.FakeService{ExpectedError: dashboards.ErrFolderNotFound} - service.folderService = &folderSvc - _, err := service.SaveDashboard(context.Background(), dto, false) - require.Equal(t, err, dashboards.ErrFolderNotFound) - }) - - t.Run("Should return validation error if dashboard is provisioned", func(t *testing.T) { - fakeStore.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() - fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(&dashboards.DashboardProvisioningSearchResults{}, nil).Once() - - dto.Dashboard = dashboards.NewDashboard("Dash") - dto.Dashboard.SetID(3) - dto.User = &user.SignedInUser{UserID: 1} - _, err := service.SaveDashboard(context.Background(), dto, false) - require.Equal(t, err, dashboards.ErrDashboardCannotSaveProvisionedDashboard) - }) - - t.Run("Should not return validation error if dashboard is provisioned but UI updates allowed", func(t *testing.T) { - fakeStore.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() - fakeStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{Data: simplejson.New()}, nil).Once() - - dto.Dashboard = dashboards.NewDashboard("Dash") - dto.Dashboard.SetID(3) - dto.User = &user.SignedInUser{UserID: 1} - _, err := service.SaveDashboard(context.Background(), dto, true) - require.NoError(t, err) - }) + _, err := service.BuildSaveDashboardCommand(ctx, dto, false) + require.Equal(t, err, tc.Error) + } }) - t.Run("Save provisioned dashboard validation", func(t *testing.T) { - dto := &dashboards.SaveDashboardDTO{} - - t.Run("Should not return validation error if dashboard is provisioned", func(t *testing.T) { - fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand"), mock.AnythingOfType("*dashboards.DashboardProvisioning")).Return(&dashboards.Dashboard{Data: simplejson.New()}, nil).Once() - dto.Dashboard = dashboards.NewDashboard("Dash") - dto.Dashboard.SetID(3) - dto.User = &user.SignedInUser{UserID: 1} - _, err := service.SaveProvisionedDashboard(context.Background(), dto, nil) - require.NoError(t, err) - }) - - t.Run("Should override invalid refresh interval if dashboard is provisioned", func(t *testing.T) { - fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand"), mock.AnythingOfType("*dashboards.DashboardProvisioning")).Return(&dashboards.Dashboard{Data: simplejson.New()}, nil).Once() - oldRefreshInterval := service.cfg.MinRefreshInterval - service.cfg.MinRefreshInterval = "5m" - defer func() { service.cfg.MinRefreshInterval = oldRefreshInterval }() - - dto.Dashboard = dashboards.NewDashboard("Dash") - dto.Dashboard.SetID(3) - dto.User = &user.SignedInUser{UserID: 1} - dto.Dashboard.Data.Set("refresh", "1s") - _, err := service.SaveProvisionedDashboard(context.Background(), dto, nil) - require.NoError(t, err) - require.Equal(t, dto.Dashboard.Data.Get("refresh").MustString(), "5m") - }) - }) - - t.Run("Import dashboard validation", func(t *testing.T) { - dto := &dashboards.SaveDashboardDTO{} - - t.Run("Should return validation error if dashboard is provisioned", func(t *testing.T) { - fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(&dashboards.DashboardProvisioningSearchResults{}, nil).Once() - - dto.Dashboard = dashboards.NewDashboard("Dash") - dto.Dashboard.SetID(3) - dto.User = &user.SignedInUser{UserID: 1} - _, err := service.ImportDashboard(context.Background(), dto) - require.Equal(t, err, dashboards.ErrDashboardCannotSaveProvisionedDashboard) - }) - }) - - t.Run("Given provisioned dashboard", func(t *testing.T) { - t.Run("DeleteProvisionedDashboard should delete it", func(t *testing.T) { - args := &dashboards.DeleteDashboardCommand{OrgID: 1, ID: 1} - fakeStore.On("DeleteDashboard", mock.Anything, args).Return(nil).Once() - fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - err := service.DeleteProvisionedDashboard(context.Background(), 1, 1) - require.NoError(t, err) - }) - - t.Run("DeleteDashboard should fail to delete it when provisioning information is missing", func(t *testing.T) { - fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(&dashboards.DashboardProvisioningSearchResults{}, nil).Once() - err := service.DeleteDashboard(context.Background(), 1, "", 1) - require.Equal(t, err, dashboards.ErrDashboardCannotDeleteProvisionedDashboard) - }) - }) - - t.Run("Given non provisioned dashboard", func(t *testing.T) { - t.Run("DeleteProvisionedDashboard should delete the dashboard", func(t *testing.T) { - args := &dashboards.DeleteDashboardCommand{OrgID: 1, ID: 1} - fakeStore.On("DeleteDashboard", mock.Anything, args).Return(nil).Once() - fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - err := service.DeleteProvisionedDashboard(context.Background(), 1, 1) - require.NoError(t, err) - }) - - t.Run("DeleteDashboard should delete it", func(t *testing.T) { - args := &dashboards.DeleteDashboardCommand{OrgID: 1, ID: 1} - fakeStore.On("DeleteDashboard", mock.Anything, args).Return(nil).Once() - fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(nil, nil).Once() - fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - err := service.DeleteDashboard(context.Background(), 1, "", 1) - require.NoError(t, err) - }) - }) - - t.Run("Count dashboards in folder", func(t *testing.T) { - fakeStore.On("CountDashboardsInFolders", mock.Anything, mock.AnythingOfType("*dashboards.CountDashboardsInFolderRequest")).Return(int64(3), nil) - folderSvc.ExpectedFolder = &folder.Folder{UID: "i am a folder"} - // set up a ctx with signed in user - usr := &user.SignedInUser{UserID: 1} - ctx := identity.WithRequester(context.Background(), usr) - - count, err := service.CountInFolders(ctx, 1, []string{"i am a folder"}, usr) - require.NoError(t, err) - require.Equal(t, int64(3), count) - }) - - t.Run("Delete dashboards in folder", func(t *testing.T) { - args := &dashboards.DeleteDashboardsInFolderRequest{OrgID: 1, FolderUIDs: []string{"uid"}} - fakeStore.On("DeleteDashboardsInFolders", mock.Anything, args).Return(nil).Once() - fakeStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once() - fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - err := service.DeleteInFolders(context.Background(), 1, []string{"uid"}, nil) - require.NoError(t, err) + t.Run("Should return validation error if a folder that is specified can't be found", func(t *testing.T) { + dto.Dashboard = dashboards.NewDashboard("Dash") + dto.Dashboard.FolderUID = "non-existing-folder" + folderSvc := foldertest.FakeService{ExpectedError: dashboards.ErrFolderNotFound} + service.folderService = &folderSvc + _, err := service.SaveDashboard(ctx, dto, false) + require.Equal(t, err, dashboards.ErrFolderNotFound) }) }) } @@ -271,7 +149,6 @@ func TestDashboardService(t *testing.T) { func setupK8sDashboardTests(service *DashboardServiceImpl) (context.Context, *client.MockK8sHandler) { mockCli := new(client.MockK8sHandler) service.k8sclient = mockCli - service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders) ctx := context.Background() userCtx := &user.SignedInUser{UserID: 1, OrgID: 1} @@ -281,27 +158,15 @@ func setupK8sDashboardTests(service *DashboardServiceImpl) (context.Context, *cl } func TestGetDashboard(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), } query := &dashboards.GetDashboardQuery{ UID: "test-uid", OrgID: 1, } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("GetDashboard", mock.Anything, query).Return(&dashboards.Dashboard{}, nil).Once() - dashboard, err := service.GetDashboard(context.Background(), query) - require.NoError(t, err) - require.NotNil(t, dashboard) - fakeStore.AssertExpectations(t) - }) - - t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { + t.Run("Should get dashboard", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ @@ -421,426 +286,350 @@ func TestGetDashboard(t *testing.T) { } func TestGetAllDashboards(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), + } + ctx, k8sCliMock := setupK8sDashboardTests(service) + + dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "generation": int64(1), + }, + "spec": map[string]any{ + "test": "test", + "title": "testing slugify", + }, + }} + + dashboardExpected := dashboards.Dashboard{ + UID: "uid", // uid is the name of the k8s object + Title: "testing slugify", + Slug: "testing-slugify", // slug is taken from title + OrgID: 1, // orgID is populated from the query + Version: 1, // default to version 1 + Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("GetAllDashboardsByOrgId", mock.Anything, int64(1)).Return([]*dashboards.Dashboard{}, nil).Once() - dashboard, err := service.GetAllDashboardsByOrgId(context.Background(), 1) - require.NoError(t, err) - require.NotNil(t, dashboard) - fakeStore.AssertExpectations(t) - }) + k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) + k8sCliMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once() - t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { - ctx, k8sCliMock := setupK8sDashboardTests(service) - - dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{ - "metadata": map[string]any{ - "name": "uid", - "generation": int64(1), - }, - "spec": map[string]any{ - "test": "test", - "title": "testing slugify", - }, - }} - - dashboardExpected := dashboards.Dashboard{ - UID: "uid", // uid is the name of the k8s object - Title: "testing slugify", - Slug: "testing-slugify", // slug is taken from title - OrgID: 1, // orgID is populated from the query - Version: 1, // default to version 1 - Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), - } - - k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*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) - require.NoError(t, err) - require.NotNil(t, dashes) - k8sCliMock.AssertExpectations(t) - // make sure the conversion is working - require.True(t, reflect.DeepEqual(dashes, []*dashboards.Dashboard{&dashboardExpected})) - }) + dashes, err := service.GetAllDashboardsByOrgId(ctx, 1) + require.NoError(t, err) + require.NotNil(t, dashes) + k8sCliMock.AssertExpectations(t) + // make sure the conversion is working + require.True(t, reflect.DeepEqual(dashes, []*dashboards.Dashboard{&dashboardExpected})) } func TestGetAllDashboardsByOrgId(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), + } + ctx, k8sCliMock := setupK8sDashboardTests(service) + + dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "generation": int64(1), + }, + "spec": map[string]any{ + "test": "test", + "title": "testing slugify", + }, + }} + + dashboardExpected := dashboards.Dashboard{ + UID: "uid", // uid is the name of the k8s object + Title: "testing slugify", + Slug: "testing-slugify", // slug is taken from title + OrgID: 1, // orgID is populated from the query + Version: 1, // default to version 1 + Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("GetAllDashboardsByOrgId", mock.Anything, int64(1)).Return([]*dashboards.Dashboard{}, nil).Once() - dashboard, err := service.GetAllDashboardsByOrgId(context.Background(), 1) - require.NoError(t, err) - require.NotNil(t, dashboard) - fakeStore.AssertExpectations(t) - }) + k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) + k8sCliMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once() - t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { - ctx, k8sCliMock := setupK8sDashboardTests(service) - - dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{ - "metadata": map[string]any{ - "name": "uid", - "generation": int64(1), - }, - "spec": map[string]any{ - "test": "test", - "title": "testing slugify", - }, - }} - - dashboardExpected := dashboards.Dashboard{ - UID: "uid", // uid is the name of the k8s object - Title: "testing slugify", - Slug: "testing-slugify", // slug is taken from title - OrgID: 1, // orgID is populated from the query - Version: 1, // default to version 1 - Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), - } - - k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*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) - require.NoError(t, err) - require.NotNil(t, dashes) - k8sCliMock.AssertExpectations(t) - // make sure the conversion is working - require.True(t, reflect.DeepEqual(dashes, []*dashboards.Dashboard{&dashboardExpected})) - }) + dashes, err := service.GetAllDashboardsByOrgId(ctx, 1) + require.NoError(t, err) + require.NotNil(t, dashes) + k8sCliMock.AssertExpectations(t) + // make sure the conversion is working + require.True(t, reflect.DeepEqual(dashes, []*dashboards.Dashboard{&dashboardExpected})) } func TestGetProvisionedDashboardData(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), orgService: &orgtest.FakeOrgService{ ExpectedOrgs: []*org.OrgDTO{{ID: 1}, {ID: 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("GetProvisionedDashboardData", mock.Anything, "test").Return([]*dashboards.DashboardProvisioning{}, nil).Once() - dashboard, err := service.GetProvisionedDashboardData(context.Background(), "test") - require.NoError(t, err) - require.NotNil(t, dashboard) - fakeStore.AssertExpectations(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) - provisioningTimestamp := int64(1234567) - k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") - repo := "test" - k8sCliMock.On("Search", mock.Anything, int64(1), - mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool { - // make sure the kind is added to the query - return req.Options.Fields[0].Values[0] == string(utils.ManagerKindClassicFP) && // nolint:staticcheck - req.Options.Fields[1].Values[0] == repo - })).Return(&resourcepb.ResourceSearchResponse{ - Results: &resourcepb.ResourceTable{ - Columns: []*resourcepb.ResourceTableColumnDefinition{}, - Rows: []*resourcepb.ResourceTableRow{}, - }, - TotalHits: 0, - }, nil).Once() - k8sCliMock.On("Search", mock.Anything, int64(2), mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool { + ctx, k8sCliMock := setupK8sDashboardTests(service) + provisioningTimestamp := int64(1234567) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") + repo := "test" + k8sCliMock.On("Search", mock.Anything, int64(1), + mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool { // make sure the kind is added to the query return req.Options.Fields[0].Values[0] == string(utils.ManagerKindClassicFP) && // nolint:staticcheck req.Options.Fields[1].Values[0] == repo })).Return(&resourcepb.ResourceSearchResponse{ - Results: &resourcepb.ResourceTable{ - Columns: []*resourcepb.ResourceTableColumnDefinition{ - { - Name: "title", - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: "folder", - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_LEGACY_ID, - Type: resourcepb.ResourceTableColumnDefinition_INT64, - }, - { - Name: resource.SEARCH_FIELD_MANAGER_KIND, // nolint:staticcheck - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_MANAGER_ID, - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_SOURCE_PATH, - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_SOURCE_CHECKSUM, - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_SOURCE_TIME, - Type: resourcepb.ResourceTableColumnDefinition_INT64, - }, + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{}, + Rows: []*resourcepb.ResourceTableRow{}, + }, + TotalHits: 0, + }, nil).Once() + k8sCliMock.On("Search", mock.Anything, int64(2), mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool { + // make sure the kind is added to the query + return req.Options.Fields[0].Values[0] == string(utils.ManagerKindClassicFP) && // nolint:staticcheck + req.Options.Fields[1].Values[0] == repo + })).Return(&resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resourcepb.ResourceTableColumnDefinition_STRING, }, - Rows: []*resourcepb.ResourceTableRow{ - { - Key: &resourcepb.ResourceKey{ - Name: "uid", - Resource: "dashboard", - }, - Cells: [][]byte{ - []byte("Dashboard 1"), - []byte("folder 1"), - []byte("1"), - []byte(string(utils.ManagerKindClassicFP)), // nolint:staticcheck - []byte(repo), - []byte("path/to/file"), - []byte("hash"), - []byte("1234567"), - }, + { + Name: "folder", + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_LEGACY_ID, + Type: resourcepb.ResourceTableColumnDefinition_INT64, + }, + { + Name: resource.SEARCH_FIELD_MANAGER_KIND, // nolint:staticcheck + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_MANAGER_ID, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_SOURCE_PATH, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_SOURCE_CHECKSUM, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_SOURCE_TIME, + Type: resourcepb.ResourceTableColumnDefinition_INT64, + }, + }, + Rows: []*resourcepb.ResourceTableRow{ + { + Key: &resourcepb.ResourceKey{ + Name: "uid", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder 1"), + []byte("1"), + []byte(string(utils.ManagerKindClassicFP)), // nolint:staticcheck + []byte(repo), + []byte("path/to/file"), + []byte("hash"), + []byte("1234567"), }, }, }, - TotalHits: 1, - }, nil).Once() - dashes, err := service.GetProvisionedDashboardData(ctx, repo) - require.NoError(t, err) - require.Len(t, dashes, 1) - require.Equal(t, dashes[0], &dashboards.DashboardProvisioning{ - ID: 0, - DashboardID: 1, - Name: "test", - ExternalID: "path/to/file", - CheckSum: "hash", - Updated: provisioningTimestamp, - }) - k8sCliMock.AssertExpectations(t) + }, + TotalHits: 1, + }, nil).Once() + dashes, err := service.GetProvisionedDashboardData(ctx, repo) + require.NoError(t, err) + require.Len(t, dashes, 1) + require.Equal(t, dashes[0], &dashboards.DashboardProvisioning{ + ID: 0, + DashboardID: 1, + Name: "test", + ExternalID: "path/to/file", + CheckSum: "hash", + Updated: provisioningTimestamp, }) + k8sCliMock.AssertExpectations(t) } func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), orgService: &orgtest.FakeOrgService{ ExpectedOrgs: []*org.OrgDTO{{ID: 1}, {ID: 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("GetProvisionedDataByDashboardID", mock.Anything, int64(1)).Return(&dashboards.DashboardProvisioningSearchResults{}, nil).Once() - dashboard, err := service.GetProvisionedDashboardDataByDashboardID(context.Background(), 1) - require.NoError(t, err) - require.NotNil(t, dashboard) - fakeStore.AssertExpectations(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) - provisioningTimestamp := int64(1234567) - k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") - k8sCliMock.On("Search", mock.Anything, int64(1), mock.Anything).Return(&resourcepb.ResourceSearchResponse{ - Results: &resourcepb.ResourceTable{ - Columns: []*resourcepb.ResourceTableColumnDefinition{}, - Rows: []*resourcepb.ResourceTableRow{}, - }, - TotalHits: 0, - }, nil) - k8sCliMock.On("Search", mock.Anything, int64(2), mock.Anything).Return(&resourcepb.ResourceSearchResponse{ - Results: &resourcepb.ResourceTable{ - Columns: []*resourcepb.ResourceTableColumnDefinition{ - { - Name: "title", - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: "folder", - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_LEGACY_ID, - Type: resourcepb.ResourceTableColumnDefinition_INT64, - }, - { - Name: resource.SEARCH_FIELD_MANAGER_KIND, // nolint:staticcheck - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_MANAGER_ID, - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_SOURCE_PATH, - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_SOURCE_CHECKSUM, - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_SOURCE_TIME, - Type: resourcepb.ResourceTableColumnDefinition_INT64, - }, + ctx, k8sCliMock := setupK8sDashboardTests(service) + provisioningTimestamp := int64(1234567) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") + k8sCliMock.On("Search", mock.Anything, int64(1), mock.Anything).Return(&resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{}, + Rows: []*resourcepb.ResourceTableRow{}, + }, + TotalHits: 0, + }, nil) + k8sCliMock.On("Search", mock.Anything, int64(2), mock.Anything).Return(&resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resourcepb.ResourceTableColumnDefinition_STRING, }, - Rows: []*resourcepb.ResourceTableRow{ - { - Key: &resourcepb.ResourceKey{ - Name: "uid", - Resource: "dashboard", - }, - Cells: [][]byte{ - []byte("Dashboard 1"), - []byte("folder 1"), - []byte("1"), - []byte(string(utils.ManagerKindClassicFP)), // nolint:staticcheck - []byte("test"), - []byte("path/to/file"), - []byte("hash"), - []byte("1234567"), - }, + { + Name: "folder", + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_LEGACY_ID, + Type: resourcepb.ResourceTableColumnDefinition_INT64, + }, + { + Name: resource.SEARCH_FIELD_MANAGER_KIND, // nolint:staticcheck + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_MANAGER_ID, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_SOURCE_PATH, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_SOURCE_CHECKSUM, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_SOURCE_TIME, + Type: resourcepb.ResourceTableColumnDefinition_INT64, + }, + }, + Rows: []*resourcepb.ResourceTableRow{ + { + Key: &resourcepb.ResourceKey{ + Name: "uid", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder 1"), + []byte("1"), + []byte(string(utils.ManagerKindClassicFP)), // nolint:staticcheck + []byte("test"), + []byte("path/to/file"), + []byte("hash"), + []byte("1234567"), }, }, }, - TotalHits: 1, - }, nil) - dash, err := service.GetProvisionedDashboardDataByDashboardID(ctx, 1) - require.NoError(t, err) - require.Equal(t, dash, &dashboards.DashboardProvisioning{ - ID: 0, - DashboardID: 1, - Name: "test", - ExternalID: "path/to/file", - CheckSum: "hash", - Updated: provisioningTimestamp, - }) - k8sCliMock.AssertExpectations(t) + }, + TotalHits: 1, + }, nil) + dash, err := service.GetProvisionedDashboardDataByDashboardID(ctx, 1) + require.NoError(t, err) + require.Equal(t, dash, &dashboards.DashboardProvisioning{ + ID: 0, + DashboardID: 1, + Name: "test", + ExternalID: "path/to/file", + CheckSum: "hash", + Updated: provisioningTimestamp, }) + k8sCliMock.AssertExpectations(t) } func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), orgService: &orgtest.FakeOrgService{ ExpectedOrgs: []*org.OrgDTO{{ID: 1}, {ID: 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("GetProvisionedDataByDashboardUID", mock.Anything, int64(1), "test").Return(&dashboards.DashboardProvisioningSearchResults{}, nil).Once() - dashboard, err := service.GetProvisionedDashboardDataByDashboardUID(context.Background(), 1, "test") - require.NoError(t, err) - require.NotNil(t, dashboard) - fakeStore.AssertExpectations(t) - }) - - t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { - ctx, k8sCliMock := setupK8sDashboardTests(service) - provisioningTimestamp := int64(1234567) - k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") - k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{ - Results: &resourcepb.ResourceTable{ - Columns: []*resourcepb.ResourceTableColumnDefinition{ - { - Name: "title", - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: "folder", - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_LEGACY_ID, - Type: resourcepb.ResourceTableColumnDefinition_INT64, - }, - { - Name: resource.SEARCH_FIELD_MANAGER_KIND, // nolint:staticcheck - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_MANAGER_ID, - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_SOURCE_PATH, - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_SOURCE_CHECKSUM, - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: resource.SEARCH_FIELD_SOURCE_TIME, - Type: resourcepb.ResourceTableColumnDefinition_INT64, - }, + ctx, k8sCliMock := setupK8sDashboardTests(service) + provisioningTimestamp := int64(1234567) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resourcepb.ResourceTableColumnDefinition_STRING, }, - Rows: []*resourcepb.ResourceTableRow{ - { - Key: &resourcepb.ResourceKey{ - Name: "uid", - Resource: "dashboard", - }, - Cells: [][]byte{ - []byte("Dashboard 1"), - []byte("folder 1"), - []byte("1"), - []byte(string(utils.ManagerKindClassicFP)), // nolint:staticcheck - []byte("test"), - []byte("path/to/file"), - []byte("hash"), - []byte("1234567"), - }, + { + Name: "folder", + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_LEGACY_ID, + Type: resourcepb.ResourceTableColumnDefinition_INT64, + }, + { + Name: resource.SEARCH_FIELD_MANAGER_KIND, // nolint:staticcheck + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_MANAGER_ID, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_SOURCE_PATH, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_SOURCE_CHECKSUM, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: resource.SEARCH_FIELD_SOURCE_TIME, + Type: resourcepb.ResourceTableColumnDefinition_INT64, + }, + }, + Rows: []*resourcepb.ResourceTableRow{ + { + Key: &resourcepb.ResourceKey{ + Name: "uid", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder 1"), + []byte("1"), + []byte(string(utils.ManagerKindClassicFP)), // nolint:staticcheck + []byte("test"), + []byte("path/to/file"), + []byte("hash"), + []byte("1234567"), }, }, }, - TotalHits: 1, - }, nil).Once() - dash, err := service.GetProvisionedDashboardDataByDashboardUID(ctx, 1, "uid") - require.NoError(t, err) - require.Equal(t, dash, &dashboards.DashboardProvisioning{ - ID: 0, - DashboardID: 1, - Name: "test", - ExternalID: "path/to/file", - CheckSum: "hash", - Updated: provisioningTimestamp, - }) - k8sCliMock.AssertExpectations(t) + }, + TotalHits: 1, + }, nil).Once() + dash, err := service.GetProvisionedDashboardDataByDashboardUID(ctx, 1, "uid") + require.NoError(t, err) + require.Equal(t, dash, &dashboards.DashboardProvisioning{ + ID: 0, + DashboardID: 1, + Name: "test", + ExternalID: "path/to/file", + CheckSum: "hash", + Updated: provisioningTimestamp, }) + k8sCliMock.AssertExpectations(t) } func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), orgService: &orgtest.FakeOrgService{ ExpectedOrgs: []*org.OrgDTO{{ID: 1}, {ID: 2}}, }, @@ -848,19 +637,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { log: log.NewNopLogger(), } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("DeleteOrphanedProvisionedDashboards", mock.Anything, &dashboards.DeleteOrphanedProvisionedDashboardsCommand{ - ReaderNames: []string{"test"}, - }).Return(nil).Once() - err := service.DeleteOrphanedProvisionedDashboards(context.Background(), &dashboards.DeleteOrphanedProvisionedDashboardsCommand{ - ReaderNames: []string{"test"}, - }) - require.NoError(t, err) - fakeStore.AssertExpectations(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) { + t.Run("Should 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) @@ -1007,8 +784,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { t.Run("Should retry until deleted dashboard not found in search", func(t *testing.T) { repo := "test" singleOrgService := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), orgService: &orgtest.FakeOrgService{ ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, }, @@ -1101,8 +877,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { t.Run("Will not wait for indexer when no dashboards were deleted", func(t *testing.T) { repo := "test" singleOrgService := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), orgService: &orgtest.FakeOrgService{ ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, }, @@ -1141,92 +916,78 @@ func TestUnprovisionDashboard(t *testing.T) { }, } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("UnprovisionDashboard", mock.Anything, int64(1)).Return(nil).Once() - err := service.UnprovisionDashboard(context.Background(), 1) - require.NoError(t, err) - fakeStore.AssertExpectations(t) - }) - - t.Run("Should use Kubernetes client if feature flags are enabled - should remove annotations", func(t *testing.T) { - ctx, k8sCliMock := setupK8sDashboardTests(service) - dash := &unstructured.Unstructured{Object: map[string]any{ - "metadata": map[string]any{ - "name": "uid", - "annotations": map[string]any{ - utils.AnnoKeyManagerKind: utils.ManagerKindClassicFP, // nolint:staticcheck - utils.AnnoKeyManagerIdentity: "test", - utils.AnnoKeySourceChecksum: "hash", - utils.AnnoKeySourcePath: "path/to/file", - utils.AnnoKeySourceTimestamp: "2025-01-01T00:00:00Z", + ctx, k8sCliMock := setupK8sDashboardTests(service) + dash := &unstructured.Unstructured{Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "annotations": map[string]any{ + utils.AnnoKeyManagerKind: utils.ManagerKindClassicFP, // nolint:staticcheck + utils.AnnoKeyManagerIdentity: "test", + utils.AnnoKeySourceChecksum: "hash", + utils.AnnoKeySourcePath: "path/to/file", + utils.AnnoKeySourceTimestamp: "2025-01-01T00:00:00Z", + }, + }, + "spec": map[string]any{}, + }} + fakeStore.On("UnprovisionDashboard", mock.Anything, int64(1)).Return(nil).Once() + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dash, nil) + dashWithoutAnnotations := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": dashboardv0.APIVERSION, + "kind": "Dashboard", + "metadata": map[string]any{ + "name": "uid", + "namespace": "default", + "annotations": map[string]any{}, + }, + "spec": map[string]any{ + "uid": "uid", + "version": 1, + }, + }} + // should update it to be without annotations + k8sCliMock.On("Update", mock.Anything, dashWithoutAnnotations, mock.Anything, metav1.UpdateOptions{ + FieldValidation: metav1.FieldValidationIgnore, + }).Return(dashWithoutAnnotations, nil) + k8sCliMock.On("GetNamespace", mock.Anything).Return("default") + k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resourcepb.ResourceTableColumnDefinition_STRING, }, }, - "spec": map[string]any{}, - }} - fakeStore.On("UnprovisionDashboard", mock.Anything, int64(1)).Return(nil).Once() - k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dash, nil) - dashWithoutAnnotations := &unstructured.Unstructured{Object: map[string]any{ - "apiVersion": dashboardv0.APIVERSION, - "kind": "Dashboard", - "metadata": map[string]any{ - "name": "uid", - "namespace": "default", - "annotations": map[string]any{}, - }, - "spec": map[string]any{ - "uid": "uid", - "version": 1, - }, - }} - // should update it to be without annotations - k8sCliMock.On("Update", mock.Anything, dashWithoutAnnotations, mock.Anything, metav1.UpdateOptions{ - FieldValidation: metav1.FieldValidationIgnore, - }).Return(dashWithoutAnnotations, nil) - k8sCliMock.On("GetNamespace", mock.Anything).Return("default") - k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) - k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{ - Results: &resourcepb.ResourceTable{ - Columns: []*resourcepb.ResourceTableColumnDefinition{ - { - Name: "title", - Type: resourcepb.ResourceTableColumnDefinition_STRING, + Rows: []*resourcepb.ResourceTableRow{ + { + Key: &resourcepb.ResourceKey{ + Name: "uid", + Resource: "dashboard", }, - { - Name: "folder", - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - }, - Rows: []*resourcepb.ResourceTableRow{ - { - Key: &resourcepb.ResourceKey{ - Name: "uid", - Resource: "dashboard", - }, - Cells: [][]byte{ - []byte("Dashboard 1"), - []byte("folder 1"), - }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder 1"), }, }, }, - TotalHits: 1, - }, nil) - err := service.UnprovisionDashboard(ctx, 1) - require.NoError(t, err) - k8sCliMock.AssertExpectations(t) - fakeStore.AssertExpectations(t) - }) + }, + TotalHits: 1, + }, nil) + err := service.UnprovisionDashboard(ctx, 1) + require.NoError(t, err) + k8sCliMock.AssertExpectations(t) + fakeStore.AssertExpectations(t) } func TestGetDashboardsByPluginID(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), } - query := &dashboards.GetDashboardsByPluginIDQuery{ PluginID: "testing", OrgID: 1, @@ -1240,119 +1001,87 @@ func TestGetDashboardsByPluginID(t *testing.T) { }, }} - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("GetDashboardsByPluginID", mock.Anything, mock.Anything).Return([]*dashboards.Dashboard{}, nil).Once() - _, err := service.GetDashboardsByPluginID(context.Background(), query) - 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("GetNamespace", mock.Anything, mock.Anything).Return("default") - k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(uidUnstructured, nil) - k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) - k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool { - return ( // gofmt comment helper - req.Options.Fields[0].Key == "manager.kind" && req.Options.Fields[0].Values[0] == string(utils.ManagerKindPlugin) && - req.Options.Fields[1].Key == "manager.id" && req.Options.Fields[1].Values[0] == "testing") - })).Return(&resourcepb.ResourceSearchResponse{ - Results: &resourcepb.ResourceTable{ - Columns: []*resourcepb.ResourceTableColumnDefinition{ - { - Name: "title", - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: "folder", - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, + 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("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool { + return ( // gofmt comment helper + req.Options.Fields[0].Key == "manager.kind" && req.Options.Fields[0].Values[0] == string(utils.ManagerKindPlugin) && + req.Options.Fields[1].Key == "manager.id" && req.Options.Fields[1].Values[0] == "testing") + })).Return(&resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resourcepb.ResourceTableColumnDefinition_STRING, }, - Rows: []*resourcepb.ResourceTableRow{ - { - Key: &resourcepb.ResourceKey{ - Name: "uid", - Resource: "dashboard", - }, - Cells: [][]byte{ - []byte("Dashboard 1"), - []byte("folder 1"), - }, + { + Name: "folder", + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resourcepb.ResourceTableRow{ + { + Key: &resourcepb.ResourceKey{ + Name: "uid", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder 1"), }, }, }, - TotalHits: 1, - }, nil) - dashes, err := service.GetDashboardsByPluginID(ctx, query) - require.NoError(t, err) - require.Len(t, dashes, 1) - k8sCliMock.AssertExpectations(t) - }) + }, + TotalHits: 1, + }, nil) + dashes, err := service.GetDashboardsByPluginID(ctx, query) + require.NoError(t, err) + require.Len(t, dashes, 1) + k8sCliMock.AssertExpectations(t) } func TestSetDefaultPermissionsWhenSavingFolderForProvisionedDashboards(t *testing.T) { fakeStore := dashboards.FakeDashboardStore{} defer fakeStore.AssertExpectations(t) - type testCase struct { - description string - expectedCallsToSetPermissions int - featuresArr []any - } + folderPermService := acmock.NewMockedPermissionsService() + folderPermService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - tcs := []testCase{ - { - description: "folder creation succeeds, via legacy storage", - expectedCallsToSetPermissions: 1, - }, - { - description: "folder creation succeeds, via API Server", - expectedCallsToSetPermissions: 0, - featuresArr: []any{featuremgmt.FlagKubernetesClientDashboardsFolders}, + cfg := setting.NewCfg() + f := ini.Empty() + f.Section("rbac").Key("resources_with_managed_permissions_on_creation").SetValue("folder") + tempCfg, err := setting.NewCfgFromINIFile(f) + require.NoError(t, err) + cfg.RBAC = tempCfg.RBAC + + service := &DashboardServiceImpl{ + cfg: cfg, + dashboardStore: &fakeStore, + folderPermissions: folderPermService, + folderService: &foldertest.FakeService{ + ExpectedFolder: &folder.Folder{ + ID: 0, + UID: "general", + }, }, + ac: actest.FakeAccessControl{ExpectedEvaluate: true}, + acService: &actest.FakeService{}, + log: log.NewNopLogger(), } - for _, tc := range tcs { - t.Run(tc.description, func(t *testing.T) { - folderPermService := acmock.NewMockedPermissionsService() - folderPermService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - - cfg := setting.NewCfg() - f := ini.Empty() - f.Section("rbac").Key("resources_with_managed_permissions_on_creation").SetValue("folder") - tempCfg, err := setting.NewCfgFromINIFile(f) - require.NoError(t, err) - cfg.RBAC = tempCfg.RBAC - - service := &DashboardServiceImpl{ - cfg: cfg, - dashboardStore: &fakeStore, - folderPermissions: folderPermService, - folderService: &foldertest.FakeService{ - ExpectedFolder: &folder.Folder{ - ID: 0, - UID: "general", - }, - }, - ac: actest.FakeAccessControl{ExpectedEvaluate: true}, - acService: &actest.FakeService{}, - log: log.NewNopLogger(), - } - - cmd := &folder.CreateFolderCommand{ - Title: "foo", - OrgID: 1, - } - - service.features = featuremgmt.WithFeatures(tc.featuresArr...) - folder, err := service.SaveFolderForProvisionedDashboards(context.Background(), cmd) - require.NoError(t, err) - require.NotNil(t, folder) - - folderPermService.AssertNumberOfCalls(t, "SetPermissions", tc.expectedCallsToSetPermissions) - }) + cmd := &folder.CreateFolderCommand{ + Title: "foo", + OrgID: 1, } + + service.features = featuremgmt.WithFeatures() + folder, err := service.SaveFolderForProvisionedDashboards(context.Background(), cmd) + require.NoError(t, err) + require.NotNil(t, folder) + + folderPermService.AssertNumberOfCalls(t, "SetPermissions", 0) } func TestSaveProvisionedDashboard(t *testing.T) { @@ -1382,17 +1111,6 @@ func TestSaveProvisionedDashboard(t *testing.T) { Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid"}), }, } - - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil) - fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil) - dashboard, err := service.SaveProvisionedDashboard(context.Background(), query, &dashboards.DashboardProvisioning{}) - require.NoError(t, err) - require.NotNil(t, dashboard) - fakeStore.AssertExpectations(t) - }) - dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", @@ -1404,22 +1122,20 @@ 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(&dashboards.Dashboard{}, nil) - k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) - k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{ - FieldValidation: metav1.FieldValidationIgnore, - }).Return(&dashboardUnstructured, nil) - k8sCliMock.On("GetNamespace", mock.Anything).Return("default") + ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) + k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{ + FieldValidation: metav1.FieldValidationIgnore, + }).Return(&dashboardUnstructured, nil) + k8sCliMock.On("GetNamespace", mock.Anything).Return("default") - dashboard, err := service.SaveProvisionedDashboard(ctx, query, &dashboards.DashboardProvisioning{}) - require.NoError(t, err) - require.NotNil(t, dashboard) - k8sCliMock.AssertExpectations(t) - // ensure the provisioning data is still saved to the db - fakeStore.AssertExpectations(t) - }) + dashboard, err := service.SaveProvisionedDashboard(ctx, query, &dashboards.DashboardProvisioning{}) + require.NoError(t, err) + require.NotNil(t, dashboard) + k8sCliMock.AssertExpectations(t) + // ensure the provisioning data is still saved to the db + fakeStore.AssertExpectations(t) } func TestSaveDashboard(t *testing.T) { @@ -1446,17 +1162,6 @@ func TestSaveDashboard(t *testing.T) { }, } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.Anything).Return(nil, nil) - fakeStore.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil) - fakeStore.On("SaveDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil) - dashboard, err := service.SaveDashboard(context.Background(), query, false) - require.NoError(t, err) - require.NotNil(t, dashboard) - fakeStore.AssertExpectations(t) - }) - dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{ "metadata": map[string]any{ "name": "uid", @@ -1470,6 +1175,7 @@ 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, mock.Anything).Return(nil, nil) k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{ @@ -1483,6 +1189,7 @@ 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, mock.Anything).Return(&dashboardUnstructured, nil) k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{ @@ -1517,17 +1224,6 @@ func TestDeleteDashboard(t *testing.T) { publicDashboardService: fakePublicDashboardService, } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("DeleteDashboard", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.Anything).Return(nil, nil).Once() - fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - - err := service.DeleteDashboard(context.Background(), 1, "uid", 1) - 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("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() @@ -1583,22 +1279,12 @@ func TestDeleteAllDashboards(t *testing.T) { dashboardStore: &fakeStore, } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("DeleteAllDashboards", mock.Anything, mock.Anything).Return(nil).Once() - err := service.DeleteAllDashboards(context.Background(), 1) - require.NoError(t, err) - fakeStore.AssertExpectations(t) - }) + ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { - ctx, k8sCliMock := setupK8sDashboardTests(service) - k8sCliMock.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - - err := service.DeleteAllDashboards(ctx, 1) - require.NoError(t, err) - k8sCliMock.AssertExpectations(t) - }) + err := service.DeleteAllDashboards(ctx, 1) + require.NoError(t, err) + k8sCliMock.AssertExpectations(t) } func TestSearchDashboards(t *testing.T) { @@ -1612,6 +1298,7 @@ func TestSearchDashboards(t *testing.T) { defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ cfg: setting.NewCfg(), + features: featuremgmt.WithFeatures(), dashboardStore: &fakeStore, folderService: fakeFolders, metrics: newDashboardsMetrics(prometheus.NewRegistry()), @@ -1651,38 +1338,8 @@ func TestSearchDashboards(t *testing.T) { query := dashboards.FindPersistedDashboardsQuery{ DashboardUIDs: []string{"uid1", "uid2"}, } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(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"}, - FolderTitle: "testing-folder-1", - FolderSlug: "testing-folder-1", - FolderUID: "f1", - FolderID: 1, - }, - { - UID: "uid2", - Slug: "dashboard-2", - OrgID: 1, - Title: "Dashboard 2", - FolderTitle: "testing-folder-1", - FolderSlug: "testing-folder-1", - FolderUID: "f1", - FolderID: 1, - }, - }, nil).Once() - result, err := service.SearchDashboards(context.Background(), &query) - require.NoError(t, err) - require.Equal(t, expectedResult, result) - fakeStore.AssertExpectations(t) - }) - t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { + t.Run("Should search correctly", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) expectedFolders := model.HitList{ { @@ -1745,7 +1402,7 @@ func TestSearchDashboards(t *testing.T) { t.Run("Should handle Shared with me folder correctly", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) - service.features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagKubernetesClientDashboardsFolders) + service.features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders) k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Search", mock.Anything, int64(1), mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool { if len(req.Options.Fields) == 0 { @@ -1848,11 +1505,8 @@ func TestSearchDashboards(t *testing.T) { } func TestGetDashboards(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), } expectedResult := []*dashboards.Dashboard{ @@ -1899,88 +1553,66 @@ func TestGetDashboards(t *testing.T) { DashboardUIDs: []string{"uid1", "uid2"}, OrgID: 1, } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - // by ids - fakeStore.On("GetDashboards", mock.Anything, queryByIDs).Return(expectedResult, nil).Once() - result, err := service.GetDashboards(context.Background(), queryByIDs) - require.NoError(t, err) - require.Equal(t, expectedResult, result) - fakeStore.AssertExpectations(t) - - // by uids - fakeStore.On("GetDashboards", mock.Anything, queryByUIDs).Return(expectedResult, nil).Once() - result, err = service.GetDashboards(context.Background(), queryByUIDs) - require.NoError(t, err) - require.Equal(t, expectedResult, result) - fakeStore.AssertExpectations(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("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) - k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{ - Results: &resourcepb.ResourceTable{ - Columns: []*resourcepb.ResourceTableColumnDefinition{ - { - Name: "title", - Type: resourcepb.ResourceTableColumnDefinition_STRING, + 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("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resourcepb.ResourceTableRow{ + { + Key: &resourcepb.ResourceKey{ + Name: "uid1", + Resource: "dashboard", }, - { - Name: "folder", - Type: resourcepb.ResourceTableColumnDefinition_STRING, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte(""), }, }, - Rows: []*resourcepb.ResourceTableRow{ - { - Key: &resourcepb.ResourceKey{ - Name: "uid1", - Resource: "dashboard", - }, - Cells: [][]byte{ - []byte("Dashboard 1"), - []byte(""), - }, + { + Key: &resourcepb.ResourceKey{ + Name: "uid2", + Resource: "dashboard", }, - { - Key: &resourcepb.ResourceKey{ - Name: "uid2", - Resource: "dashboard", - }, - Cells: [][]byte{ - []byte("Dashboard 2"), - []byte(""), - }, + Cells: [][]byte{ + []byte("Dashboard 2"), + []byte(""), }, }, }, - TotalHits: 1, - }, nil) + }, + TotalHits: 1, + }, nil) - // by ids - result, err := service.GetDashboards(ctx, queryByIDs) - require.NoError(t, err) - require.Equal(t, expectedResult, result) - k8sCliMock.AssertExpectations(t) + // by ids + result, err := service.GetDashboards(ctx, queryByIDs) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + k8sCliMock.AssertExpectations(t) - // by uids - result, err = service.GetDashboards(ctx, queryByUIDs) - require.NoError(t, err) - require.Equal(t, expectedResult, result) - k8sCliMock.AssertExpectations(t) - }) + // by uids + result, err = service.GetDashboards(ctx, queryByUIDs) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + k8sCliMock.AssertExpectations(t) } func TestGetDashboardUIDByID(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), } expectedResult := &dashboards.DashboardRef{ @@ -1991,51 +1623,39 @@ func TestGetDashboardUIDByID(t *testing.T) { query := &dashboards.GetDashboardRefByIDQuery{ ID: 1, } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("GetDashboardUIDByID", mock.Anything, query).Return(expectedResult, nil).Once() - - result, err := service.GetDashboardUIDByID(context.Background(), query) - require.NoError(t, err) - require.Equal(t, expectedResult, result) - fakeStore.AssertExpectations(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(&resourcepb.ResourceSearchResponse{ - Results: &resourcepb.ResourceTable{ - Columns: []*resourcepb.ResourceTableColumnDefinition{ - { - Name: "title", - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, - { - Name: "folder", - Type: resourcepb.ResourceTableColumnDefinition_STRING, - }, + ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resourcepb.ResourceTableColumnDefinition_STRING, }, - Rows: []*resourcepb.ResourceTableRow{ - { - Key: &resourcepb.ResourceKey{ - Name: "uid1", - Resource: "dashboard", - }, - Cells: [][]byte{ - []byte("Dashboard 1"), - []byte("folder1"), - }, + { + Name: "folder", + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resourcepb.ResourceTableRow{ + { + Key: &resourcepb.ResourceKey{ + Name: "uid1", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder1"), }, }, }, - TotalHits: 1, - }, nil) - result, err := service.GetDashboardUIDByID(ctx, query) - require.NoError(t, err) - require.Equal(t, expectedResult, result) - k8sCliMock.AssertExpectations(t) - }) + }, + TotalHits: 1, + }, nil) + result, err := service.GetDashboardUIDByID(ctx, query) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + k8sCliMock.AssertExpectations(t) } func TestUnstructuredToLegacyDashboard(t *testing.T) { @@ -2092,11 +1712,8 @@ func TestUnstructuredToLegacyDashboard(t *testing.T) { } func TestGetDashboardTags(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), } expectedResult := []*dashboards.DashboardTagCloudItem{ @@ -2112,47 +1729,32 @@ func TestGetDashboardTags(t *testing.T) { query := &dashboards.GetDashboardTagsQuery{ OrgID: 1, } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("GetDashboardTags", mock.Anything, query).Return(expectedResult, nil).Once() - result, err := service.GetDashboardTags(context.Background(), query) - require.NoError(t, err) - require.Equal(t, expectedResult, result) - 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(&resourcepb.ResourceSearchResponse{ - Facet: map[string]*resourcepb.ResourceSearchResponse_Facet{ - "tags": { - Terms: []*resourcepb.ResourceSearchResponse_TermFacet{ - { - Term: "tag1", - Count: 1, - }, - { - Term: "tag2", - Count: 3, - }, + ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{ + Facet: map[string]*resourcepb.ResourceSearchResponse_Facet{ + "tags": { + Terms: []*resourcepb.ResourceSearchResponse_TermFacet{ + { + Term: "tag1", + Count: 1, + }, + { + Term: "tag2", + Count: 3, }, }, }, - }, nil) + }, + }, nil) - result, err := service.GetDashboardTags(ctx, query) - require.NoError(t, err) - require.Equal(t, expectedResult, result) - fakeStore.AssertExpectations(t) - }) + result, err := service.GetDashboardTags(ctx, query) + require.NoError(t, err) + require.Equal(t, expectedResult, result) } func TestQuotaCount(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), } orgs := []*org.OrgDTO{ @@ -2182,44 +1784,29 @@ func TestQuotaCount(t *testing.T) { query := "a.ScopeParameters{ OrgID: 1, } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("Count", mock.Anything, mock.Anything).Return(nil, nil).Once() - _, err := service.Count(context.Background(), query) - require.NoError(t, err) - fakeStore.AssertExpectations(t) - }) + ctx, k8sCliMock := setupK8sDashboardTests(service) + orgSvc := orgtest.FakeOrgService{ExpectedOrgs: orgs} + service.orgService = &orgSvc + k8sCliMock.On("GetStats", mock.Anything, int64(2)).Return(&countOrg2, nil).Once() + k8sCliMock.On("GetStats", mock.Anything, int64(1)).Return(&countOrg1, nil).Once() - t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { - ctx, k8sCliMock := setupK8sDashboardTests(service) - orgSvc := orgtest.FakeOrgService{ExpectedOrgs: orgs} - service.orgService = &orgSvc - k8sCliMock.On("GetStats", mock.Anything, int64(2)).Return(&countOrg2, nil).Once() - k8sCliMock.On("GetStats", mock.Anything, int64(1)).Return(&countOrg1, nil).Once() + result, err := service.Count(ctx, query) + require.NoError(t, err) - result, err := service.Count(ctx, query) - require.NoError(t, err) + orgTag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope) + require.NoError(t, err) + c, _ := result.Get(orgTag) + require.Equal(t, c, int64(1)) - orgTag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope) - require.NoError(t, err) - c, _ := result.Get(orgTag) - require.Equal(t, c, int64(1)) - - globalTag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope) - require.NoError(t, err) - c, _ = result.Get(globalTag) - require.Equal(t, c, int64(3)) - - fakeStore.AssertExpectations(t) - }) + globalTag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope) + require.NoError(t, err) + c, _ = result.Get(globalTag) + require.Equal(t, c, int64(3)) } func TestCountDashboardsInOrg(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), } count := resourcepb.ResourceStatsResponse{ Stats: []*resourcepb.ResourceStatsResponse_Stats{ @@ -2229,30 +1816,18 @@ func TestCountDashboardsInOrg(t *testing.T) { }, } - t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { - service.features = featuremgmt.WithFeatures() - fakeStore.On("CountInOrg", mock.Anything, mock.Anything, false).Return(int64(1), nil).Once() - _, err := service.CountDashboardsInOrg(context.Background(), 1) - 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("GetStats", mock.Anything, mock.Anything).Return(&count, nil).Once() - result, err := service.CountDashboardsInOrg(ctx, 1) - require.NoError(t, err) - require.Equal(t, result, int64(3)) - }) + ctx, k8sCliMock := setupK8sDashboardTests(service) + k8sCliMock.On("GetStats", mock.Anything, mock.Anything).Return(&count, nil).Once() + result, err := service.CountDashboardsInOrg(ctx, 1) + require.NoError(t, err) + require.Equal(t, result, int64(3)) } func TestCountInFolders(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - defer fakeStore.AssertExpectations(t) service := &DashboardServiceImpl{ - cfg: setting.NewCfg(), - dashboardStore: &fakeStore, + cfg: setting.NewCfg(), } + ctx, k8sCliMock := setupK8sDashboardTests(service) dashs := &resourcepb.ResourceSearchResponse{ Results: &resourcepb.ResourceTable{ Columns: []*resourcepb.ResourceTableColumnDefinition{ @@ -2291,22 +1866,11 @@ func TestCountInFolders(t *testing.T) { 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("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) - require.Equal(t, result, int64(2)) - }) + 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) + require.Equal(t, result, int64(2)) } func TestSearchDashboardsThroughK8sRaw(t *testing.T) { @@ -2654,26 +2218,18 @@ func TestIntegrationK8sDashboardCleanupJob(t *testing.T) { } tests := []struct { name string - featureEnabled bool readFromUnified bool batchSize int setupFunc func(*DashboardServiceImpl, context.Context, *client.MockK8sHandler) verifyFunc func(*testing.T, *DashboardServiceImpl, context.Context, *client.MockK8sHandler, *kvstore.FakeKVStore) }{ { - name: "Should not run cleanup when feature flag is disabled", - featureEnabled: false, - batchSize: 10, - }, - { - name: "Should not run cleanup when feature flag is enabled but we're reading from legacy", - featureEnabled: true, + name: "Should not run cleanup when we're reading from legacy", readFromUnified: false, batchSize: 10, }, { name: "Should process dashboard cleanup for all orgs", - featureEnabled: true, readFromUnified: true, batchSize: 10, setupFunc: func(service *DashboardServiceImpl, ctx context.Context, k8sCliMock *client.MockK8sHandler) { @@ -2744,7 +2300,6 @@ func TestIntegrationK8sDashboardCleanupJob(t *testing.T) { }, { name: "Should handle pagination and batching when processing large sets of dashboards", - featureEnabled: true, readFromUnified: true, batchSize: 3, setupFunc: func(service *DashboardServiceImpl, ctx context.Context, k8sCliMock *client.MockK8sHandler) { @@ -2836,11 +2391,7 @@ func TestIntegrationK8sDashboardCleanupJob(t *testing.T) { fakeStore := dashboards.FakeDashboardStore{} fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) fakeOrgService := orgtest.NewOrgServiceFake() - features := featuremgmt.WithFeatures() - if tc.featureEnabled { - features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders) - } service := &DashboardServiceImpl{ cfg: setting.NewCfg(), @@ -2885,7 +2436,7 @@ func TestIntegrationK8sDashboardCleanupJob(t *testing.T) { service := &DashboardServiceImpl{ cfg: cfg, log: log.New("test.logger"), - features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders), + features: featuremgmt.WithFeatures(), serverLockService: lockService, } @@ -2942,7 +2493,7 @@ func TestGetDashboardsByLibraryPanelUID(t *testing.T) { dashboardStore: &fakeStore, folderService: folderSvc, ac: actest.FakeAccessControl{ExpectedEvaluate: true}, - features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders, featuremgmt.FlagKubernetesLibraryPanelConnections), + features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesLibraryPanelConnections), publicDashboardService: fakePublicDashboardService, k8sclient: k8sCliMock, } diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 5b208c34b7f..9185887aec9 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -62,34 +62,6 @@ func (_m *FakeDashboardStore) Count(_a0 context.Context, _a1 *quota.ScopeParamet return r0, r1 } -// CountDashboardsInFolders provides a mock function with given fields: ctx, request -func (_m *FakeDashboardStore) CountDashboardsInFolders(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) { - ret := _m.Called(ctx, request) - - if len(ret) == 0 { - panic("no return value specified for CountDashboardsInFolders") - } - - var r0 int64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *CountDashboardsInFolderRequest) (int64, error)); ok { - return rf(ctx, request) - } - if rf, ok := ret.Get(0).(func(context.Context, *CountDashboardsInFolderRequest) int64); ok { - r0 = rf(ctx, request) - } else { - r0 = ret.Get(0).(int64) - } - - if rf, ok := ret.Get(1).(func(context.Context, *CountDashboardsInFolderRequest) error); ok { - r1 = rf(ctx, request) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // CountInOrg provides a mock function with given fields: ctx, orgID, isFolder func (_m *FakeDashboardStore) CountInOrg(ctx context.Context, orgID int64, isFolder bool) (int64, error) { ret := _m.Called(ctx, orgID, isFolder) @@ -118,23 +90,6 @@ func (_m *FakeDashboardStore) CountInOrg(ctx context.Context, orgID int64, isFol return r0, r1 } -// DeleteAllDashboards provides a mock function with given fields: ctx, orgID -func (_m *FakeDashboardStore) DeleteAllDashboards(ctx context.Context, orgID int64) error { - ret := _m.Called(ctx, orgID) - - if len(ret) == 0 { - panic("no return value specified for DeleteAllDashboards") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { - r0 = rf(ctx, orgID) - } else { - r0 = ret.Error(0) - } - - return r0 -} // DeleteDashboard provides a mock function with given fields: ctx, cmd func (_m *FakeDashboardStore) DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) error { @@ -172,23 +127,6 @@ func (_m *FakeDashboardStore) DeleteDashboardsInFolders(ctx context.Context, req return r0 } -// DeleteOrphanedProvisionedDashboards provides a mock function with given fields: ctx, cmd -func (_m *FakeDashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error { - ret := _m.Called(ctx, cmd) - - if len(ret) == 0 { - panic("no return value specified for DeleteOrphanedProvisionedDashboards") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *DeleteOrphanedProvisionedDashboardsCommand) error); ok { - r0 = rf(ctx, cmd) - } else { - r0 = ret.Error(0) - } - - return r0 -} // FindDashboards provides a mock function with given fields: ctx, query func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { @@ -310,66 +248,6 @@ func (_m *FakeDashboardStore) GetDashboardTags(ctx context.Context, query *GetDa return r0, r1 } -// GetDashboardUIDByID provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) { - ret := _m.Called(ctx, query) - - if len(ret) == 0 { - panic("no return value specified for GetDashboardUIDByID") - } - - var r0 *DashboardRef - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) (*DashboardRef, error)); ok { - return rf(ctx, query) - } - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) *DashboardRef); ok { - r0 = rf(ctx, query) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*DashboardRef) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardRefByIDQuery) error); ok { - r1 = rf(ctx, query) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetDashboards provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) { - ret := _m.Called(ctx, query) - - if len(ret) == 0 { - panic("no return value specified for GetDashboards") - } - - var r0 []*Dashboard - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) ([]*Dashboard, error)); ok { - return rf(ctx, query) - } - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) []*Dashboard); ok { - r0 = rf(ctx, query) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*Dashboard) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardsQuery) error); ok { - r1 = rf(ctx, query) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID func (_m *FakeDashboardStore) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) { ret := _m.Called(ctx, libraryPanelUID, orgID) diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index d5621692cf9..3d19209a0b0 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -4,33 +4,18 @@ import ( "context" "encoding/json" "testing" - "time" "github.com/stretchr/testify/require" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/infra/serverlock" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "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" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" dashsnapdb "github.com/grafana/grafana/pkg/services/dashboardsnapshots/database" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder/folderimpl" - "github.com/grafana/grafana/pkg/services/folder/foldertest" - "github.com/grafana/grafana/pkg/services/quota/quotatest" - "github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/secrets/database" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" - "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -81,7 +66,6 @@ func TestIntegrationDashboardSnapshotsService(t *testing.T) { require.Equal(t, rawDashboard, decrypted) }) - t.Run("get dashboard snapshot should return the dashboard decrypted", func(t *testing.T) { ctx := context.Background() @@ -99,69 +83,3 @@ func TestIntegrationDashboardSnapshotsService(t *testing.T) { require.Equal(t, rawDashboard, decrypted) }) } - -func TestIntegrationValidateDashboardExists(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode") - } - sqlStore := db.InitTestDB(t) - cfg := setting.NewCfg() - dsStore := dashsnapdb.ProvideStore(sqlStore, cfg) - secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) - feats := featuremgmt.WithFeatures() - dashboardStore, err := dashdb.ProvideDashboardStore(sqlStore, cfg, feats, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - dashSvc, err := dashsvc.ProvideDashboardServiceImpl( - cfg, - dashboardStore, - folderimpl.ProvideDashboardFolderStore(sqlStore), - feats, - nil, - actest.FakeAccessControl{}, - actest.FakeService{}, - foldertest.NewFakeService(), - nil, - client.MockTestRestConfig{}, - nil, - quotatest.New(false, nil), - nil, - nil, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore(), - ) - require.NoError(t, err) - s := ProvideService(dsStore, secretsService, dashSvc) - ctx := context.Background() - - t.Run("returns false when dashboard does not exist", func(t *testing.T) { - err := s.ValidateDashboardExists(ctx, 1, "test") - require.Error(t, err) - require.Equal(t, dashboards.ErrDashboardNotFound, err) - }) - - t.Run("returns true when dashboard exists", func(t *testing.T) { - err := createDashboard(sqlStore) - require.NoError(t, err) - - err = s.ValidateDashboardExists(ctx, 1, "test") - require.NoError(t, err) - }) -} - -func createDashboard(store db.DB) error { - return store.WithDbSession(context.Background(), func(sess *db.Session) error { - dashboard := &dashboards.Dashboard{ - ID: 1, - UID: "test", - OrgID: 1, - Created: time.Now(), - Updated: time.Now(), - } - - _, err := sess.Insert(dashboard) - return err - }) -} diff --git a/pkg/services/dashboardsnapshots/service_test.go b/pkg/services/dashboardsnapshots/service_test.go new file mode 100644 index 00000000000..94657daa597 --- /dev/null +++ b/pkg/services/dashboardsnapshots/service_test.go @@ -0,0 +1,75 @@ +package dashboardsnapshots + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" + "github.com/grafana/grafana/pkg/infra/log" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/web" +) + +func TestCreateDashboardSnapshot_DashboardNotFound(t *testing.T) { + mockService := &MockService{} + cfg := dashboardsnapshot.SnapshotSharingOptions{ + SnapshotsEnabled: true, + ExternalEnabled: false, + } + testUser := &user.SignedInUser{ + UserID: 1, + OrgID: 1, + Login: "testuser", + Name: "Test User", + Email: "test@example.com", + } + dashboard := &common.Unstructured{} + dashboardData := map[string]interface{}{ + "uid": "test-dashboard-uid", + "id": 123, + } + dashboardBytes, _ := json.Marshal(dashboardData) + _ = json.Unmarshal(dashboardBytes, dashboard) + + cmd := CreateDashboardSnapshotCommand{ + DashboardCreateCommand: dashboardsnapshot.DashboardCreateCommand{ + Dashboard: dashboard, + Name: "Test Snapshot", + }, + } + + mockService.On("ValidateDashboardExists", mock.Anything, int64(1), "test-dashboard-uid"). + Return(dashboards.ErrDashboardNotFound) + + req, _ := http.NewRequest("POST", "/api/snapshots", nil) + req = req.WithContext(identity.WithRequester(req.Context(), testUser)) + + recorder := httptest.NewRecorder() + ctx := &contextmodel.ReqContext{ + Context: &web.Context{ + Req: req, + Resp: web.NewResponseWriter("POST", recorder), + }, + SignedInUser: testUser, + Logger: log.NewNopLogger(), + } + + CreateDashboardSnapshot(ctx, cfg, cmd, mockService) + + mockService.AssertExpectations(t) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + var response map[string]interface{} + err := json.Unmarshal(recorder.Body.Bytes(), &response) + require.NoError(t, err) + assert.Equal(t, "Dashboard not found", response["message"]) +} diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 9b16131262e..b2063f44091 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -89,20 +89,11 @@ func (s *Service) Get(ctx context.Context, query *dashver.GetDashboardVersionQue query.DashboardID = id } - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - 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) + version, err := s.getHistoryThroughK8s(ctx, query.OrgID, query.DashboardUID, query.Version) if err != nil { return nil, err } - version.Data.Set("id", version.DashboardID) - return version.ToDTO(query.DashboardUID), nil + return version, nil } func (s *Service) DeleteExpired(ctx context.Context, cmd *dashver.DeleteExpiredVersionsCommand) error { @@ -160,31 +151,17 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions query.Limit = 1000 } - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - 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) + versions, err := s.listHistoryThroughK8s( + ctx, + query.OrgID, + query.DashboardUID, + int64(query.Limit), + query.ContinueToken, + ) if err != nil { return nil, err } - dtos := make([]*dashver.DashboardVersionDTO, len(dvs)) - for i, v := range dvs { - dtos[i] = v.ToDTO(query.DashboardUID) - } - return &dashver.DashboardVersionResponse{ - Versions: dtos, - }, nil + return versions, nil } // getDashUIDMaybeEmpty is a helper function which takes a dashboardID and diff --git a/pkg/services/dashboardversion/dashverimpl/dashver_test.go b/pkg/services/dashboardversion/dashverimpl/dashver_test.go index d63a5199cb2..1b32379e1a5 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver_test.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver_test.go @@ -15,7 +15,6 @@ import ( "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" @@ -25,29 +24,12 @@ import ( ) func TestDashboardVersionService(t *testing.T) { - dashboardVersionStore := newDashboardVersionStoreFake() - dashboardService := dashboards.NewFakeDashboardService(t) - dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, features: featuremgmt.WithFeatures()} - - t.Run("Get dashboard version", func(t *testing.T) { - dashboard := &dashver.DashboardVersion{ - ID: 11, - Data: &simplejson.Json{}, - } - dashboardVersionStore.ExpectedDashboardVersion = dashboard - dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(&dashboards.Dashboard{ID: 42}, nil) - dashboardVersion, err := dashboardVersionService.Get(context.Background(), &dashver.GetDashboardVersionQuery{}) - require.NoError(t, err) - require.Equal(t, dashboard.ToDTO("uid"), dashboardVersion) - }) - - t.Run("Get dashboard versions through k8s", func(t *testing.T) { + t.Run("Get dashboard versions", 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.FlagKubernetesClientDashboardsFolders) + dashboardVersionService.features = featuremgmt.WithFeatures() dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil) creationTimestamp := time.Now().Add(time.Hour * -24).UTC() @@ -133,7 +115,7 @@ func TestDashboardVersionService(t *testing.T) { dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()} mockCli := new(client.MockK8sHandler) dashboardVersionService.k8sclient = mockCli - dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders) + dashboardVersionService.features = featuremgmt.WithFeatures() dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil) mockCli.On("List", mock.Anything, int64(1), mock.Anything).Return(nil, apierrors.NewNotFound(schema.GroupResource{Group: "dashboards.dashboard.grafana.app", Resource: "dashboard"}, "uid")) @@ -176,93 +158,12 @@ func TestDeleteExpiredVersions(t *testing.T) { } 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, features: featuremgmt.WithFeatures()} - dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{ - {ID: 1, DashboardID: 42}, - } - dashboardService.On("GetDashboardUIDByID", mock.Anything, - mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")). - Return(&dashboards.DashboardRef{UID: "uid"}, nil) - - query := dashver.ListDashboardVersionsQuery{DashboardID: 42} - res, err := dashboardVersionService.List(context.Background(), &query) - require.Nil(t, err) - require.Equal(t, 1, len(res.Versions)) - // validate that the UID was populated - 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(), features: featuremgmt.WithFeatures()} - dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{ - {ID: 1, DashboardID: 42}, - } - dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")). - Return(nil, dashboards.ErrDashboardNotFound).Once() - - query := dashver.ListDashboardVersionsQuery{DashboardID: 42} - res, err := dashboardVersionService.List(context.Background(), &query) - require.Nil(t, err) - 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.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(), 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) - - query := dashver.ListDashboardVersionsQuery{DashboardUID: "uid"} - res, err := dashboardVersionService.List(context.Background(), &query) - require.Nil(t, err) - require.Equal(t, 1, len(res.Versions)) - // validate that the dashboardID was populated from the GetDashboard method call. - 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(), features: featuremgmt.WithFeatures()} - dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{{DashboardID: 42, ID: 1}} - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")). - Return(nil, dashboards.ErrDashboardNotFound) - - query := dashver.ListDashboardVersionsQuery{DashboardUID: "uid"} - res, err := dashboardVersionService.List(context.Background(), &query) - require.Nil(t, err) - 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.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(), features: featuremgmt.WithFeatures()} - dashboardVersionStore.ExpectedError = dashver.ErrDashboardVersionNotFound - - query := dashver.ListDashboardVersionsQuery{DashboardID: 42, DashboardUID: "42"} - res, err := dashboardVersionService.List(context.Background(), &query) - 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.FlagKubernetesClientDashboardsFolders) + dashboardVersionService.features = featuremgmt.WithFeatures() dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")). @@ -301,7 +202,7 @@ func TestListDashboardVersions(t *testing.T) { dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()} mockCli := new(client.MockK8sHandler) dashboardVersionService.k8sclient = mockCli - dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders) + dashboardVersionService.features = featuremgmt.WithFeatures() dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")). Return(&dashboards.DashboardRef{UID: "uid"}, nil) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index bef148ef73c..94caceac460 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -483,13 +483,6 @@ var ( Owner: grafanaAppPlatformSquad, FrontendOnly: true, }, - { - Name: "kubernetesClientDashboardsFolders", - Description: "Route the folder and dashboard service requests to k8s", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAppPlatformSquad, - Expression: "true", // enabled by default - }, { Name: "dashboardDisableSchemaValidationV1", Description: "Disable schema validation for dashboards/v1", diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv index 01a52394b2a..4a0ff55bc6e 100644 --- a/pkg/services/featuremgmt/toggles-gitlog.csv +++ b/pkg/services/featuremgmt/toggles-gitlog.csv @@ -440,7 +440,6 @@ alertingConversionAPI,2025-02-12T07:13:21Z,2025-04-05T08:27:02Z,9593e51da7c05ed5 alertingJiraIntegration,2025-02-14T12:22:04Z,,af8cab92109ecfbb54625ff25a3b90a4f92dc46a,Sonia Aguilar alertingRuleVersionHistoryRestore,2025-02-17T12:25:32Z,,2014d27defe5668ba07913cec6f2186c90eaaab2,Sonia Aguilar newShareReportDrawer,2025-02-17T19:05:46Z,,9df6412e92559f2329c2baac0025f56fc57c5bf9,Ezequiel Victorero -kubernetesClientDashboardsFolders,2025-02-18T23:11:26Z,,3e6f40c87386984be60e391b26a7559e3469dac3,Stephanie Hingtgen managedDualWriter,2025-02-19T14:50:39Z,,5a40c84568485da55ccb42998c6d291d310abd56,Ryan McKinley rendererDisableAppPluginsPreload,2025-02-24T14:43:06Z,,608d974585c696253ac629f3c7bfc3a0043cbd49,Agnès Toulet assetSriChecks,2025-03-04T10:56:35Z,,bbfeb8d220cc67c329aa2b5d6ed693ae1cb54325,Jack Westbrook diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 720d9a00434..24de39bdf99 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -62,7 +62,6 @@ kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true, kubernetesLibraryPanels,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesLibraryPanelConnections,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true -kubernetesClientDashboardsFolders,GA,@grafana/grafana-app-platform-squad,false,false,false dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false dashboardDisableSchemaValidationV2,experimental,@grafana/grafana-app-platform-squad,false,false,false dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 6f82a45302d..a5ae09b01b3 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -259,10 +259,6 @@ const ( // Use the kubernetes API in the frontend for dashboards FlagKubernetesDashboards = "kubernetesDashboards" - // FlagKubernetesClientDashboardsFolders - // Route the folder and dashboard service requests to k8s - FlagKubernetesClientDashboardsFolders = "kubernetesClientDashboardsFolders" - // FlagDashboardDisableSchemaValidationV1 // Disable schema validation for dashboards/v1 FlagDashboardDisableSchemaValidationV1 = "dashboardDisableSchemaValidationV1" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index ac1e306cd71..6d6ecd4880c 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1729,19 +1729,6 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "kubernetesClientDashboardsFolders", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-02-18T23:11:26Z" - }, - "spec": { - "description": "Route the folder and dashboard service requests to k8s", - "stage": "GA", - "codeowner": "@grafana/grafana-app-platform-squad", - "expression": "true" - } - }, { "metadata": { "name": "kubernetesDashboards", diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 277ee43d442..0bae0f65d6b 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -112,39 +112,35 @@ func ProvideService( ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, srv)) ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(srv)) - if features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - k8sHandler := client.NewK8sHandler( - dual, - request.GetNamespaceMapper(cfg), - folderv1.FolderResourceInfo.GroupVersionResource(), - restConfig.GetRestConfig, - dashboardStore, - userService, - resourceClient, - sorter, - features, - ) + k8sHandler := client.NewK8sHandler( + dual, + request.GetNamespaceMapper(cfg), + folderv1.FolderResourceInfo.GroupVersionResource(), + restConfig.GetRestConfig, + dashboardStore, + userService, + resourceClient, + sorter, + features, + ) - unifiedStore := ProvideUnifiedStore(k8sHandler, userService, tracer) + unifiedStore := ProvideUnifiedStore(k8sHandler, userService, tracer) - srv.unifiedStore = unifiedStore - srv.k8sclient = k8sHandler - } + srv.unifiedStore = unifiedStore + srv.k8sclient = k8sHandler - if features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - dashHandler := client.NewK8sHandler( - dual, - request.GetNamespaceMapper(cfg), - dashboardv1.DashboardResourceInfo.GroupVersionResource(), - restConfig.GetRestConfig, - dashboardStore, - userService, - resourceClient, - sorter, - features, - ) - srv.dashboardK8sClient = dashHandler - } + dashHandler := client.NewK8sHandler( + dual, + request.GetNamespaceMapper(cfg), + dashboardv1.DashboardResourceInfo.GroupVersionResource(), + restConfig.GetRestConfig, + dashboardStore, + userService, + resourceClient, + sorter, + features, + ) + srv.dashboardK8sClient = dashHandler return srv } @@ -201,32 +197,22 @@ func (s *Service) DBMigration(db db.DB) { func (s *Service) CountFoldersInOrg(ctx context.Context, orgID int64) (int64, error) { ctx, span := s.tracer.Start(ctx, "folder.CountFoldersInOrg") defer span.End() - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return s.unifiedStore.CountInOrg(ctx, orgID) - } - - return s.store.CountInOrg(ctx, orgID) + return s.unifiedStore.CountInOrg(ctx, orgID) } func (s *Service) SearchFolders(ctx context.Context, q folder.SearchFoldersQuery) (model.HitList, error) { ctx, span := s.tracer.Start(ctx, "folder.SearchFolders") defer span.End() - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - // 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") + // 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) } func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) { ctx, span := s.tracer.Start(ctx, "folder.GetFolders") defer span.End() - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return s.getFoldersFromApiServer(ctx, q) - } - return s.GetFoldersLegacy(ctx, q) + return s.getFoldersFromApiServer(ctx, q) } func (s *Service) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) { @@ -286,10 +272,7 @@ func (s *Service) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) { ctx, span := s.tracer.Start(ctx, "folder.Get") defer span.End() - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return s.getFromApiServer(ctx, q) - } - return s.GetLegacy(ctx, q) + return s.getFromApiServer(ctx, q) } func (s *Service) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) { @@ -377,10 +360,7 @@ func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, forceLegacy func (s *Service) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) { ctx, span := s.tracer.Start(ctx, "folder.GetChildren") defer span.End() - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return s.getChildrenFromApiServer(ctx, q) - } - return s.GetChildrenLegacy(ctx, q) + return s.getChildrenFromApiServer(ctx, q) } func (s *Service) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) { @@ -660,10 +640,7 @@ func (s *Service) deduplicateAvailableFolders(ctx context.Context, folders []*fo func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) { ctx, span := s.tracer.Start(ctx, "folder.GetParents") defer span.End() - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return s.getParentsFromApiServer(ctx, q) - } - return s.GetParentsLegacy(ctx, q) + return s.getParentsFromApiServer(ctx, q) } func (s *Service) GetParentsLegacy(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) { @@ -679,10 +656,7 @@ func (s *Service) GetParentsLegacy(ctx context.Context, q folder.GetParentsQuery } func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) { - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return s.createOnApiServer(ctx, cmd) - } - return s.CreateLegacy(ctx, cmd) + return s.createOnApiServer(ctx, cmd) } func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) { @@ -800,10 +774,7 @@ func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderComm func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) { ctx, span := s.tracer.Start(ctx, "folder.Update") defer span.End() - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return s.updateOnApiServer(ctx, cmd) - } - return s.UpdateLegacy(ctx, cmd) + return s.updateOnApiServer(ctx, cmd) } func (s *Service) UpdateLegacy(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) { @@ -943,10 +914,7 @@ func prepareForUpdate(dashFolder *dashboards.Dashboard, orgId int64, userId int6 func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) error { ctx, span := s.tracer.Start(ctx, "folder.Delete") defer span.End() - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return s.deleteFromApiServer(ctx, cmd) - } - return s.DeleteLegacy(ctx, cmd) + return s.deleteFromApiServer(ctx, cmd) } func (s *Service) DeleteLegacy(ctx context.Context, cmd *folder.DeleteFolderCommand) error { @@ -1067,10 +1035,7 @@ func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderComm func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) { ctx, span := s.tracer.Start(ctx, "folder.Move") defer span.End() - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return s.moveOnApiServer(ctx, cmd) - } - return s.MoveLegacy(ctx, cmd) + return s.moveOnApiServer(ctx, cmd) } func (s *Service) MoveLegacy(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) { @@ -1296,11 +1261,7 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold func (s *Service) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) { ctx, span := s.tracer.Start(ctx, "folder.GetDescendantCounts") defer span.End() - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - return s.getDescendantCountsFromApiServer(ctx, q) - } - - return s.GetDescendantCountsLegacy(ctx, q) + return s.getDescendantCountsFromApiServer(ctx, q) } func (s *Service) GetDescendantCountsLegacy(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) { diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 609ede53d19..feba5cbeed0 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -193,9 +193,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { ExpectedUser: &user.User{}, } - featuresArr := []any{ - featuremgmt.FlagKubernetesClientDashboardsFolders} - features := featuremgmt.WithFeatures(featuresArr...) + features := featuremgmt.WithFeatures() tracer := noop.NewTracerProvider().Tracer("TestIntegrationFolderServiceViaUnifiedStorage") dashboardStore := dashboards.NewFakeDashboardStore(t) @@ -521,7 +519,7 @@ func TestSearchFoldersFromApiServer(t *testing.T) { tracer := noop.NewTracerProvider().Tracer("TestSearchFoldersFromApiServer") service := Service{ k8sclient: fakeK8sClient, - features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders), + features: featuremgmt.WithFeatures(), unifiedStore: folderStore, tracer: tracer, accessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, @@ -764,7 +762,7 @@ func TestGetFoldersFromApiServer(t *testing.T) { tracer := noop.NewTracerProvider().Tracer("TestGetFoldersFromApiServer") service := Service{ k8sclient: fakeK8sClient, - features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders), + features: featuremgmt.WithFeatures(), unifiedStore: folderStore, accessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, tracer: tracer, @@ -866,7 +864,7 @@ func TestIntegrationDeleteFoldersFromApiServer(t *testing.T) { publicDashboardService: publicDashboardFakeService, accessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, registry: make(map[string]folder.RegistryService), - features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders), + features: featuremgmt.WithFeatures(), tracer: tracer, } user := &user.SignedInUser{OrgID: 1} diff --git a/pkg/services/folder/foldertest/foldertest.go b/pkg/services/folder/foldertest/foldertest.go index 45621c6d9a6..35e55b26d0c 100644 --- a/pkg/services/folder/foldertest/foldertest.go +++ b/pkg/services/folder/foldertest/foldertest.go @@ -15,16 +15,49 @@ type FakeService struct { ExpectedError error ExpectedDescendantCounts map[string]int64 LastQuery folder.GetFoldersQuery + foldersByUID map[string]*folder.Folder } func NewFakeService() *FakeService { - return &FakeService{} + return &FakeService{ + foldersByUID: make(map[string]*folder.Folder), + } +} + +func (s *FakeService) AddFolder(f *folder.Folder) { + if s.foldersByUID == nil { + s.foldersByUID = make(map[string]*folder.Folder) + } + s.foldersByUID[f.UID] = f + s.ExpectedFolders = append(s.ExpectedFolders, f) +} + +func (s *FakeService) SetFolders(folders map[string]*folder.Folder) { + s.foldersByUID = folders + s.ExpectedFolders = make([]*folder.Folder, 0, len(folders)) + for _, f := range folders { + s.ExpectedFolders = append(s.ExpectedFolders, f) + } } var _ folder.Service = (*FakeService)(nil) func (s *FakeService) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) { - return s.ExpectedFoldersRef, s.ExpectedError + if s.ExpectedError != nil { + return nil, s.ExpectedError + } + + if s.ExpectedFoldersRef != nil { + return s.ExpectedFoldersRef, nil + } + + var result []*folder.FolderReference + for _, f := range s.ExpectedFolders { + if f.OrgID == q.OrgID && f.ParentUID == q.UID { + result = append(result, f.ToFolderReference()) + } + } + return result, nil } func (s *FakeService) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) { return s.ExpectedFoldersRef, s.ExpectedError @@ -45,6 +78,11 @@ func (s *FakeService) CreateLegacy(ctx context.Context, cmd *folder.CreateFolder } func (s *FakeService) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) { + if q.UID != nil && s.foldersByUID != nil { + if f, exists := s.foldersByUID[*q.UID]; exists { + return f, nil + } + } return s.ExpectedFolder, s.ExpectedError } func (s *FakeService) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) { @@ -84,7 +122,23 @@ 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 + if s.foldersByUID != nil && len(q.UIDs) > 0 { + var result []*folder.Folder + for _, uid := range q.UIDs { + if f, exists := s.foldersByUID[uid]; exists { + result = append(result, f) + } + } + return result, nil + } + + folders := make([]*folder.Folder, 0, len(s.ExpectedFolders)) + for _, f := range s.ExpectedFolders { + if f.OrgID == q.OrgID { + folders = append(folders, f) + } + } + return folders, s.ExpectedError } func (s *FakeService) SearchFolders(ctx context.Context, q folder.SearchFoldersQuery) (model.HitList, error) { diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 96c5f82cd22..82531e39077 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -893,39 +893,20 @@ func getFoldersWithMatchingTitles(c context.Context, l *LibraryElementService, s return nil, nil } - if l.features.IsEnabled(c, featuremgmt.FlagKubernetesClientDashboardsFolders) { - searchQuery := folder.SearchFoldersQuery{ - OrgID: signedInUser.GetOrgID(), - Title: query.SearchString, - SignedInUser: signedInUser, - } - - folderHits, err := l.folderService.SearchFolders(c, searchQuery) - if err != nil { - return nil, err - } - - foldersWithMatchingTitles := make([]string, 0, len(folderHits)) - for _, hit := range folderHits { - foldersWithMatchingTitles = append(foldersWithMatchingTitles, hit.UID) - } - return foldersWithMatchingTitles, nil + searchQuery := folder.SearchFoldersQuery{ + OrgID: signedInUser.GetOrgID(), + Title: query.SearchString, + SignedInUser: signedInUser, } - // Fallback to GetFolders - fs, err := l.folderService.GetFolders(c, folder.GetFoldersQuery{ - OrgID: signedInUser.GetOrgID(), - SignedInUser: signedInUser, - }) + folderHits, err := l.folderService.SearchFolders(c, searchQuery) if err != nil { return nil, err } - foldersWithMatchingTitles := make([]string, 0, len(fs)) - for _, f := range fs { - if strings.Contains(strings.ToLower(f.Title), strings.ToLower(query.SearchString)) { - foldersWithMatchingTitles = append(foldersWithMatchingTitles, f.UID) - } + foldersWithMatchingTitles := make([]string, 0, len(folderHits)) + for _, hit := range folderHits { + foldersWithMatchingTitles = append(foldersWithMatchingTitles, hit.UID) } return foldersWithMatchingTitles, nil } diff --git a/pkg/services/libraryelements/libraryelements_create_test.go b/pkg/services/libraryelements/libraryelements_create_test.go index 53f1ed38e68..7567a658b85 100644 --- a/pkg/services/libraryelements/libraryelements_create_test.go +++ b/pkg/services/libraryelements/libraryelements_create_test.go @@ -46,8 +46,8 @@ func TestIntegration_CreateLibraryElement(t *testing.T) { }, Version: 1, Meta: model.LibraryElementDTOMeta{ - FolderName: "ScenarioFolder", - FolderUID: "uid_for_ScenarioFolder", + FolderName: sc.folder.Title, + FolderUID: sc.folder.UID, ConnectedDashboards: 0, Created: sc.initialResult.Result.Meta.Created, Updated: sc.initialResult.Result.Meta.Updated, @@ -97,8 +97,8 @@ func TestIntegration_CreateLibraryElement(t *testing.T) { }, Version: 1, Meta: model.LibraryElementDTOMeta{ - FolderName: "ScenarioFolder", - FolderUID: "uid_for_ScenarioFolder", + FolderName: sc.folder.Title, + FolderUID: sc.folder.UID, ConnectedDashboards: 0, Created: result.Result.Meta.Created, Updated: result.Result.Meta.Updated, @@ -176,8 +176,8 @@ func TestIntegration_CreateLibraryElement(t *testing.T) { }, Version: 1, Meta: model.LibraryElementDTOMeta{ - FolderName: "ScenarioFolder", - FolderUID: "uid_for_ScenarioFolder", + FolderName: sc.folder.Title, + FolderUID: sc.folder.UID, ConnectedDashboards: 0, Created: result.Result.Meta.Created, Updated: result.Result.Meta.Updated, diff --git a/pkg/services/libraryelements/libraryelements_delete_test.go b/pkg/services/libraryelements/libraryelements_delete_test.go index 86d7a886015..44d142536f6 100644 --- a/pkg/services/libraryelements/libraryelements_delete_test.go +++ b/pkg/services/libraryelements/libraryelements_delete_test.go @@ -4,9 +4,9 @@ import ( "encoding/json" "testing" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" @@ -25,6 +25,7 @@ func TestIntegration_DeleteLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to delete a library panel that exists, it should succeed and return correct ID", func(t *testing.T, sc scenarioContext) { + sc.dashboardSvc.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil) 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()) @@ -47,39 +48,14 @@ func TestIntegration_DeleteLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to delete a library panel that is connected, it should fail", func(t *testing.T, sc scenarioContext) { - dashJSON := map[string]any{ - "panels": []any{ - map[string]any{ - "id": int64(1), - "gridPos": map[string]any{ - "h": 6, - "w": 6, - "x": 0, - "y": 0, - }, - }, - map[string]any{ - "id": int64(2), - "gridPos": map[string]any{ - "h": 6, - "w": 6, - "x": 6, - "y": 0, - }, - "libraryPanel": map[string]any{ - "uid": sc.initialResult.Result.UID, - "name": sc.initialResult.Result.Name, - }, - }, + sc.dashboardSvc.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{ + { + ID: 1, + UID: "test", }, - } - dash := dashboards.Dashboard{ - Title: "Testing deleteHandler ", - Data: simplejson.NewFromAny(dashJSON), - } + }, nil) // 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) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1) require.NoError(t, err) sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) @@ -89,6 +65,7 @@ func TestIntegration_DeleteLibraryElement(t *testing.T) { 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) { + sc.dashboardSvc.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil) err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 9999999) require.NoError(t, err) diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index b3a4acb6cd3..5e1bd884258 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -8,8 +8,10 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/kinds/librarypanel" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" + searchmodel "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/search/sort" ) @@ -406,7 +408,14 @@ func TestIntegration_GetAllLibraryElements(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", sc.folderSvc) + newFolder := &folder.Folder{ + ID: 2, + OrgID: 1, + UID: "uid_for_NewFolder", + Title: "NewFolder", + } + sc.folderSvc.ExpectedFolder = newFolder + sc.folderSvc.ExpectedFolders = []*folder.Folder{newFolder} // nolint:staticcheck command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) @@ -448,7 +457,7 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { }, Version: 1, Meta: model.LibraryElementDTOMeta{ - FolderName: "NewFolder", + FolderName: newFolder.Title, FolderUID: newFolder.UID, ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, @@ -1171,6 +1180,14 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { // Folder name search integration tests scenarioWithPanel(t, "When searching by folder name, it should return panels in that folder", func(t *testing.T, sc scenarioContext) { + sc.folderSvc.ExpectedHitList = searchmodel.HitList{ + { + UID: sc.folder.UID, + Title: sc.folder.Title, + Type: searchmodel.DashHitFolder, + }, + } + // Create a panel in the existing folder // nolint:staticcheck command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Panel in ScenarioFolder") @@ -1275,6 +1292,14 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When searching by partial folder name, it should return panels in matching folders", func(t *testing.T, sc scenarioContext) { + sc.folderSvc.ExpectedHitList = searchmodel.HitList{ + { + UID: sc.folder.UID, + Title: sc.folder.Title, + Type: searchmodel.DashHitFolder, + }, + } + // Create a panel in the existing folder // nolint:staticcheck command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Test Panel") diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index b05c269ee63..8baedc13c5a 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -7,7 +7,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" - "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" @@ -57,7 +56,7 @@ func TestIntegration_GetLibraryElement(t *testing.T) { }, Version: 1, Meta: model.LibraryElementDTOMeta{ - FolderName: "ScenarioFolder", + FolderName: sc.folder.Title, FolderUID: sc.folder.UID, ConnectedDashboards: 0, Created: res.Result.Meta.Created, @@ -120,6 +119,8 @@ func TestIntegration_GetLibraryElement(t *testing.T) { SignedInUser: sc.reqContext.SignedInUser, }) require.NoError(t, err) + sc.folderSvc.ExpectedFolder = nil + sc.folderSvc.ExpectedError = folder.ErrFolderNotFound 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) @@ -131,39 +132,7 @@ func TestIntegration_GetLibraryElement(t *testing.T) { 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{ - "panels": []any{ - map[string]any{ - "id": int64(1), - "gridPos": map[string]any{ - "h": 6, - "w": 6, - "x": 0, - "y": 0, - }, - }, - map[string]any{ - "id": int64(2), - "gridPos": map[string]any{ - "h": 6, - "w": 6, - "x": 6, - "y": 0, - }, - "libraryPanel": map[string]any{ - "uid": sc.initialResult.Result.UID, - "name": sc.initialResult.Result.Name, - }, - }, - }, - } - dash := dashboards.Dashboard{ - Title: "Testing getHandler", - Data: simplejson.NewFromAny(dashJSON), - } - // 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) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1) require.NoError(t, err) expected := func(res libraryElementResult) libraryElementResult { @@ -187,7 +156,7 @@ func TestIntegration_GetLibraryElement(t *testing.T) { }, Version: 1, Meta: model.LibraryElementDTOMeta{ - FolderName: "ScenarioFolder", + FolderName: sc.folder.Title, FolderUID: sc.folder.UID, ConnectedDashboards: 1, Created: res.Result.Meta.Created, diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index 1dc29d065ee..9d83f707aff 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/kinds/librarypanel" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/util" @@ -28,7 +29,14 @@ func TestIntegration_PatchLibraryElement(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", sc.folderSvc) + newFolder := &folder.Folder{ + ID: 2, + OrgID: 1, + UID: "uid_for_NewFolder", + Title: "NewFolder", + } + sc.folderSvc.ExpectedFolder = newFolder + sc.folderSvc.ExpectedFolders = []*folder.Folder{newFolder} cmd := model.PatchLibraryElementCommand{ FolderID: newFolder.ID, // nolint:staticcheck FolderUID: &newFolder.UID, @@ -95,7 +103,14 @@ func TestIntegration_PatchLibraryElement(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", sc.folderSvc) + newFolder := &folder.Folder{ + ID: 2, + OrgID: 1, + UID: "uid_for_NewFolder", + Title: "NewFolder", + } + sc.folderSvc.ExpectedFolder = newFolder + sc.folderSvc.ExpectedFolders = []*folder.Folder{newFolder} cmd := model.PatchLibraryElementCommand{ FolderID: newFolder.ID, // nolint:staticcheck FolderUID: &newFolder.UID, @@ -340,26 +355,35 @@ func TestIntegration_PatchLibraryElement(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", sc.folderSvc) + newFolder := &folder.Folder{ + ID: 2, + OrgID: 1, + UID: "uid_for_NewFolder", + Title: "NewFolder", + } + sc.folderSvc.ExpectedFolder = newFolder + sc.folderSvc.ExpectedFolders = []*folder.Folder{newFolder} + // nolint:staticcheck command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel") sc.ctx.Req.Body = mockRequestBody(command) - resp := sc.service.createHandler(sc.reqContext) - var result = validateAndUnMarshalResponse(t, resp) + sc.service.createHandler(sc.reqContext) cmd := model.PatchLibraryElementCommand{ - FolderID: 1, // nolint:staticcheck - FolderUID: &sc.folder.UID, + FolderID: newFolder.ID, // nolint:staticcheck + FolderUID: &newFolder.UID, Version: 1, Kind: int64(model.PanelElement), } - sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) + sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) - resp = sc.service.patchHandler(sc.reqContext) + resp := sc.service.patchHandler(sc.reqContext) require.Equal(t, 400, resp.Status()) }) scenarioWithPanel(t, "When an admin tries to patch a library panel in another org, it should fail", func(t *testing.T, sc scenarioContext) { + sc.folderSvc.ExpectedFolder = nil + sc.folderSvc.ExpectedError = folder.ErrFolderNotFound cmd := model.PatchLibraryElementCommand{ FolderID: sc.folder.ID, // nolint:staticcheck FolderUID: &sc.folder.UID, diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index def4ff5660a..d4a88a34395 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -16,26 +16,17 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/identity" "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/kvstore" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/services/accesscontrol" "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" - "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" - dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" "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/libraryelements/model" ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" @@ -43,13 +34,9 @@ import ( "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/search/sort" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" - "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/setting" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/web" ) @@ -67,39 +54,7 @@ func TestIntegration_DeleteLibraryPanelsInFolder(t *testing.T) { } scenarioWithPanel(t, "When an admin tries to delete a folder that contains connected library elements, it should fail", func(t *testing.T, sc scenarioContext) { - dashJSON := map[string]any{ - "panels": []any{ - map[string]any{ - "id": int64(1), - "gridPos": map[string]any{ - "h": 6, - "w": 6, - "x": 0, - "y": 0, - }, - }, - map[string]any{ - "id": int64(2), - "gridPos": map[string]any{ - "h": 6, - "w": 6, - "x": 6, - "y": 0, - }, - "libraryPanel": map[string]any{ - "uid": sc.initialResult.Result.UID, - "name": sc.initialResult.Result.Name, - }, - }, - }, - } - dash := dashboards.Dashboard{ - Title: "Testing DeleteLibraryElementsInFolder", - Data: simplejson.NewFromAny(dashJSON), - } - // 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) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1) require.NoError(t, err) err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.UID) @@ -148,39 +103,7 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) { } scenarioWithPanel(t, "When an admin tries to get connections of library panel, it should succeed and return correct result", func(t *testing.T, sc scenarioContext) { - dashJSON := map[string]any{ - "panels": []any{ - map[string]any{ - "id": int64(1), - "gridPos": map[string]any{ - "h": 6, - "w": 6, - "x": 0, - "y": 0, - }, - }, - map[string]any{ - "id": int64(2), - "gridPos": map[string]any{ - "h": 6, - "w": 6, - "x": 6, - "y": 0, - }, - "libraryPanel": map[string]any{ - "uid": sc.initialResult.Result.UID, - "name": sc.initialResult.Result.Name, - }, - }, - }, - } - dash := dashboards.Dashboard{ - Title: "Testing GetLibraryPanelConnections", - Data: simplejson.NewFromAny(dashJSON), - } - // 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) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1) require.NoError(t, err) // add a connection where the dashboard doesn't exist. Shouldn't be returned in the list @@ -191,12 +114,11 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) { return model.LibraryElementConnectionsResponse{ Result: []model.LibraryElementConnectionDTO{ { - ID: sc.initialResult.Result.ID, - Kind: sc.initialResult.Result.Kind, - ElementID: 1, - ConnectionID: dashInDB.ID, - ConnectionUID: dashInDB.UID, - Created: res.Result[0].Created, + ID: sc.initialResult.Result.ID, + Kind: sc.initialResult.Result.Kind, + ElementID: 1, + ConnectionID: 1, + Created: res.Result[0].Created, CreatedBy: librarypanel.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, @@ -207,6 +129,13 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) { } } + sc.dashboardSvc.On("GetDashboardsByLibraryPanelUID", mock.Anything, mock.Anything, mock.Anything).Return([]*dashboards.DashboardRef{ + { + ID: 1, + UID: "", + }, + }, nil) + sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) resp := sc.service.getConnectionsHandler(sc.reqContext) var result = validateAndUnMarshalConnectionResponse(t, resp) @@ -239,13 +168,7 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) { }) 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) + err = sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1) require.NoError(t, err) }) } @@ -319,77 +242,26 @@ type scenarioContext struct { initialResult libraryElementResult sqlStore db.DB log log.Logger - folderSvc folder.Service + folderSvc *foldertest.FakeService + dashboardSvc *dashboards.FakeDashboardService } -func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *dashboards.Dashboard, folderID int64, folderUID string) *dashboards.Dashboard { - // nolint:staticcheck - dash.FolderID = folderID - dash.FolderUID = folderUID - dashItem := &dashboards.SaveDashboardDTO{ - Dashboard: dash, - Message: "", - OrgID: user.OrgID, - User: &user, - Overwrite: false, - } - - features := featuremgmt.WithFeatures() - cfg := setting.NewCfg() - quotaService := quotatest.New(false, nil) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - 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) - 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, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - _, 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, - actest.FakeService{}, - folderSvc, - nil, - client.MockTestRestConfig{}, - nil, - quotaService, - nil, - nil, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore(), - ) - require.NoError(t, err) - service.RegisterDashboardPermissions(dashboardPermissions) - dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) - require.NoError(t, err) - - return dashboard -} - -func createFolder(t *testing.T, sc scenarioContext, title string, folderSvc folder.Service) *folder.Folder { +func createFolder(t *testing.T, sc scenarioContext, title string, folderSvc *foldertest.FakeService) *folder.Folder { t.Helper() ctx := identity.WithRequester(context.Background(), &sc.user) - folder, err := folderSvc.Create(ctx, &folder.CreateFolderCommand{ + f, err := folderSvc.Create(ctx, &folder.CreateFolderCommand{ OrgID: sc.user.OrgID, Title: title, UID: "uid_for_" + title, SignedInUser: &sc.user, }) require.NoError(t, err) + folderSvc.ExpectedFolder = f + folderSvc.ExpectedFolders = append(folderSvc.ExpectedFolders, f) // Set user permissions on the newly created folder so that they can interact with library elements stored in it - sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.UID)) - sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.UID)) - sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionDashboardsCreate] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionDashboardsCreate], dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.UID)) + sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID)) + sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID)) + sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionDashboardsCreate] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionDashboardsCreate], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID)) - return folder + return f } func validateAndUnMarshalResponse(t *testing.T, resp response.Response) libraryElementResult { @@ -465,32 +337,28 @@ func setupTestScenario(t *testing.T) scenarioContext { sqlStore, cfg := db.InitTestDBWithCfg(t) t.Cleanup(db.CleanupTestDB) quotaService := quotatest.New(false, nil) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) ac := acimpl.ProvideAccessControl(features) folderPermissions := acmock.NewMockedPermissionsService() 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) 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(), publicDash, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) + + folderSvc := foldertest.NewFakeService() + f := &folder.Folder{ + ID: 1, + OrgID: 1, + UID: "uid_for_ScenarioFolder", + Title: "ScenarioFolder", + } + folderSvc.ExpectedFolder = f + folderSvc.ExpectedFolders = []*folder.Folder{f} + + dashService := dashboards.NewFakeDashboardService(t) + 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, actest.FakeService{}, folderSvc, - nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), - serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore(), - ) - require.NoError(t, dashSvcErr) - dashService.RegisterDashboardPermissions(dashboardPermissions) service := LibraryElementService{ Cfg: cfg, features: featuremgmt.WithFeatures(), @@ -530,7 +398,8 @@ func setupTestScenario(t *testing.T) scenarioContext { Context: &webCtx, SignedInUser: &usr, }, - folderSvc: folderSvc, + folderSvc: folderSvc, + dashboardSvc: dashService, } sc.folder = createFolder(t, sc, "ScenarioFolder", folderSvc) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index e7118888e67..9250d690542 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -7,43 +7,27 @@ import ( "time" "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" - "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/kvstore" - "github.com/grafana/grafana/pkg/infra/serverlock" - "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/kinds/librarypanel" - "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" - "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" "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/libraryelements" "github.com/grafana/grafana/pkg/services/libraryelements/model" "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/search/sort" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" - "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/setting" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -93,7 +77,7 @@ func TestIntegrationConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) + dashInDB := createDashboard(t, sc, &dash) err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) @@ -191,7 +175,7 @@ func TestIntegrationConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) + dashInDB := createDashboard(t, sc, &dash) err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) @@ -237,7 +221,7 @@ func TestIntegrationConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) + dashInDB := createDashboard(t, sc, &dash) err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) @@ -293,7 +277,7 @@ func TestIntegrationConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) + dashInDB := createDashboard(t, sc, &dash) err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) @@ -411,7 +395,7 @@ func TestIntegrationImportLibraryPanelsForDashboard(t *testing.T) { element, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: missingUID, FolderName: dashboards.RootFolderName}) require.NoError(t, err) - var expected = getExpected(t, element, missingUID, missingName, missingModel) + var expected = getExpected(t, element, missingUID, missingName, missingModel, "Test Folder") var result = toLibraryElement(t, element) if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) @@ -452,7 +436,7 @@ func TestIntegrationImportLibraryPanelsForDashboard(t *testing.T) { element, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: existingUID, FolderName: dashboards.RootFolderName}) require.NoError(t, err) - var expected = getExpected(t, element, existingUID, existingName, sc.initialResult.Result.Model) + var expected = getExpected(t, element, existingUID, existingName, sc.initialResult.Result.Model, "Test Folder") expected.FolderUID = sc.initialResult.Result.FolderUID expected.Description = sc.initialResult.Result.Description expected.Meta.FolderUID = sc.folder.UID @@ -567,7 +551,7 @@ func TestIntegrationImportLibraryPanelsForDashboard(t *testing.T) { element, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: outsideUID, FolderName: dashboards.RootFolderName}) require.NoError(t, err) - expected := getExpected(t, element, outsideUID, outsideName, outsideModel) + expected := getExpected(t, element, outsideUID, outsideName, outsideModel, "Test Folder") result := toLibraryElement(t, element) if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) @@ -575,7 +559,7 @@ func TestIntegrationImportLibraryPanelsForDashboard(t *testing.T) { element, err = sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: insideUID, FolderName: dashboards.RootFolderName}) require.NoError(t, err) - expected = getExpected(t, element, insideUID, insideName, insideModel) + expected = getExpected(t, element, insideUID, insideName, insideModel, "Test Folder") result = toLibraryElement(t, element) if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) @@ -648,6 +632,8 @@ type scenarioContext struct { initialResult libraryPanelResult sqlStore db.DB lps LibraryPanelService + mockDashboard *dashboards.FakeDashboardService + mockFolder *foldertest.FakeService } func toLibraryElement(t *testing.T, res model.LibraryElementDTO) libraryElement { @@ -685,7 +671,7 @@ func toLibraryElement(t *testing.T, res model.LibraryElementDTO) libraryElement } } -func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name string, lEModel map[string]any) libraryElement { +func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name string, lEModel map[string]any, folderName string) libraryElement { marshalled, err := json.Marshal(lEModel) require.NoError(t, err) var libModel libraryElementModel @@ -704,7 +690,7 @@ func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name str Model: libModel, Version: 1, Meta: model.LibraryElementDTOMeta{ - FolderName: "General", + FolderName: folderName, FolderUID: res.FolderUID, ConnectedDashboards: 0, Created: res.Meta.Created, @@ -722,59 +708,14 @@ func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name str }, } } +func createDashboard(t *testing.T, sc scenarioContext, dash *dashboards.Dashboard) *dashboards.Dashboard { + dash.ID = 1 + dash.UID = "test-dashboard-uid" + dash.Created = time.Now() + dash.Updated = time.Now() + dash.Version = 1 -func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash *dashboards.Dashboard) *dashboards.Dashboard { - dashItem := &dashboards.SaveDashboardDTO{ - Dashboard: dash, - Message: "", - OrgID: user.OrgID, - User: user, - Overwrite: false, - } - - features := featuremgmt.WithFeatures() - cfg := setting.NewCfg() - quotaService := quotatest.New(false, nil) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - ac := actest.FakeAccessControl{ExpectedEvaluate: true} - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - dashPermissionService := acmock.NewMockedPermissionsService() - dashPermissionService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - service, err := dashboardservice.ProvideDashboardServiceImpl( - cfg, dashboardStore, folderStore, - features, acmock.NewMockedPermissionsService(), ac, actest.FakeService{}, foldertest.NewFakeService(), - nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), - serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore()) - require.NoError(t, err) - service.RegisterDashboardPermissions(dashPermissionService) - dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) - require.NoError(t, err) - - return dashboard -} - -func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder { - t.Helper() - - features := featuremgmt.WithFeatures() - ac := actest.FakeAccessControl{ExpectedEvaluate: true} - cfg := setting.NewCfg() - dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore)) - require.NoError(t, err) - folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) - 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, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - - t.Logf("Creating folder with title and UID %q", title) - ctx := identity.WithRequester(context.Background(), sc.user) - folder, err := s.Create(ctx, &folder.CreateFolderCommand{OrgID: sc.user.OrgID, Title: title, UID: title, SignedInUser: sc.user}) - require.NoError(t, err) - - return folder + return dash } func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) { @@ -821,8 +762,6 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s }) } -// testScenario is a wrapper around t.Run performing common setup for library panel tests. -// It takes your real test function as a callback. func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) { t.Helper() @@ -832,38 +771,29 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo sqlStore, cfg := db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) features := featuremgmt.WithFeatures() - ac := actest.FakeAccessControl{ExpectedEvaluate: true} - dashStore := &dashboards.FakeDashboardStore{} - 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, actest.FakeService{}, folderSvc, - nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), - serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore()) - require.NoError(t, err) - dashService.RegisterDashboardPermissions(dashPermissionService) - - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - fStore := folderimpl.ProvideStore(sqlStore) - - folderService := folderimpl.ProvideService( - fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - - elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, features, ac, dashService, nil, nil) + mockDashboardService := dashboards.NewFakeDashboardService(t) + mockFolderService := foldertest.NewFakeService() + mockFolder := &folder.Folder{ + ID: 1, + UID: "test-folder-uid", + Title: "Test Folder", + URL: "/dashboards/f/test-folder-uid/test-folder", + Version: 0, + Created: time.Now(), + Updated: time.Now(), + UpdatedBy: 0, + CreatedBy: 0, + HasACL: false, + } + mockFolderService.ExpectedFolder = mockFolder + elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), mockFolderService, features, ac, mockDashboardService, nil, nil) service := LibraryPanelService{ Cfg: cfg, SQLStore: sqlStore, LibraryElementService: elementService, - FolderService: folderService, + FolderService: mockFolderService, } - usr := &user.SignedInUser{ UserID: 1, Name: "Signed In User", @@ -872,17 +802,12 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo OrgID: orgID, OrgRole: role, LastSeenAt: time.Now(), - // Allow the user to create folders Permissions: map[int64]map[string][]string{ orgID: { dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, }, }, } - - // deliberate difference between signed in user and user in db to make it crystal clear - // what to expect in the tests - // In the real world these are identical cmd := user.CreateUserCommand{ Email: "user.in.db@test.com", Name: "User In DB", @@ -898,6 +823,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo require.NoError(t, err) _, err = usrSvc.Create(context.Background(), &cmd) require.NoError(t, err) + sc := scenarioContext{ user: usr, ctx: ctx, @@ -905,21 +831,11 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo elementService: elementService, sqlStore: sqlStore, lps: service, + mockDashboard: mockDashboardService, + mockFolder: mockFolderService, } - foldr := createFolder(t, sc, "ScenarioFolder") - sc.folder = &folder.Folder{ - ID: foldr.ID, // nolint:staticcheck - UID: foldr.UID, - Title: foldr.Title, - URL: dashboards.GetFolderURL(foldr.UID, slugify.Slugify(foldr.Title)), - Version: 0, - Created: foldr.Created, - Updated: foldr.Updated, - UpdatedBy: 0, - CreatedBy: 0, - HasACL: false, - } + sc.folder = mockFolder fn(t, sc) }) } diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 35d3da6d72a..ea3a9a16a13 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -571,6 +571,7 @@ func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) { fldr.ParentUID = "" folderService.ExpectedFolder = fldr folderService.ExpectedFolders = []*folder.Folder{fldr} + folderService.AddFolder(fldr) ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) // Create rules in both folders @@ -669,6 +670,8 @@ func TestRouteConvertPrometheusGetNamespace(t *testing.T) { fldr2 := randFolder() fldr2.ParentUID = "" folderService.ExpectedFolders = []*folder.Folder{fldr, fldr2} + folderService.AddFolder(fldr) + folderService.AddFolder(fldr2) ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr, fldr2) // Create a Grafana rule for each Prometheus rule @@ -798,6 +801,7 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) { // Create a folder in the root fldr := randFolder() fldr.ParentUID = "" + folderService.AddFolder(fldr) folderService.ExpectedFolders = []*folder.Folder{fldr} ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index dc60b8dad58..e21f949e4f7 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -29,14 +29,10 @@ import ( "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/apiserver" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "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/folder/foldertest" ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" @@ -47,14 +43,10 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" - "github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/secrets" secrets_fakes "github.com/grafana/grafana/pkg/services/secrets/fakes" - "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" - "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -353,14 +345,6 @@ func TestIntegrationProvisioningApi(t *testing.T) { orgID := int64(2) rule := createTestAlertRule("rule", orgID) - _, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ - UID: rule.FolderUID, - Title: "Folder Title", - OrgID: orgID, - SignedInUser: &user.SignedInUser{OrgID: orgID}, - }) - require.NoError(t, err) - insertRuleInOrg(t, sut, rule, orgID) rule.FolderUID = "does-not-exist" @@ -462,14 +446,7 @@ func TestIntegrationProvisioningApi(t *testing.T) { rc.Req.Header = map[string][]string{"X-Disable-Provenance": {"true"}} rc.OrgID = 3 rule := createTestAlertRule("rule", 1) - - _, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ - UID: "folder-uid", - Title: "Folder Title", - OrgID: rc.OrgID, - SignedInUser: &user.SignedInUser{OrgID: rc.OrgID}, - }) - require.NoError(t, err) + rule.FolderUID = "folder-uid3" response := sut.RoutePostAlertRule(&rc, rule) @@ -486,13 +463,7 @@ func TestIntegrationProvisioningApi(t *testing.T) { rule.UID = uid orgID := int64(3) - _, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ - UID: "folder-uid", - Title: "Folder Title", - OrgID: orgID, - SignedInUser: &user.SignedInUser{OrgID: orgID}, - }) - require.NoError(t, err) + rule.FolderUID = "folder-uid3" insertRuleInOrg(t, sut, rule, orgID) rc := createTestRequestCtx() @@ -560,14 +531,7 @@ func TestIntegrationProvisioningApi(t *testing.T) { uid := util.GenerateShortUID() rule := createTestAlertRule("rule", 3) rule.UID = uid - - _, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ - UID: rule.FolderUID, - Title: "Folder Title", - OrgID: rule.OrgID, - SignedInUser: &user.SignedInUser{OrgID: rule.OrgID}, - }) - require.NoError(t, err) + rule.FolderUID = "folder-uid3" insertRuleInOrg(t, sut, rule, 3) @@ -2053,7 +2017,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: string(raw), }) - sqlStore, cfg := db.InitTestDBWithCfg(t) + sqlStore, _ := db.InitTestDBWithCfg(t) quotas := &provisioning.MockQuotaChecker{} quotas.EXPECT().LimitOK() @@ -2077,14 +2041,39 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { }}, nil).Maybe() ac := &recordingAccessControlFake{} - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - 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, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) + folderService := foldertest.NewFakeService() + folder1 := &folder.Folder{ + UID: "folder-uid", + Title: "Folder Title", + Fullpath: "Folder Title", + OrgID: 1, + } + folder2 := &folder.Folder{ + UID: "folder-uid2", + Title: "Folder Title2", + ParentUID: "folder-uid", + Fullpath: "Folder Title2", + OrgID: 1, + } + folder3 := &folder.Folder{ + UID: "folder-uid3", + Title: "Folder Title3", + ParentUID: "folder-uid", + Fullpath: "Folder Title3", + OrgID: 3, + } + folderService.SetFolders(map[string]*folder.Folder{ + "folder-uid": folder1, + "folder-uid2": folder2, + "folder-uid3": folder3, + }) + folderService.ExpectedFolders = []*folder.Folder{ + folder1, + folder2, + folder3, + } + // if not one of the two above, return ErrFolderNotFound + folderService.ExpectedError = dashboards.ErrFolderNotFound store := store.DBstore{ Logger: log, SQLStore: sqlStore, @@ -2104,23 +2093,6 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { */ } - parent, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{ - OrgID: 1, - UID: "folder-uid", - Title: "Folder Title", - SignedInUser: user, - }) - require.NoError(t, err) - - _, err = folderService.Create(context.Background(), &folder.CreateFolderCommand{ - OrgID: 1, - UID: "folder-uid2", - Title: "Folder Title2", - ParentUID: parent.UID, - SignedInUser: user, - }) - require.NoError(t, err) - ruleAuthz := &fakes.FakeRuleService{} features := featuremgmt.WithFeatures() diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 97c6002b853..74f51fd305a 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -27,12 +27,12 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/ngalert/testutil" - "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/infra/db" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -242,10 +242,10 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) { } sqlStore := db.InitTestDB(t) - folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + fakeFolderService := foldertest.NewFakeService() b := &fakeBus{} logger := &logtest.Fake{} - store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) + store := createTestStore(sqlStore, fakeFolderService, logger, cfg.UnifiedAlerting, b) store.FeatureToggles = featuremgmt.WithFeatures() gen := models.RuleGen @@ -258,15 +258,38 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) { parentFolderUid := uuid.NewString() parentFolderTitle := "Very Parent Folder" - createFolder(t, store, parentFolderUid, parentFolderTitle, rule1.OrgID, "") rule1FolderTitle := "folder-" + rule1.Title rule2FolderTitle := "folder-" + rule2.Title rule3FolderTitle := "folder-" + rule3.Title - createFolder(t, store, rule1.NamespaceUID, rule1FolderTitle, rule1.OrgID, parentFolderUid) - createFolder(t, store, rule2.NamespaceUID, rule2FolderTitle, rule2.OrgID, "") - createFolder(t, store, rule3.NamespaceUID, rule3FolderTitle, rule3.OrgID, "") - createFolder(t, store, rule2.NamespaceUID, "same UID folder", gen.GenerateRef().OrgID, "") // create a folder with the same UID but in the different org + fakeFolderService.AddFolder(&folder.Folder{ + UID: rule1.NamespaceUID, + Title: rule1FolderTitle, + OrgID: rule1.OrgID, + ParentUID: parentFolderUid, + Fullpath: rule1FolderTitle, + }) + fakeFolderService.AddFolder(&folder.Folder{ + UID: rule2.NamespaceUID, + Title: rule2FolderTitle, + OrgID: rule2.OrgID, + ParentUID: "", + Fullpath: rule2FolderTitle, + }) + fakeFolderService.AddFolder(&folder.Folder{ + UID: rule3.NamespaceUID, + Title: rule3FolderTitle, + OrgID: rule3.OrgID, + ParentUID: "", + Fullpath: rule3FolderTitle, + }) + fakeFolderService.AddFolder(&folder.Folder{ + UID: parentFolderUid, + Title: parentFolderTitle, + OrgID: rule1.OrgID, + ParentUID: "", + Fullpath: parentFolderTitle, + }) tc := []struct { name string @@ -343,7 +366,14 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) { } t.Run("when nested folders are enabled folders should contain full path", func(t *testing.T) { - store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)) + fakeFolderService.AddFolder(&folder.Folder{ + UID: rule1.NamespaceUID, + Title: rule1FolderTitle, + OrgID: rule1.OrgID, + ParentUID: parentFolderUid, + Fullpath: parentFolderTitle + "/" + rule1FolderTitle, + }) + query := &models.GetAlertRulesForSchedulingQuery{ PopulateFolders: true, } @@ -1597,27 +1627,6 @@ func createRule(t *testing.T, store *DBstore, generator *models.AlertRuleGenerat return rule } -func createFolder(t *testing.T, store *DBstore, uid, title string, orgID int64, parentUID string) { - t.Helper() - u := &user.SignedInUser{ - UserID: 1, - OrgID: orgID, - OrgRole: org.RoleAdmin, - IsGrafanaAdmin: true, - } - - _, err := store.FolderService.Create(context.Background(), &folder.CreateFolderCommand{ - UID: uid, - OrgID: orgID, - Title: title, - Description: "", - SignedInUser: u, - ParentUID: parentUID, - }) - - require.NoError(t, err) -} - func setupFolderService(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles) folder.Service { tracer := tracing.InitializeTracerForTest() inProcBus := bus.ProvideBus(tracer) diff --git a/pkg/services/ngalert/store/namespace_test.go b/pkg/services/ngalert/store/namespace_test.go index 477ce0ef151..03fea1b514e 100644 --- a/pkg/services/ngalert/store/namespace_test.go +++ b/pkg/services/ngalert/store/namespace_test.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/org" @@ -28,7 +27,7 @@ func TestIntegration_GetUserVisibleNamespaces(t *testing.T) { sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() - folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + folderService := foldertest.NewFakeService() b := &fakeBus{} logger := log.New("test-dbstore") store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) @@ -40,18 +39,14 @@ func TestIntegration_GetUserVisibleNamespaces(t *testing.T) { IsGrafanaAdmin: true, } - folders := []struct { - uid string - title string - parentUid string - }{ - {uid: uuid.NewString(), title: "folder1", parentUid: ""}, - {uid: uuid.NewString(), title: "folder2", parentUid: ""}, - {uid: uuid.NewString(), title: "nested/folder", parentUid: ""}, + folders := []*folder.Folder{ + {UID: uuid.NewString(), Title: "folder1", ParentUID: "", OrgID: 1}, + {UID: uuid.NewString(), Title: "folder2", ParentUID: "", OrgID: 1}, + {UID: uuid.NewString(), Title: "nested/folder", ParentUID: "", OrgID: 1}, } for _, f := range folders { - createFolder(t, store, f.uid, f.title, 1, f.parentUid) + folderService.AddFolder(f) } t.Run("returns all folders", func(t *testing.T) { diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 762dde12a36..60674e8153c 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -24,13 +24,12 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "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/ngalert" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" - "github.com/grafana/grafana/pkg/services/ngalert/testutil" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -83,9 +82,8 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration, opts ...TestEnvOpti tracer := tracing.InitializeTracerForTest() bus := bus.ProvideBus(tracer) - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - dashboardService, dashboardStore := testutil.SetupDashboardService(tb, sqlStore, folderStore, cfg) - folderService := testutil.SetupFolderService(tb, cfg, sqlStore, dashboardStore, folderStore, bus, options.featureToggles, ac) + folderService := foldertest.NewFakeService() + dashboardService := dashboards.NewFakeDashboardService(tb) ruleStore, err := store.ProvideDBStore(cfg, options.featureToggles, sqlStore, folderService, &dashboards.FakeDashboardService{}, ac, bus) require.NoError(tb, err) ng, err := ngalert.ProvideService( diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index e25bcb44a4c..bf89d4aec5d 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -504,8 +504,10 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe folderSvc := folderimpl.ProvideService( fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) + orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) + require.NoError(t, err) dashService, err := dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), - ac, actest.FakeService{}, folderSvc, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), + ac, actest.FakeService{}, folderSvc, nil, client.MockTestRestConfig{}, nil, quotaService, orgService, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), kvstore.NewFakeKVStore()) require.NoError(t, err) diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 46474d70df7..716323d3151 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -10,13 +10,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "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/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -25,14 +21,11 @@ import ( "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" - "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -826,31 +819,42 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol // dashboard store commands that should be called. dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db)) require.NoError(t, err) - 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, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - - // create parent folder - parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ - UID: "parent", - OrgID: orgID, + // create in both the folder & dashboard tables + parent, err := fStore.Create(context.Background(), folder.CreateFolderCommand{ Title: "parent", - SignedInUser: usr, - }) - require.NoError(t, err) - - // create subfolder - subfolder, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ - UID: "subfolder", - ParentUID: "parent", OrgID: orgID, - Title: "subfolder", + UID: "parent", SignedInUser: usr, }) require.NoError(t, err) - + _, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{ + OrgID: orgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "parent", + "uid": parent.UID, + }), + IsFolder: true, + }) + require.NoError(t, err) + subfolder, err := fStore.Create(context.Background(), folder.CreateFolderCommand{ + Title: "subfolder", + OrgID: orgID, + UID: "subfolder", + ParentUID: parent.UID, + SignedInUser: usr, + }) + require.NoError(t, err) + _, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{ + OrgID: orgID, + FolderUID: parent.UID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "subfolder", + "uid": subfolder.UID, + }), + IsFolder: true, + }) + require.NoError(t, err) // create a root level dashboard _, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{ OrgID: orgID, diff --git a/pkg/services/stats/statsimpl/stats.go b/pkg/services/stats/statsimpl/stats.go index f4e2b0db479..6399fc8b771 100644 --- a/pkg/services/stats/statsimpl/stats.go +++ b/pkg/services/stats/statsimpl/stats.go @@ -158,11 +158,6 @@ func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *stats.GetS if ss.IsUnifiedAlertingEnabled() { sb.Write(`(SELECT COUNT(DISTINCT (` + dialect.Quote("rule_group") + `)) FROM ` + dialect.Quote("alert_rule") + `) AS rule_groups,`) } - // currently not supported when dashboards are in unified storage - if !ss.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) { - sb.Write(`(SELECT SUM(LENGTH(data)) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS dashboard_bytes_total,`, dialect.BooleanValue(false)) - sb.Write(`(SELECT MAX(LENGTH(data)) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS dashboard_bytes_max,`, dialect.BooleanValue(false)) - } sb.Write(ss.roleCounterSQL(ctx)) diff --git a/pkg/storage/unified/README.md b/pkg/storage/unified/README.md index 11344724271..03301cf12bc 100644 --- a/pkg/storage/unified/README.md +++ b/pkg/storage/unified/README.md @@ -62,7 +62,6 @@ protocol = https [feature_toggles] grafanaAPIServerWithExperimentalAPIs = true -kubernetesClientDashboardsFolders = true [unified_storage.folders.folder.grafana.app] dualWriterMode = 4 @@ -259,7 +258,6 @@ To enable it, add the following to your `custom.ini` under the `[feature_toggles [feature_toggles] ; Used by the Grafana instance unifiedStorageSearchUI = true -kubernetesClientDashboardsFolders = true ; Used by unified storage unifiedStorageSearch = true @@ -369,7 +367,6 @@ signing_keys_url = http://localhost:3011/api/signing-keys/keys mode = "on-prem" [feature_toggles] -kubernetesClientDashboardsFolders = true kubernetesDashboardsAPI = true kubernetesFolders = true unifiedStorage = true @@ -418,7 +415,6 @@ http_port = 3011 http_addr = "127.0.0.2" [feature_toggles] -kubernetesClientDashboardsFolders = true kubernetesDashboardsAPI = true kubernetesFolders = true unifiedStorageSearchUI = true diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 0d87e0c6f00..50355421314 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -19,7 +19,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboards" - "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/services/plugindashboards" @@ -41,8 +40,7 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) { } dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + DisableAnonymous: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) @@ -276,10 +274,9 @@ func TestIntegrationDashboardQuota(t *testing.T) { // Setup Grafana and its Database dashboardQuota := int64(1) dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - EnableQuota: true, - DashboardOrgQuota: &dashboardQuota, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + DisableAnonymous: true, + EnableQuota: true, + DashboardOrgQuota: &dashboardQuota, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) @@ -337,8 +334,7 @@ func TestIntegrationDashboardQuota(t *testing.T) { func TestIntegrationUpdatingProvisionionedDashboards(t *testing.T) { // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + DisableAnonymous: true, }) provDashboardsDir := filepath.Join(dir, "conf", "provisioning", "dashboards") @@ -491,8 +487,7 @@ providers: func TestIntegrationCreate(t *testing.T) { // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + DisableAnonymous: true, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) @@ -647,8 +642,7 @@ func intPtr(n int) *int { func TestIntegrationPreserveSchemaVersion(t *testing.T) { dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + DisableAnonymous: true, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) @@ -738,8 +732,7 @@ func TestIntegrationPreserveSchemaVersion(t *testing.T) { func TestIntegrationImportDashboardWithLibraryPanels(t *testing.T) { dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + DisableAnonymous: true, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) @@ -996,8 +989,7 @@ func TestIntegrationDashboardServicePermissions(t *testing.T) { } dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + DisableAnonymous: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ diff --git a/pkg/tests/api/folders/api_folder_test.go b/pkg/tests/api/folders/api_folder_test.go index 2de64723508..156097bcda8 100644 --- a/pkg/tests/api/folders/api_folder_test.go +++ b/pkg/tests/api/folders/api_folder_test.go @@ -46,7 +46,7 @@ func TestIntegrationFolderServiceGetFolder(t *testing.T) { EnableUnifiedAlerting: true, DisableAnonymous: true, AppModeProduction: true, - EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders, featuremgmt.FlagKubernetesClientDashboardsFolders}, + EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders}, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, p) @@ -114,9 +114,8 @@ func TestIntegrationUpdateFolder(t *testing.T) { } dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - EnableQuota: true, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + DisableAnonymous: true, + EnableQuota: true, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) @@ -157,9 +156,8 @@ func TestIntegrationUpdateFolder(t *testing.T) { func TestIntegrationCreateFolder(t *testing.T) { dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - EnableQuota: true, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + DisableAnonymous: true, + EnableQuota: true, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) @@ -206,9 +204,8 @@ func TestIntegrationNestedFoldersOn(t *testing.T) { t.Skip("skipping integration test") } dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - EnableQuota: true, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + DisableAnonymous: true, + EnableQuota: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) @@ -362,7 +359,7 @@ func TestIntegrationSharedWithMe(t *testing.T) { EnableUnifiedAlerting: true, DisableAnonymous: true, AppModeProduction: true, - EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders, featuremgmt.FlagKubernetesClientDashboardsFolders}, + EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders}, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) @@ -416,7 +413,6 @@ func TestIntegrationBasicRoles(t *testing.T) { EnableUnifiedAlerting: true, DisableAnonymous: true, AppModeProduction: true, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) @@ -554,7 +550,7 @@ func TestIntegrationFineGrainedPermissions(t *testing.T) { EnableUnifiedAlerting: true, DisableAnonymous: true, AppModeProduction: true, - EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders, featuremgmt.FlagKubernetesClientDashboardsFolders}, + EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders}, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) diff --git a/pkg/tests/api/folders/api_folders_test.go b/pkg/tests/api/folders/api_folders_test.go index 80e96e67edb..ddcf722cc49 100644 --- a/pkg/tests/api/folders/api_folders_test.go +++ b/pkg/tests/api/folders/api_folders_test.go @@ -35,7 +35,6 @@ func TestGetFolders(t *testing.T) { EnableUnifiedAlerting: true, DisableAnonymous: true, AppModeProduction: true, - EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index c20d235eea9..8d4bcb8a744 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -222,12 +222,7 @@ func TestIntegrationLegacySupport(t *testing.T) { t.Skip("skipping integration test in short mode") } ctx := context.Background() - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - EnableFeatureToggles: []string{ - // NOTE: when using this feature toggle, the read is always v0! - // featuremgmt.FlagKubernetesClientDashboardsFolders - }, - }) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{}) clientV0 := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, diff --git a/pkg/tests/apis/dashboard/integration/api_validation_test.go b/pkg/tests/apis/dashboard/integration/api_validation_test.go index 881ea3c1430..a3a1ae25829 100644 --- a/pkg/tests/apis/dashboard/integration/api_validation_test.go +++ b/pkg/tests/apis/dashboard/integration/api_validation_test.go @@ -69,7 +69,6 @@ func TestIntegrationDashboardAPIValidation(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ DisableAnonymous: true, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, // Enable dashboard feature featuremgmt.FlagUnifiedStorageSearch, featuremgmt.FlagKubernetesDashboards, // Enable FE-only dashboard feature flag }, @@ -101,7 +100,6 @@ func TestIntegrationDashboardAPIValidation(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ DisableAnonymous: true, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, // Enable dashboard feature featuremgmt.FlagUnifiedStorageSearch, }, DisableFeatureToggles: []string{ @@ -138,7 +136,6 @@ func TestIntegrationDashboardAPIAuthorization(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ DisableAnonymous: true, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, // Enable dashboard feature featuremgmt.FlagUnifiedStorageSearch, }, UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ @@ -189,7 +186,6 @@ func TestIntegrationDashboardAPI(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ DisableAnonymous: true, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, // Enable dashboard feature featuremgmt.FlagUnifiedStorageSearch, featuremgmt.FlagKubernetesDashboards, }, diff --git a/pkg/tests/apis/dashboard/integration/library_panels_api_validation_test.go b/pkg/tests/apis/dashboard/integration/library_panels_api_validation_test.go index b2857d97a69..5f001cf3e15 100644 --- a/pkg/tests/apis/dashboard/integration/library_panels_api_validation_test.go +++ b/pkg/tests/apis/dashboard/integration/library_panels_api_validation_test.go @@ -32,7 +32,6 @@ func TestIntegrationLibraryPanelConnections(t *testing.T) { DisableAnonymous: true, EnableFeatureToggles: []string{ "unifiedStorageSearch", - "kubernetesClientDashboardsFolders", "kubernetesLibraryPanels", }, }) @@ -98,7 +97,6 @@ func TestIntegrationLibraryElementPermissions(t *testing.T) { DisableAnonymous: true, EnableFeatureToggles: []string{ "unifiedStorageSearch", - "kubernetesClientDashboardsFolders", "kubernetesLibraryPanels", "grafanaAPIServerWithExperimentalAPIs", // needed until we move it to v0beta1 at least (currently v0alpha1) }, @@ -303,7 +301,6 @@ func TestIntegrationLibraryPanelConnectionsWithFolderAccess(t *testing.T) { EnableFeatureToggles: []string{ "unifiedStorageSearch", "kubernetesLibraryPanels", - "kubernetesClientDashboardsFolders", }, }) ctx := createTestContext(t, helper, helper.Org1, dualWriterMode) diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index 5001e90f42c..98b0509df10 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -50,10 +50,8 @@ func TestIntegrationFoldersApp(t *testing.T) { } helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, + AppModeProduction: true, + EnableFeatureToggles: []string{}, }) t.Run("Check discovery client", func(t *testing.T) { @@ -138,9 +136,7 @@ func TestIntegrationFoldersApp(t *testing.T) { DualWriterMode: modeDw, }, }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, + EnableFeatureToggles: []string{}, })) }) @@ -155,7 +151,6 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, featuremgmt.FlagNestedFolders, }, })) @@ -172,7 +167,6 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, featuremgmt.FlagNestedFolders, }, })) @@ -189,7 +183,6 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, featuremgmt.FlagNestedFolders, }, })) @@ -206,7 +199,6 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, featuremgmt.FlagNestedFolders, }, })) @@ -237,7 +229,6 @@ func TestIntegrationFoldersApp(t *testing.T) { // We set it to 1 here, so we always get forced pagination based on the response size. UnifiedStorageMaxPageSizeBytes: 1, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, featuremgmt.FlagNestedFolders, }, }), mode) @@ -680,7 +671,6 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { }, EnableFeatureToggles: []string{ featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesClientDashboardsFolders, }, }) @@ -788,7 +778,6 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { }, EnableFeatureToggles: []string{ featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesClientDashboardsFolders, }, }) @@ -972,7 +961,6 @@ func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) { }, EnableFeatureToggles: []string{ featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesClientDashboardsFolders, }, }) @@ -1148,7 +1136,6 @@ func TestIntegrationFoldersGetAPIEndpointK8S(t *testing.T) { EnableFeatureToggles: []string{ featuremgmt.FlagNestedFolders, featuremgmt.FlagUnifiedStorageSearch, - featuremgmt.FlagKubernetesClientDashboardsFolders, }, }) diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index 05ed40bdd34..9e5b754c4d9 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -28,8 +28,7 @@ func TestIntegrationOpenAPIs(t *testing.T) { h := NewK8sTestHelper(t, testinfra.GrafanaOpts{ AppModeProduction: true, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, // Will be default on by G12 - featuremgmt.FlagQueryService, // Query Library + featuremgmt.FlagQueryService, // Query Library featuremgmt.FlagProvisioning, featuremgmt.FlagInvestigationsBackend, featuremgmt.FlagGrafanaAdvisor, diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go index 9c7949c2032..d708b0e9cc3 100644 --- a/pkg/tests/apis/provisioning/helper_test.go +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -221,7 +221,6 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper AppModeProduction: false, // required for experimental APIs EnableFeatureToggles: []string{ featuremgmt.FlagProvisioning, - featuremgmt.FlagKubernetesClientDashboardsFolders, }, UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ "dashboards.dashboard.grafana.app": { diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 46e67142648..694c37469f7 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -383,13 +383,8 @@ export class DashboardScene extends SceneObjectBase impleme } public onRestore = async (version: DecoratedRevisionModel): Promise => { - let versionRsp; - if (config.featureToggles.kubernetesClientDashboardsFolders) { - // 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); - } + // the id here is the resource version in k8s, use this instead to get the specific version + let versionRsp = await historySrv.restoreDashboard(version.uid, version.id); 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 501b6d7dfae..997ed92fb55 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx @@ -1,4 +1,3 @@ -import { config } from '@grafana/runtime'; import { SceneTimeRange } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; @@ -149,10 +148,7 @@ describe('VersionsEditView', () => { expect(versionsView.versions.find((rev) => rev.version === 1)).toBeUndefined(); }); - it('should correctly identify last page when kubernetesClientDashboardsFolders is enabled and continueToken is empty', async () => { - // @ts-ignore - config.featureToggles.kubernetesClientDashboardsFolders = true; - + it('should correctly identify last page when continueToken is empty', async () => { jest.mocked(historySrv.getHistoryList).mockResolvedValueOnce({ continueToken: '', versions: [ @@ -190,10 +186,6 @@ describe('VersionsEditView', () => { expect(versionsView.versions.length).toBeLessThan(VERSIONS_FETCH_LIMIT); expect(versionsView.versions.find((rev) => rev.version === 1)).toBeUndefined(); expect(versionsView.continueToken).toBe(''); - - // reset feature flag - // @ts-ignore - config.featureToggles.kubernetesClientDashboardsFolders = false; }); }); }); diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx index 1620111aecd..e21c7c00cf1 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx @@ -1,7 +1,6 @@ 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'; @@ -136,15 +135,9 @@ export class VersionsEditView extends SceneObjectBase imp if (!this._dashboard.state.uid) { return; } - let lhs, rhs; - if (config.featureToggles.kubernetesClientDashboardsFolders) { - // 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); - } + // the id here is the resource version in k8s, use this instead to get the specific version + let lhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, baseInfo.id); + let rhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, newInfo.id); this.setState({ baseInfo, @@ -204,10 +197,10 @@ function VersionsEditorSettingsListView({ model }: SceneComponentProps 1; const hasMore = model.versions.length >= model.limit; // older versions may have been cleaned up in the db, so also check if the last page is less than the limit, if so, we are at the end - let isLastPage = model.versions.find((rev) => rev.version === 1) || model.versions.length % model.limit !== 0; - if (config.featureToggles.kubernetesClientDashboardsFolders) { - isLastPage = isLastPage || model.continueToken === ''; - } + let isLastPage = + model.versions.find((rev) => rev.version === 1) || + model.versions.length % model.limit !== 0 || + model.continueToken === ''; const viewModeCompare = ( <> diff --git a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx index 6fc2e3c5334..b679d1ccf69 100644 --- a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx @@ -2,7 +2,6 @@ import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render } from 'test/test-utils'; -import { config } from '@grafana/runtime'; import { historySrv } from 'app/features/dashboard-scene/settings/version-history/HistorySrv'; import { createDashboardModelFixture } from '../../state/__fixtures__/dashboardFixtures'; @@ -110,7 +109,7 @@ describe('VersionSettings', () => { test('renders buttons if versions >= VERSIONS_FETCH_LIMIT', async () => { historySrv.getHistoryList = jest.fn().mockResolvedValue({ - continueToken: versions.continueToken, + continueToken: 'next-page-token', versions: versions.versions.slice(0, VERSIONS_FETCH_LIMIT), }); @@ -135,13 +134,13 @@ describe('VersionSettings', () => { .fn() .mockImplementationOnce(() => Promise.resolve({ - continueToken: versions.continueToken, + continueToken: 'next-page-token', versions: versions.versions.slice(0, VERSIONS_FETCH_LIMIT), }) ) .mockImplementationOnce(() => Promise.resolve({ - continueToken: versions.continueToken, + continueToken: '', versions: versions.versions.slice(VERSIONS_FETCH_LIMIT), }) ); @@ -184,8 +183,7 @@ describe('VersionSettings', () => { expect(screen.getByRole('button', { name: /compare versions/i })).toBeInTheDocument(); }); - test('does not show more button when kubernetesClientDashboardsFolders is enabled and continueToken is empty', async () => { - config.featureToggles.kubernetesClientDashboardsFolders = true; + test('does not show more button when continueToken is empty', async () => { historySrv.getHistoryList = jest.fn().mockResolvedValueOnce({ continueToken: '', versions: versions.versions.slice(0, VERSIONS_FETCH_LIMIT - 1), @@ -197,8 +195,6 @@ describe('VersionSettings', () => { expect(screen.queryByRole('button', { name: /show more versions/i })).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: /compare versions/i })).toBeInTheDocument(); - - config.featureToggles.kubernetesClientDashboardsFolders = false; }); test('selecting two versions and clicking compare button should render compare view', async () => { diff --git a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx index fad6f79bffc..9c20ba24802 100644 --- a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx @@ -1,7 +1,6 @@ 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 { historySrv, RevisionsModel } from 'app/features/dashboard-scene/settings/version-history/HistorySrv'; @@ -90,15 +89,9 @@ export class VersionsSettings extends PureComponent { isLoading: true, }); - let lhs, rhs; - if (config.featureToggles.kubernetesClientDashboardsFolders) { - // 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); - } + // the id here is the resource version in k8s, use this instead to get the specific version + let lhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, baseInfo.id); + let rhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, newInfo.id); this.setState({ baseInfo, @@ -122,14 +115,11 @@ export class VersionsSettings extends PureComponent { })); isLastPage() { - if (config.featureToggles.kubernetesClientDashboardsFolders) { - return ( - this.state.versions.find((rev) => rev.version === 1) || - this.state.versions.length % this.limit !== 0 || - this.continueToken === '' - ); - } - return this.state.versions.find((rev) => rev.version === 1) || this.state.versions.length % this.limit !== 0; + return ( + this.state.versions.find((rev) => rev.version === 1) || + this.state.versions.length % this.limit !== 0 || + this.continueToken === '' + ); } onCheck = (ev: React.FormEvent, versionId: number) => { diff --git a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx index 53c7633fbaa..ff08bf44b6c 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 { config, locationService } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { useAppNotification } from 'app/core/copy/appNotification'; import { historySrv } from 'app/features/dashboard-scene/settings/version-history/HistorySrv'; import { useSelector } from 'app/types/store'; @@ -18,11 +18,7 @@ const restoreDashboard = async (version: number, dashboard: DashboardModel) => { export const useDashboardRestore = (id: number, version: number) => { const dashboard = useSelector((state) => state.dashboard.getModel()); - const [state, onRestoreDashboard] = useAsyncFn( - async () => - await restoreDashboard(config.featureToggles.kubernetesClientDashboardsFolders ? id : version, dashboard!), - [] - ); + const [state, onRestoreDashboard] = useAsyncFn(async () => await restoreDashboard(id, dashboard!), []); const notifyApp = useAppNotification(); useEffect(() => { diff --git a/public/app/features/provisioning/GettingStarted/features.ts b/public/app/features/provisioning/GettingStarted/features.ts index 8bcff361c68..3a30093b55a 100644 --- a/public/app/features/provisioning/GettingStarted/features.ts +++ b/public/app/features/provisioning/GettingStarted/features.ts @@ -1,11 +1,7 @@ import { FeatureToggles } from '@grafana/data'; import { config } from '@grafana/runtime'; -export const requiredFeatureToggles: Array = [ - 'provisioning', - 'kubernetesDashboards', - 'kubernetesClientDashboardsFolders', -]; +export const requiredFeatureToggles: Array = ['provisioning', 'kubernetesDashboards']; /** * Checks if all required feature toggles are enabled From af066d231259593a4b2c568a2255f36075cf9814 Mon Sep 17 00:00:00 2001 From: Jennifer Villa Date: Wed, 30 Jul 2025 02:20:01 -0500 Subject: [PATCH 025/126] Explore: Compact mode (#107351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(explore): implement compact mode feature This commit implements compact mode for Grafana Explore to address issue #102867. Features implemented: - URL parameter support: ?compact=true or ?compact=1 activates compact mode - Query rows auto-collapse: Query editors start collapsed showing summary line - Content outline auto-collapse: Sections start collapsed to save space - Single DrilldownAlertBox: Shows single banner in split view (no duplicates) Changes: - Added compactMode to ExploreState interface and selectors - Added URL parameter parsing in useStateSync hook - Modified QueryRows and QueryEditorRow components to support compact mode - Updated DrilldownAlertBox logic to prevent duplicates in split view - Fixed test files to use proper mock state objects Fixes #102867 * Move compact mode to pane state and sync with URL * Clean up * Show Drilldown alert box only when not splitted * Rename and clean up * Clean up * Add missing props * Add missing props * Add missing translations --------- Co-authored-by: Piotr Jamróz --- packages/grafana-data/src/types/explore.ts | 2 ++ public/app/features/explore/Explore.test.tsx | 2 ++ public/app/features/explore/Explore.tsx | 34 +++++++++++++++---- public/app/features/explore/ExplorePage.tsx | 12 ++++--- .../app/features/explore/QueryRows.test.tsx | 6 ++-- public/app/features/explore/QueryRows.tsx | 23 +++++++++---- .../hooks/useStateSync/external.utils.ts | 1 + .../hooks/useStateSync/migrators/v1.ts | 1 + .../useStateSync/synchronizer/fromURL.ts | 1 + .../hooks/useStateSync/synchronizer/init.ts | 7 ++-- .../hooks/useStateSync/synchronizer/toURL.ts | 4 ++- .../app/features/explore/state/explorePane.ts | 22 +++++++++++- public/app/features/explore/state/main.ts | 1 + public/app/features/explore/state/utils.ts | 1 + .../query/components/QueryEditorRow.tsx | 6 +++- .../query/components/QueryEditorRows.tsx | 6 ++++ public/app/types/explore.ts | 5 +++ public/locales/en-US/grafana.json | 3 ++ 18 files changed, 111 insertions(+), 26 deletions(-) diff --git a/packages/grafana-data/src/types/explore.ts b/packages/grafana-data/src/types/explore.ts index 68f9b999d40..62c4fdcf62b 100644 --- a/packages/grafana-data/src/types/explore.ts +++ b/packages/grafana-data/src/types/explore.ts @@ -49,6 +49,7 @@ export interface ExploreUrlState { queries: T[]; range: URLRange; panelsState?: ExplorePanelsState; + compact?: boolean; } export interface ExplorePanelsState extends Partial> { @@ -89,6 +90,7 @@ export interface SplitOpenOptions { range?: TimeRange; panelsState?: ExplorePanelsState; correlationHelperData?: ExploreCorrelationHelperData; + compact?: boolean; } /** diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index c1bcd2a7d97..cb47295d59d 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -108,6 +108,8 @@ const dummyProps: Props = { dsToExplore: [], }, changeDatasource: jest.fn(), + compact: false, + changeCompactMode: jest.fn(), }; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 57ef823de4d..a88d594c6d1 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -58,7 +58,7 @@ 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 { changeSize, changeCompactMode } from './state/explorePane'; import { splitOpen } from './state/main'; import { addQueryRow, @@ -182,14 +182,16 @@ export class Explore extends PureComponent { onContentOutlineToogle = () => { store.set(CONTENT_OUTLINE_LOCAL_STORAGE_KEYS.visible, !this.state.contentOutlineVisible); this.setState((state) => { + const newContentOutlineVisible = this.props.compact ? true : !state.contentOutlineVisible; reportInteraction('explore_toolbar_contentoutline_clicked', { item: 'outline', - type: state.contentOutlineVisible ? 'close' : 'open', + type: newContentOutlineVisible ? 'open' : 'close', }); return { - contentOutlineVisible: !state.contentOutlineVisible, + contentOutlineVisible: newContentOutlineVisible, }; }); + this.props.changeCompactMode(this.props.exploreId, false); }; /** @@ -306,9 +308,12 @@ export class Explore extends PureComponent { updateTimeRange({ exploreId, absoluteRange }); }; + /** + * Used for interaction from the visualizations. Will open split view in compact mode. + */ onSplitOpen = (panelType: string) => { return async (options?: SplitOpenOptions) => { - this.props.splitOpen(options); + this.props.splitOpen(options ? { ...options, compact: true } : options); if (options && this.props.datasourceInstance) { const target = (await getDataSourceSrv().get(options.datasourceUid)).type; const source = @@ -573,6 +578,8 @@ export class Explore extends PureComponent { correlationEditorHelperData, showQueryInspector, setShowQueryInspector, + splitted, + compact, } = this.props; const { contentOutlineVisible } = this.state; const styles = getStyles(theme); @@ -614,7 +621,7 @@ export class Explore extends PureComponent { }} >
- {contentOutlineVisible && ( + {contentOutlineVisible && !compact && ( )} { mergeSingleChild={true} > - + {!splitted && } {correlationsBox} - + + this.props.changeCompactMode(this.props.exploreId, false) + } + /> size && updateSplitSize(size)} > - {panes.map(([exploreId]) => { + {panes.map(([exploreId, pane]) => { return ( - + {pane.initialized ? ( + + ) : ( + + )} ); })} diff --git a/public/app/features/explore/QueryRows.test.tsx b/public/app/features/explore/QueryRows.test.tsx index 03989ec2950..cd11f943811 100644 --- a/public/app/features/explore/QueryRows.test.tsx +++ b/public/app/features/explore/QueryRows.test.tsx @@ -79,7 +79,7 @@ describe('Explore QueryRows', () => { render( - + ); @@ -100,7 +100,7 @@ describe('Explore QueryRows', () => { render( - + ); @@ -117,7 +117,7 @@ describe('Explore QueryRows', () => { render( - + ); diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 084c180544f..7e74a736f8b 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react'; import { CoreApp, getNextRefId } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { DataQuery, DataSourceRef } from '@grafana/schema'; +import { ExploreItemState } from 'app/types/explore'; import { useDispatch, useSelector } from 'app/types/store'; import { getDatasourceSrv } from '../plugins/datasource_srv'; @@ -16,23 +17,25 @@ import { getExploreItemSelector } from './state/selectors'; interface Props { exploreId: string; + changeCompactMode: (compact: boolean) => void; + isOpen?: boolean; } const makeSelectors = (exploreId: string) => { const exploreItemSelector = getExploreItemSelector(exploreId); return { - getQueries: createSelector(exploreItemSelector, (s) => s!.queries), - getQueryResponse: createSelector(exploreItemSelector, (s) => s!.queryResponse), - getHistory: createSelector(exploreItemSelector, (s) => s!.history), - getEventBridge: createSelector(exploreItemSelector, (s) => s!.eventBridge), + getQueries: createSelector(exploreItemSelector, (s: ExploreItemState | undefined) => s!.queries), + getQueryResponse: createSelector(exploreItemSelector, (s: ExploreItemState | undefined) => s!.queryResponse), + getHistory: createSelector(exploreItemSelector, (s: ExploreItemState | undefined) => s!.history), + getEventBridge: createSelector(exploreItemSelector, (s: ExploreItemState | undefined) => s!.eventBridge), getDatasourceInstanceSettings: createSelector( exploreItemSelector, - (s) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.uid)! + (s: ExploreItemState | undefined) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.uid)! ), }; }; -export const QueryRows = ({ exploreId }: Props) => { +export const QueryRows = ({ exploreId, isOpen, changeCompactMode }: Props) => { const dispatch = useDispatch(); const { getQueries, getDatasourceInstanceSettings, getQueryResponse, getHistory, getEventBridge } = useMemo( () => makeSelectors(exploreId), @@ -86,6 +89,12 @@ export const QueryRows = ({ exploreId }: Props) => { reportInteraction('grafana_query_row_toggle', queryStatus === undefined ? {} : { queryEnabled: queryStatus }); }; + const onQueryOpenChanged = () => { + // Disables compact mode when query is opened. + // Compact mode can also be disabled by opening Content Outline. + changeCompactMode(false); + }; + return ( { onQueryRemoved={onQueryRemoved} onQueryToggled={onQueryToggled} onQueryReplacedFromLibrary={onQueryReplacedFromLibrary} + onQueryOpenChanged={onQueryOpenChanged} data={queryResponse} app={CoreApp.Explore} history={history} eventBus={eventBridge} + isOpen={isOpen} queryRowWrapper={(children, refId) => ( { + Object.entries(urlState.panes).map(([exploreId, { datasource, queries, range, panelsState, compact }]) => { return getPaneDatasource(datasource, queries, orgId).then((paneDatasource) => { return Promise.resolve( // Given the Grafana datasource will always be present, this should always be defined. @@ -64,13 +64,13 @@ export function initializeFromURL( ]; } - return { exploreId, range, panelsState, queries: validQueries, datasource: paneDatasource }; + return { exploreId, compact, range, panelsState, queries: validQueries, datasource: paneDatasource }; }); }); }) ).then(async (panes) => { const initializedPanes = await Promise.all( - panes.map(({ exploreId, range, panelsState, queries, datasource }) => { + panes.map(({ exploreId, range, panelsState, queries, datasource, compact }) => { return dispatch( initializeExplore({ exploreId, @@ -79,6 +79,7 @@ export function initializeFromURL( range: fromURLRange(range), panelsState, eventBridge: new EventBusSrv(), + compact: !!compact, }) ).unwrap(); }) diff --git a/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts b/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts index 9485eef2446..c746d767048 100644 --- a/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts +++ b/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts @@ -5,7 +5,7 @@ import { MutableRefObject } from 'react'; import { UrlQueryMap } from '@grafana/data'; import { LocationService } from '@grafana/runtime'; import { changeDatasource } from 'app/features/explore/state/datasource'; -import { changePanelsStateAction } from 'app/features/explore/state/explorePane'; +import { changeCompactModeAction, changePanelsStateAction } from 'app/features/explore/state/explorePane'; import { splitClose, splitOpen } from 'app/features/explore/state/main'; import { runQueries } from 'app/features/explore/state/query'; import { changeRangeAction } from 'app/features/explore/state/time'; @@ -21,6 +21,7 @@ We want to update the URL when: - range is changed - panel state is updated - a datasource change has completed. + - compact mode changes Note: Changing datasource causes a bunch of actions to be dispatched, we want to update the URL only when the change set has completed. This is done by checking if the changeDatasource.pending action @@ -37,6 +38,7 @@ export function syncToURLPredicate(paused: MutableRefObject, action: Ac changeRangeAction.type, changePanelsStateAction.type, changeDatasource.fulfilled.type, + changeCompactModeAction.type, ].includes(action.type) && !paused.current ); } diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index c9fc652279f..8cc977a6717 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -46,6 +46,12 @@ export interface ChangeSizePayload { export const changeSizeAction = createAction('explore/changeSize'); +interface ChangeCompactModePayload { + exploreId: string; + compact: boolean; +} +export const changeCompactModeAction = createAction('explore/changeCompactMode'); + /** * Tracks the state of explore panels that gets synced with the url. */ @@ -101,6 +107,7 @@ interface InitializeExplorePayload { range: TimeRange; history: HistoryItem[]; datasourceInstance?: DataSourceApi; + compact: boolean; eventBridge: EventBusExtended; } @@ -127,6 +134,10 @@ export function changeSize(exploreId: string, { width }: { width: number }): Pay return changeSizeAction({ exploreId, width }); } +export function changeCompactMode(exploreId: string, compact: boolean): PayloadAction { + return changeCompactModeAction({ exploreId, compact }); +} + export interface InitializeExploreOptions { exploreId: string; datasource: DataSourceRef | string | undefined; @@ -136,6 +147,7 @@ export interface InitializeExploreOptions { correlationHelperData?: ExploreCorrelationHelperData; position?: number; eventBridge: EventBusExtended; + compact: boolean; } /** @@ -155,6 +167,7 @@ export const initializeExplore = createAsyncThunk( queries, range, panelsState, + compact, correlationHelperData, eventBridge, }: InitializeExploreOptions, @@ -177,6 +190,7 @@ export const initializeExplore = createAsyncThunk( range: getRange(range, getTimeZone(getState().user)), datasourceInstance: instance, history, + compact, eventBridge, }) ); @@ -226,6 +240,11 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac return { ...state, containerWidth }; } + if (changeCompactModeAction.match(action)) { + const compact = action.payload.compact; + return { ...state, compact }; + } + if (changePanelsStateAction.match(action)) { const { panelsState } = action.payload; return { ...state, panelsState }; @@ -244,7 +263,7 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac } if (initializeExploreAction.match(action)) { - const { queries, range, datasourceInstance, history, eventBridge } = action.payload; + const { queries, range, datasourceInstance, history, eventBridge, compact } = action.payload; return { ...state, @@ -258,6 +277,7 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac queryResponse: createEmptyQueryResponse(), cache: [], correlations: [], + compact, }; } diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index d64ec7eca95..9d1304f8808 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -96,6 +96,7 @@ export const splitOpen = createAsyncThunk( panelsState: options?.panelsState || originState?.panelsState, correlationHelperData: options?.correlationHelperData, eventBridge: new EventBusSrv(), + compact: !!options?.compact, }) ); diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 4aba4a85f35..89ee58869af 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -75,6 +75,7 @@ export const makeExplorePaneState = (overrides?: Partial): Exp supplementaryQueries: loadSupplementaryQueries(), panelsState: {}, correlations: undefined, + compact: false, ...overrides, }); diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 5e1a6c845f6..352451481d0 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -64,9 +64,11 @@ export interface Props { onQueryCopied?: () => void; onQueryRemoved?: () => void; onQueryToggled?: (queryStatus?: boolean | undefined) => void; + onQueryOpenChanged?: (status?: boolean | undefined) => void; onQueryReplacedFromLibrary?: () => void; collapsable?: boolean; hideRefId?: boolean; + isOpen?: boolean; } interface State { @@ -433,7 +435,7 @@ export class QueryEditorRow extends PureComponent extends PureComponent
diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index a9b9d9c0186..b1cc3e70ba2 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -37,9 +37,11 @@ export interface Props { onQueryCopied?: () => void; onQueryRemoved?: () => void; onQueryToggled?: (queryStatus?: boolean | undefined) => void; + onQueryOpenChanged?: (status?: boolean | undefined) => void; onUpdateDatasources?: (datasource: DataSourceRef) => void; onQueryReplacedFromLibrary?: () => void; queryRowWrapper?: (children: ReactNode, refId: string) => ReactNode; + isOpen?: boolean; } export class QueryEditorRows extends PureComponent { @@ -173,8 +175,10 @@ export class QueryEditorRows extends PureComponent { onQueryCopied, onQueryRemoved, onQueryToggled, + onQueryOpenChanged, onQueryReplacedFromLibrary, queryRowWrapper, + isOpen, } = this.props; return ( @@ -206,12 +210,14 @@ export class QueryEditorRows extends PureComponent { onQueryCopied={onQueryCopied} onQueryRemoved={onQueryRemoved} onQueryToggled={onQueryToggled} + onQueryOpenChanged={onQueryOpenChanged} onQueryReplacedFromLibrary={onQueryReplacedFromLibrary} queries={queries} app={app} range={getTimeSrv().timeRange()} history={history} eventBus={eventBus} + isOpen={isOpen} /> ); diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 11ac0ad84d0..8c6912676da 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -229,6 +229,11 @@ export interface ExploreItemState { correlationEditorHelperData?: ExploreCorrelationHelperData; correlations?: CorrelationData[]; + + /** + * If set to true, all query rows will be collapsed initially and the content outline will be hidden + */ + compact: boolean; } export interface ExploreUpdateState { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a36e09b69c4..bdb0859ba38 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -7006,6 +7006,9 @@ "pro-tip-define-sources-through-configuration-files": " ProTip: You can also define data sources through configuration files. " } }, + "pane": { + "loading-placeholder": "Loading..." + }, "prev": "Prev", "queryless-apps-extensions": { "aria-label-go-queryless": "Go queryless" From 664188b7e0fea3b27e2bdf28ff221ae432628f37 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 30 Jul 2025 09:15:01 +0100 Subject: [PATCH 026/126] Chore: Remove `panels-suite` from cypress tests (#108709) remove panels-suite from cypress tests --- .github/CODEOWNERS | 12 ++ .github/workflows/pr-e2e-tests.yml | 2 - .../panels-suite/datagrid-data-change.spec.ts | 56 ++++++ .../datagrid-editing-features.spec.ts | 183 ++++++++++++++++++ e2e/panels-suite/dashlist.spec.ts | 41 ---- e2e/panels-suite/datagrid-data-change.spec.ts | 36 ---- .../datagrid-editing-features.spec.ts | 159 --------------- .../frontend-sandbox-panel.spec.ts | 135 ------------- e2e/panels-suite/geomap-layer-types.spec.ts | 99 ---------- e2e/panels-suite/geomap-map-controls.spec.ts | 70 ------- ...eomap-spatial-operations-transform.spec.ts | 98 ---------- e2e/panels-suite/panelEdit_base.spec.ts | 109 ----------- e2e/panels-suite/panelEdit_queries.spec.ts | 99 ---------- e2e/panels-suite/panelEdit_transforms.spec.ts | 28 --- 14 files changed, 251 insertions(+), 876 deletions(-) create mode 100644 e2e-playwright/panels-suite/datagrid-data-change.spec.ts create mode 100644 e2e-playwright/panels-suite/datagrid-editing-features.spec.ts delete mode 100644 e2e/panels-suite/dashlist.spec.ts delete mode 100644 e2e/panels-suite/datagrid-data-change.spec.ts delete mode 100644 e2e/panels-suite/datagrid-editing-features.spec.ts delete mode 100644 e2e/panels-suite/frontend-sandbox-panel.spec.ts delete mode 100644 e2e/panels-suite/geomap-layer-types.spec.ts delete mode 100644 e2e/panels-suite/geomap-map-controls.spec.ts delete mode 100644 e2e/panels-suite/geomap-spatial-operations-transform.spec.ts delete mode 100644 e2e/panels-suite/panelEdit_base.spec.ts delete mode 100644 e2e/panels-suite/panelEdit_queries.spec.ts delete mode 100644 e2e/panels-suite/panelEdit_transforms.spec.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4cf7c2c931f..e4c805b9850 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -408,6 +408,18 @@ /e2e/cloud-plugins-suite/ @grafana/partner-datasources /e2e-playwright/ @grafana/grafana-frontend-platform /e2e-playwright/dashboard-new-layouts @grafana/dashboards-squad +/e2e-playwright/panels-suite/dashlist.spec.ts @grafana/grafana-search-navigate-organise +/e2e-playwright/panels-suite/datagrid-data-change.spec.ts @grafana/dataviz-squad +/e2e-playwright/panels-suite/datagrid-editing-features.spec.ts @grafana/dataviz-squad +/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts @grafana/plugins-platform-frontend +/e2e-playwright/panels-suite/geomap-layer-types.spec.ts @grafana/dataviz-squad +/e2e-playwright/panels-suite/geomap-map-controls.spec.ts @grafana/dataviz-squad +/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts @grafana/dataviz-squad +/e2e-playwright/panels-suite/panelEdit_base.spec.ts @grafana/dashboards-squad +/e2e-playwright/panels-suite/panelEdit_queries.spec.ts @grafana/dashboards-squad +/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts @grafana/datapro +/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @grafana/dataviz-squad +/e2e-playwright/panels-suite/table-sparkline.spec.ts @grafana/dataviz-squad /e2e-playwright/plugin-e2e/ @grafana/oss-big-tent @grafana/partner-datasources /e2e-playwright/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend /e2e-playwright/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index ce7703d2b76..bbae7d86c65 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -143,8 +143,6 @@ jobs: path: e2e/various-suite - suite: dashboards-suite path: e2e/dashboards-suite - - suite: panels-suite - path: e2e/panels-suite - suite: various-suite (old arch) path: e2e/old-arch/various-suite flags: --flags="--env dashboardScene=false" diff --git a/e2e-playwright/panels-suite/datagrid-data-change.spec.ts b/e2e-playwright/panels-suite/datagrid-data-change.spec.ts new file mode 100644 index 00000000000..0b996d82381 --- /dev/null +++ b/e2e-playwright/panels-suite/datagrid-data-change.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b'; +const DATAGRID_SELECT_SERIES = 'Datagrid Select series'; + +test.use({ + featureToggles: { + enableDatagridEditing: true, + }, +}); + +// TODO enable this test when panel goes live +test.describe.skip( + 'Datagrid data changes', + { + tag: ['@panels'], + }, + () => { + test('Tests changing data in the grid', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + // Check that the data is series A + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel(DATAGRID_SELECT_SERIES) + ) + ).toBeVisible(); + await expect(page.getByTestId('glide-cell-2-0')).toHaveText('1'); + await expect(page.getByTestId('glide-cell-2-1')).toHaveText('20'); + await expect(page.getByTestId('glide-cell-2-2')).toHaveText('90'); + + // Change the series to B + const seriesInput = dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(DATAGRID_SELECT_SERIES)) + .locator('input'); + await seriesInput.fill('B'); + await seriesInput.press('Enter'); + await expect(page.getByTestId('glide-cell-2-3')).toHaveText('30'); + await expect(page.getByTestId('glide-cell-2-4')).toHaveText('40'); + await expect(page.getByTestId('glide-cell-2-5')).toHaveText('50'); + + // Edit datagrid which triggers a snapshot query + await page.locator('.dvn-scroller').click({ position: { x: 200, y: 100 } }); + await expect(page.getByTestId('glide-cell-2-1')).toHaveAttribute('aria-selected', 'true'); + await page.keyboard.type('12'); + await page.keyboard.press('Enter'); + + await page.getByTestId('data-testid Confirm Modal Danger Button').click(); + + await expect(page.getByTestId('query-editor-row')).toContainText('Snapshot'); + }); + } +); diff --git a/e2e-playwright/panels-suite/datagrid-editing-features.spec.ts b/e2e-playwright/panels-suite/datagrid-editing-features.spec.ts new file mode 100644 index 00000000000..8fb7a9eab81 --- /dev/null +++ b/e2e-playwright/panels-suite/datagrid-editing-features.spec.ts @@ -0,0 +1,183 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b'; +const DATAGRID_CANVAS = 'data-grid-canvas'; + +test.use({ + featureToggles: { + enableDatagridEditing: true, + }, +}); + +// TODO enable this test when panel goes live +test.describe.skip( + 'Datagrid data changes', + { + tag: ['@panels'], + }, + () => { + test('Tests changing data in the grid', async ({ gotoDashboardPage, selectors, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + // Edit datagrid which triggers a snapshot query + await page.locator('.dvn-scroller').click({ position: { x: 200, y: 100 } }); + await expect(page.getByTestId('glide-cell-2-1')).toHaveAttribute('aria-selected', 'true'); + await page.keyboard.type('123'); + await page.keyboard.press('Enter'); + + await page.getByTestId('data-testid Confirm Modal Danger Button').click(); + + // Delete a cell + await page.locator('.dvn-scroller').click({ position: { x: 200, y: 200 } }); + await page.keyboard.press('Delete'); + await expect(page.getByTestId('glide-cell-2-4')).toHaveText('0'); + + // Delete a selection + await page.locator('.dvn-scroller').click({ position: { x: 50, y: 100 }, modifiers: ['Shift'] }); + await page.keyboard.press('Delete'); + await expect(page.getByTestId('glide-cell-2-3')).toHaveText('0'); + await expect(page.getByTestId('glide-cell-2-2')).toHaveText('0'); + await expect(page.getByTestId('glide-cell-2-1')).toHaveText('0'); + await expect(page.getByTestId('glide-cell-2-0')).toHaveText('1'); + await expect(page.getByTestId('glide-cell-1-3')).toHaveText(''); + await expect(page.getByTestId('glide-cell-1-2')).toHaveText(''); + await expect(page.getByTestId('glide-cell-1-1')).toHaveText(''); + await expect(page.getByTestId('glide-cell-1-0')).not.toHaveText(''); + + // Clear column through context menu + await page.locator('.dvn-scroller').click({ button: 'right', position: { x: 200, y: 100 } }); + await page.locator('[aria-label="Context menu"]').click({ position: { x: 100, y: 120 } }); // click clear column + await expect(page.getByTestId('glide-cell-2-0')).toHaveText('0'); + await expect(page.getByTestId('glide-cell-2-4')).toHaveText('0'); + + // Clear row through context menu + await page.locator('.dvn-scroller').click({ position: { x: 200, y: 220 } }); + await page.keyboard.type('1123'); + await page.keyboard.press('Enter'); + await page.locator('.dvn-scroller').click({ button: 'right', position: { x: 200, y: 220 } }); + await page.locator('[aria-label="Context menu"]').click({ position: { x: 100, y: 100 } }); // click clear row + await expect(page.getByTestId('glide-cell-1-4')).toHaveText(''); + await expect(page.getByTestId('glide-cell-2-4')).toHaveText('0'); + + // get the data back + await page.reload(); + + // Clear row through row selector + await page.locator('.dvn-scroller').click({ position: { x: 20, y: 190 } }); + await page.locator('.dvn-scroller').click({ position: { x: 20, y: 90 }, modifiers: ['Shift'] }); // with shift to select all rows between clicks + await page.keyboard.press('Delete'); + await page.getByTestId('data-testid Confirm Modal Danger Button').click(); + await expect(page.getByTestId('glide-cell-1-4')).toHaveText(''); + await expect(page.getByTestId('glide-cell-1-3')).toHaveText(''); + await expect(page.getByTestId('glide-cell-1-2')).toHaveText(''); + await expect(page.getByTestId('glide-cell-1-1')).toHaveText(''); + await expect(page.getByTestId('glide-cell-2-4')).toHaveText('0'); + await expect(page.getByTestId('glide-cell-2-3')).toHaveText('0'); + await expect(page.getByTestId('glide-cell-2-2')).toHaveText('0'); + await expect(page.getByTestId('glide-cell-2-1')).toHaveText('0'); + await page.reload(); + await page.locator('.dvn-scroller').click({ position: { x: 20, y: 190 } }); + await page.locator('.dvn-scroller').click({ position: { x: 20, y: 90 }, modifiers: ['Meta'] }); // with cmd to select only clicked rows + await page.keyboard.press('Delete'); + + await page.getByTestId('data-testid Confirm Modal Danger Button').click(); + + await expect(page.getByTestId('glide-cell-1-1')).toHaveText(''); + await expect(page.getByTestId('glide-cell-2-1')).toHaveText('0'); + await expect(page.getByTestId('glide-cell-2-4')).toHaveText('0'); + await expect(page.getByTestId('glide-cell-1-4')).toHaveText(''); + + // Remove all data + await page.locator('.dvn-scroller').click({ button: 'right', position: { x: 100, y: 100 } }); + await page.locator('body').click({ position: { x: 150, y: 420 } }); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('th')).toHaveCount(0); + + await page.reload(); + + // Delete column through header dropdown menu + await page.locator('.dvn-scroller').click({ position: { x: 250, y: 15 } }); // click header dropdown + await page.locator('body').click({ position: { x: 450, y: 420 } }); // click delete column + await page.getByTestId('data-testid Confirm Modal Danger Button').click(); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('th')).toHaveCount(1); + + // Delete row through context menu + await page.locator('.dvn-scroller').click({ button: 'right', position: { x: 100, y: 100 } }); + await page.locator('[aria-label="Context menu"]').click({ position: { x: 10, y: 10 } }); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('tbody tr')).toHaveCount(6); // there are 5 data rows + 1 for the add new row btns + + // Delete rows through row selector + await page.locator('.dvn-scroller').click({ position: { x: 20, y: 190 } }); + await page.locator('.dvn-scroller').click({ position: { x: 20, y: 90 }, modifiers: ['Shift'] }); // with shift to select all rows between clicks + await page.locator('.dvn-scroller').click({ button: 'right', position: { x: 100, y: 100 } }); + await page.locator('[aria-label="Context menu"]').click({ position: { x: 10, y: 10 } }); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('tbody tr')).toHaveCount(2); // there are 1 data rows + 1 for the add new row btns + await page.reload(); + await page.locator('.dvn-scroller').click({ position: { x: 20, y: 190 } }); + await page.locator('.dvn-scroller').click({ position: { x: 20, y: 90 }, modifiers: ['Meta'] }); // with cmd to select only clicked rows + await page.locator('.dvn-scroller').click({ button: 'right', position: { x: 40, y: 90 } }); + await page.locator('[aria-label="Context menu"]').click({ position: { x: 10, y: 10 } }); + await page.getByTestId('data-testid Confirm Modal Danger Button').click(); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('tbody tr')).toHaveCount(5); // there are 5 data rows + 1 for the add new row btns + + // Delete column through context menu + await page.locator('.dvn-scroller').click({ button: 'right', position: { x: 100, y: 100 } }); + await page.locator('[aria-label="Context menu"]').click({ position: { x: 10, y: 50 } }); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('th')).toHaveCount(1); + + await page.reload(); + + // Add a new column + await page.locator('body').click({ position: { x: 350, y: 200 } }); + await page.keyboard.type('New Column'); + await page.keyboard.press('Enter'); + await page.getByTestId('data-testid Confirm Modal Danger Button').click(); + await page.locator('body').click({ position: { x: 350, y: 230 } }); + await page.keyboard.type('Value 1'); + await page.keyboard.press('Enter'); + await page.keyboard.type('Value 2'); + await page.keyboard.press('Enter'); + await page.keyboard.type('Value 3'); + await page.keyboard.press('Enter'); + await page.keyboard.type('Value 4'); + await page.keyboard.press('Enter'); + await page.keyboard.type('Value 5'); + await page.keyboard.press('Enter'); + await page.keyboard.type('Value 6'); + await page.keyboard.press('Enter'); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('th')).toHaveCount(3); + + // Rename a column + await page.locator('.dvn-scroller').click({ position: { x: 250, y: 15 } }); // click header dropdown + await page.locator('body').click({ position: { x: 450, y: 380 } }); + await page.keyboard.press('Control+a'); // select all + await page.keyboard.press('Backspace'); + await page.keyboard.type('Renamed column'); + await page.keyboard.press('Enter'); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('th')).toContainText('Renamed column'); + + // Change column field type + await page.locator('.dvn-scroller').click({ position: { x: 310, y: 15 } }); + await page.locator('[aria-label="Context menu"]').click({ position: { x: 50, y: 50 } }); + await page.locator('.dvn-scroller').click({ position: { x: 200, y: 100 } }); + await page.keyboard.type('Str Value'); + await page.keyboard.press('Enter'); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('tr')).toContainText('Str Value'); + + // Select all rows through row selection + await page.locator('.dvn-scroller').click({ position: { x: 10, y: 10 } }); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('[aria-selected="true"]')).toHaveCount(6); + + // Add a new row + await page.locator('.dvn-scroller').click({ position: { x: 200, y: 250 } }); + await page.keyboard.type('123'); + await page.locator('.dvn-scroller').click({ position: { x: 50, y: 250 } }); + await page.keyboard.type('Val'); + await page.keyboard.press('Enter'); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('tbody tr')).toContainText('Val'); + await expect(page.getByTestId(DATAGRID_CANVAS).locator('tbody tr')).toHaveCount(8); + }); + } +); diff --git a/e2e/panels-suite/dashlist.spec.ts b/e2e/panels-suite/dashlist.spec.ts deleted file mode 100644 index 002cf235e82..00000000000 --- a/e2e/panels-suite/dashlist.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { e2e } from '../utils'; -const PAGE_UNDER_TEST = 'a6801696-cc53-4196-b1f9-2403e3909185/panel-tests-dashlist-variables'; - -describe('DashList panel', () => { - beforeEach(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); - }); - - // this is to prevent the fix for https://github.com/grafana/grafana/issues/76800 from regressing - it('should pass current variable values correctly when `Include current template variable values` is set', () => { - e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST }); - - // check the initial value of the urls contain the variable value correctly - e2e.components.Panels.Panel.title('Include time range and variables enabled') - .should('be.visible') - .within(() => { - cy.get('a').each(($el) => { - cy.wrap($el).should('have.attr', 'href').and('contain', 'var-server=A'); - }); - }); - - // update variable to b - e2e.pages.Dashboard.SubMenu.submenuItemLabels('server') - .parent() - .within(() => { - cy.get('input').click(); - }); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').click(); - // blur the dropdown - cy.get('body').click(); - - // check the urls are updated with the new variable value - e2e.components.Panels.Panel.title('Include time range and variables enabled') - .should('be.visible') - .within(() => { - cy.get('a').each(($el) => { - cy.wrap($el).should('have.attr', 'href').and('contain', 'var-server=B'); - }); - }); - }); -}); diff --git a/e2e/panels-suite/datagrid-data-change.spec.ts b/e2e/panels-suite/datagrid-data-change.spec.ts deleted file mode 100644 index 583f30e267f..00000000000 --- a/e2e/panels-suite/datagrid-data-change.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { e2e } from '../utils'; - -const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b'; -const DATAGRID_SELECT_SERIES = 'Datagrid Select series'; - -//TODO enable this test when panel goes live -describe.skip('Datagrid data changes', () => { - beforeEach(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); - }); - - it('Tests changing data in the grid', () => { - e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - - // Check that the data is series A - e2e.components.PanelEditor.OptionsPane.fieldLabel(DATAGRID_SELECT_SERIES).should('be.visible'); - cy.get('[data-testid="glide-cell-2-0"]').should('have.text', '1'); - cy.get('[data-testid="glide-cell-2-1"]').should('have.text', '20'); - cy.get('[data-testid="glide-cell-2-2"]').should('have.text', '90'); - - // Change the series to B - e2e.components.PanelEditor.OptionsPane.fieldLabel(DATAGRID_SELECT_SERIES).find('input').type('B {enter}'); - cy.get('[data-testid="glide-cell-2-3"]').should('have.text', '30'); - cy.get('[data-testid="glide-cell-2-4"]').should('have.text', '40'); - cy.get('[data-testid="glide-cell-2-5"]').should('have.text', '50'); - - // Edit datagrid which triggers a snapshot query - cy.get('.dvn-scroller').click(200, 100); - cy.get('[data-testid="glide-cell-2-1"]').should('have.attr', 'aria-selected', 'true'); - cy.get('body').type('12{enter}', { delay: 500 }); - - cy.get('[data-testid="data-testid Confirm Modal Danger Button"]').click(); - - cy.get('[data-testid="query-editor-row"]').contains('Snapshot'); - }); -}); diff --git a/e2e/panels-suite/datagrid-editing-features.spec.ts b/e2e/panels-suite/datagrid-editing-features.spec.ts deleted file mode 100644 index e3592f60388..00000000000 --- a/e2e/panels-suite/datagrid-editing-features.spec.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { e2e } from '../utils'; - -const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b'; -const DATAGRID_CANVAS = 'data-grid-canvas'; - -//TODO enable this test when panel goes live -describe.skip('Datagrid data changes', () => { - beforeEach(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); - }); - - it('Tests changing data in the grid', () => { - e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - - // Edit datagrid which triggers a snapshot query - cy.get('.dvn-scroller').click(200, 100); - cy.get('[data-testid="glide-cell-2-1"]').should('have.attr', 'aria-selected', 'true'); - cy.get('body').type('123{enter}', { delay: 500 }); - - cy.get('[data-testid="data-testid Confirm Modal Danger Button"]').click(); - - // Delete a cell - cy.get('.dvn-scroller').click(200, 200); - cy.get('body').type('{del}'); - cy.get('[data-testid="glide-cell-2-4"]').should('have.text', 0); - - // Delete a selection - cy.get('.dvn-scroller').click(50, 100, { shiftKey: true }); - cy.get('body').type('{del}'); - cy.get('[data-testid="glide-cell-2-3"]').should('have.text', 0); - cy.get('[data-testid="glide-cell-2-2"]').should('have.text', 0); - cy.get('[data-testid="glide-cell-2-1"]').should('have.text', 0); - cy.get('[data-testid="glide-cell-2-0"]').should('have.text', 1); - cy.get('[data-testid="glide-cell-1-3"]').should('have.text', ''); - cy.get('[data-testid="glide-cell-1-2"]').should('have.text', ''); - cy.get('[data-testid="glide-cell-1-1"]').should('have.text', ''); - cy.get('[data-testid="glide-cell-1-0"]').should('not.have.text', ''); - - // Clear column through context menu - cy.get('.dvn-scroller').rightclick(200, 100); - cy.get('[aria-label="Context menu"]').click(100, 120); // click clear column - cy.get('[data-testid="glide-cell-2-0"]').should('have.text', 0); - cy.get('[data-testid="glide-cell-2-4"]').should('have.text', 0); - - // Clear row through context menu - cy.get('.dvn-scroller').click(200, 220); - cy.get('body').type('1123{enter}', { delay: 500 }); - cy.get('.dvn-scroller').rightclick(200, 220); - cy.get('[aria-label="Context menu"]').click(100, 100); // click clear row - cy.get('[data-testid="glide-cell-1-4"]').should('have.text', ''); - cy.get('[data-testid="glide-cell-2-4"]').should('have.text', 0); - - // get the data back - cy.reload(); - - // Clear row through row selector - cy.get('.dvn-scroller').click(20, 190, { waitForAnimations: true }); - cy.get('.dvn-scroller').click(20, 90, { shiftKey: true, waitForAnimations: true }); // with shift to select all rows between clicks - cy.get('body').type('{del}'); - cy.get('[data-testid="data-testid Confirm Modal Danger Button"]').click(); - cy.get('[data-testid="glide-cell-1-4"]').should('have.text', ''); - cy.get('[data-testid="glide-cell-1-3"]').should('have.text', ''); - cy.get('[data-testid="glide-cell-1-2"]').should('have.text', ''); - cy.get('[data-testid="glide-cell-1-1"]').should('have.text', ''); - cy.get('[data-testid="glide-cell-2-4"]').should('have.text', 0); - cy.get('[data-testid="glide-cell-2-3"]').should('have.text', 0); - cy.get('[data-testid="glide-cell-2-2"]').should('have.text', 0); - cy.get('[data-testid="glide-cell-2-1"]').should('have.text', 0); - cy.wait(1000); - cy.reload(); - cy.get('.dvn-scroller').click(20, 190, { waitForAnimations: true }); - cy.get('.dvn-scroller').click(20, 90, { commandKey: true, waitForAnimations: true }); // with cmd to select only clicked rows - cy.get('body').type('{del}'); - - cy.get('[data-testid="data-testid Confirm Modal Danger Button"]').click(); - - cy.get('[data-testid="glide-cell-1-1"]').should('have.text', ''); - cy.get('[data-testid="glide-cell-2-1"]').should('have.text', 0); - cy.get('[data-testid="glide-cell-2-4"]').should('have.text', 0); - cy.get('[data-testid="glide-cell-1-4"]').should('have.text', ''); - - // Remove all data - cy.get('.dvn-scroller').rightclick(100, 100); - cy.get('body').click(150, 420); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] th`).should('have.length', 0); - - cy.reload(); - - // Delete column through header dropdown menu - cy.get('.dvn-scroller').click(250, 15); // click header dropdown - cy.get('body').click(450, 420); // click delete column - cy.get('[data-testid="data-testid Confirm Modal Danger Button"]').click(); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] th`).should('have.length', 1); - - // Delete row through context menu - cy.get('.dvn-scroller').rightclick(100, 100); - cy.get('[aria-label="Context menu"]').click(10, 10); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] tbody tr`).should('have.length', 6); // there are 5 data rows + 1 for the add new row btns - - // Delete rows through row selector - cy.get('.dvn-scroller').click(20, 190, { waitForAnimations: true }); - cy.get('.dvn-scroller').click(20, 90, { shiftKey: true, waitForAnimations: true }); // with shift to select all rows between clicks - cy.get('.dvn-scroller').rightclick(100, 100); - cy.get('[aria-label="Context menu"]').click(10, 10); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] tbody tr`).should('have.length', 2); // there are 1 data rows + 1 for the add new row btns - cy.reload(); - cy.get('.dvn-scroller').click(20, 190, { waitForAnimations: true }); - cy.get('.dvn-scroller').click(20, 90, { commandKey: true, waitForAnimations: true }); // with shift to select all rows between clicks - cy.get('.dvn-scroller').rightclick(40, 90); - cy.get('[aria-label="Context menu"]').click(10, 10); - cy.get('[data-testid="data-testid Confirm Modal Danger Button"]').click(); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] tbody tr`).should('have.length', 5); // there are 5 data rows + 1 for the add new row btns - - // Delete column through context menu - cy.get('.dvn-scroller').rightclick(100, 100); - cy.get('[aria-label="Context menu"]').click(10, 50); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] th`).should('have.length', 1); - - cy.reload(); - cy.wait(3000); - - // Add a new column - cy.get('body').click(350, 200).type('New Column{enter}'); - cy.get('[data-testid="data-testid Confirm Modal Danger Button"]').click(); - cy.get('body') - .click(350, 230) - .type('Value 1{enter}') - .type('Value 2{enter}') - .type('Value 3{enter}') - .type('Value 4{enter}') - .type('Value 5{enter}') - .type('Value 6{enter}'); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] th`).should('have.length', 3); - - // Rename a column - cy.get('.dvn-scroller').click(250, 15); // click header dropdown - cy.get('body').click(450, 380).type('{selectall}{backspace}Renamed column{enter}'); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] th`).contains('Renamed column'); - - // Change column field type - cy.get('.dvn-scroller').click(310, 15); - cy.get('[aria-label="Context menu"]').click(50, 50); - cy.get('.dvn-scroller').click(200, 100); - cy.get('body').type('Str Value{enter}'); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] tr`).contains('Str Value'); - - // Select all rows through row selection - cy.get('.dvn-scroller').click(10, 10, { waitForAnimations: true }); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] [aria-selected="true"]`).should('have.length', 6); - - // Add a new row - cy.get('.dvn-scroller').click(200, 250); - cy.get('body').type('123'); - cy.get('.dvn-scroller').click(50, 250); - cy.get('body').type('Val{enter}'); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] tbody tr`).contains('Val'); - cy.get(`[data-testid="${DATAGRID_CANVAS}"] tbody tr`).should('have.length', 8); - }); -}); diff --git a/e2e/panels-suite/frontend-sandbox-panel.spec.ts b/e2e/panels-suite/frontend-sandbox-panel.spec.ts deleted file mode 100644 index 4f2f0e3a192..00000000000 --- a/e2e/panels-suite/frontend-sandbox-panel.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -import panelSandboxDashboard from '../dashboards/PanelSandboxDashboard.json'; -import { e2e } from '../utils'; - -const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950'; -// Skipping due to race conditions with same old arch test e2e/panels-suite/frontend-sandbox-panel.spec.ts -describe('Panel sandbox', () => { - beforeEach(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true); - return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true); - }); - - describe('Sandbox disabled', () => { - beforeEach(() => { - cy.window().then((win) => { - win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); - }); - cy.reload(); - }); - - it('Add iframes to body', () => { - // this button adds iframes to the body - cy.get('[data-testid="button-create-iframes"]').click(); - - const iframeIds = [ - 'createElementIframe', - 'innerHTMLIframe', - 'appendIframe', - 'prependIframe', - 'afterIframe', - 'beforeIframe', - 'outerHTMLIframe', - 'parseFromStringIframe', - 'insertBeforeIframe', - 'replaceChildIframe', - ]; - iframeIds.forEach((id) => { - cy.get(`#${id}`).should('exist'); - }); - }); - - it('Reaches out of panel div', () => { - // this button reaches out of the panel div and modifies the element dataset - cy.get('[data-testid="button-reach-out"]').click(); - - cy.get('[data-sandbox-test="true"]').should('exist'); - }); - - it('Reaches out of the panel editor', () => { - e2e.flows.openDashboard({ - uid: DASHBOARD_ID, - queryParams: { - editPanel: 1, - }, - }); - - cy.get('[data-testid="panel-editor-custom-editor-input"]').should('not.be.disabled'); - cy.get('[data-testid="panel-editor-custom-editor-input"]').should('have.value', ''); - // wait because sometimes cypress is faster than react and the value doesn't change - cy.wait(1000); - cy.get('[data-testid="panel-editor-custom-editor-input"]').type('x', { force: true, delay: 500 }); - cy.wait(100); // small delay to prevent false positives from too fast tests - cy.get('[data-testid="panel-editor-custom-editor-input"]').should('have.value', 'x'); - cy.get('[data-sandbox-test="panel-editor"]').should('exist'); - }); - }); - - describe('Sandbox enabled', () => { - beforeEach(() => { - cy.window().then((win) => { - win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); - }); - cy.reload(); - }); - - it('Does not add iframes to body', () => { - // this button adds 3 iframes to the body - cy.get('[data-testid="button-create-iframes"]').click(); - cy.wait(100); // small delay to prevent false positives from too fast tests - - const iframeIds = [ - 'createElementIframe', - 'innerHTMLIframe', - 'appendIframe', - 'prependIframe', - 'afterIframe', - 'beforeIframe', - 'outerHTMLIframe', - 'parseFromStringIframe', - 'insertBeforeIframe', - 'replaceChildIframe', - ]; - iframeIds.forEach((id) => { - cy.get(`#${id}`).should('not.exist'); - }); - }); - - it('Does not reaches out of panel div', () => { - // this button reaches out of the panel div and modifies the element dataset - cy.get('[data-testid="button-reach-out"]').click(); - cy.wait(100); // small delay to prevent false positives from too fast tests - cy.get('[data-sandbox-test="true"]').should('not.exist'); - }); - - it('Does not Reaches out of the panel editor', () => { - e2e.flows.openDashboard({ - uid: DASHBOARD_ID, - queryParams: { - editPanel: 1, - }, - }); - - cy.get('[data-testid="panel-editor-custom-editor-input"]').should('not.be.disabled'); - // wait because sometimes cypress is faster than react and the value doesn't change - cy.wait(1000); - cy.get('[data-testid="panel-editor-custom-editor-input"]').type('x', { force: true }); - cy.wait(100); // small delay to prevent false positives from too fast tests - cy.get('[data-sandbox-test="panel-editor"]').should('not.exist'); - }); - - it('Can access specific window global variables', () => { - cy.get('[data-testid="button-test-globals"]').click(); - cy.get('[data-sandbox-global="Prism"]').should('be.visible'); - cy.get('[data-sandbox-global="jQuery"]').should('be.visible'); - cy.get('[data-sandbox-global="location"]').should('be.visible'); - }); - }); - - afterEach(() => { - e2e.flows.revertAllChanges(); - }); - - after(() => { - return cy.clearCookies(); - }); -}); diff --git a/e2e/panels-suite/geomap-layer-types.spec.ts b/e2e/panels-suite/geomap-layer-types.spec.ts deleted file mode 100644 index 70af145c784..00000000000 --- a/e2e/panels-suite/geomap-layer-types.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { e2e } from '../utils'; - -const DASHBOARD_ID = 'P2jR04WVk'; - -const MAP_LAYERS_TYPE = 'Map layers Layer type'; -const MAP_LAYERS_DATA = 'Map layers Data'; -const MAP_LAYERS_GEOJSON = 'Map layers GeoJSON URL'; - -describe('Geomap layer types', () => { - beforeEach(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); - }); - - it('Tests changing the layer type', () => { - e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - cy.get('[data-testid="layer-drag-drop-list"]').should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).should('be.visible'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('markers'); - - // Heatmap - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Heatmap{enter}'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('heatmap'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); - - // GeoJSON - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('GeoJSON{enter}'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('geojson'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); - - // Open Street Map - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Open Street Map{enter}'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('osm-standard'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('not.exist'); - // e2e.components.PanelEditor.General.content().should('be.visible'); - - // CARTO basemap - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('CARTO basemap{enter}'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('carto'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show labels').should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Theme').should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); - - // ArcGIS MapServer - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('ArcGIS MapServer{enter}'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('esri-xyz'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Server instance').should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); - - // XYZ Tile layer - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('XYZ Tile layer{enter}'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('xyz'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers URL template').should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Attribution').should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); - }); - - it.skip('Tests changing the layer type (alpha)', () => { - e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - cy.get('[data-testid="layer-drag-drop-list"]').should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).should('be.visible'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('markers'); - - // Icon at last point (Alpha) - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Icon at last point{enter}'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('last-point-tracker'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); - - // Dynamic GeoJSON (Alpha) - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Dynamic GeoJSON{enter}'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('dynamic-geojson'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers ID Field').should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); - - // Night / Day (Alpha) - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Night / Day{enter}'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('dayNight'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show').should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Night region color').should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Display sun').should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); - - // Route (Alpha) - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Route{enter}'); - cy.get('[data-testid="layer-drag-drop-list"]').contains('route'); - e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Location').should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Style').should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Line width').should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); - }); -}); diff --git a/e2e/panels-suite/geomap-map-controls.spec.ts b/e2e/panels-suite/geomap-map-controls.spec.ts deleted file mode 100644 index b344dae5abe..00000000000 --- a/e2e/panels-suite/geomap-map-controls.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { e2e } from '../utils'; -const DASHBOARD_ID = 'P2jR04WVk'; -const TIMEOUT = 45000; - -describe('Geomap layer controls options', () => { - beforeEach(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); - }); - - it('Tests map controls options', () => { - e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - // Wait until the query editor has been loaded by ensuring that the QueryEditor select contains the text 'flight_info_by_state.csv' - e2e.components.Select.singleValue().contains('flight_info_by_state.csv').should('be.visible'); - e2e.components.OptionsGroup.group('Map controls').scrollIntoView().should('be.visible'); - - // Show zoom field - e2e.components.PanelEditor.showZoomField() - .should('be.visible') - .within(() => { - cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked'); - }); - - // Show attribution - e2e.components.PanelEditor.showAttributionField() - .should('be.visible') - .within(() => { - cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked'); - }); - - // Show scale - e2e.components.PanelEditor.showScaleField() - .should('be.visible') - .within(() => { - cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked'); - }); - - // Show measure tool - e2e.components.PanelEditor.showMeasureField() - .should('be.visible') - .within(() => { - cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked'); - }); - - // Show debug - e2e.components.PanelEditor.showDebugField() - .should('be.visible') - .within(() => { - cy.get('input[type="checkbox"]').check({ force: true }).should('be.checked'); - }); - - e2e.components.Panels.Panel.content({ timeout: TIMEOUT }) - .should('be.visible') - .within(() => { - // Verify zoom - cy.get('.ol-zoom', { timeout: TIMEOUT }).should('be.visible'); - - // Verify attribution - cy.get('.ol-attribution', { timeout: TIMEOUT }).should('be.visible'); - - // Verify scale - cy.get('.ol-scale-line', { timeout: TIMEOUT }).should('be.visible'); - - // Verify measure tool - e2e.components.PanelEditor.measureButton({ timeout: TIMEOUT }).should('be.visible'); - - // Verify debug tool - e2e.components.DebugOverlay.wrapper({ timeout: TIMEOUT }).should('be.visible'); - }); - }); -}); diff --git a/e2e/panels-suite/geomap-spatial-operations-transform.spec.ts b/e2e/panels-suite/geomap-spatial-operations-transform.spec.ts deleted file mode 100644 index 4eb24162791..00000000000 --- a/e2e/panels-suite/geomap-spatial-operations-transform.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { e2e } from '../utils'; - -const DASHBOARD_ID = 'P2jR04WVk'; - -describe.skip('Geomap spatial operations', () => { - beforeEach(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); - }); - - it('Tests location auto option', () => { - e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { '__feature.tableNextGen': false, editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); - e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); - - e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); - e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}'); - e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible'); - e2e.components.Transforms.SpatialOperations.locationLabel().type('Auto{enter}'); - - e2e.components.PanelEditor.toggleTableView().click({ force: true }); - e2e.components.Panels.Visualization.Table.header() - .should('be.visible') - .within(() => { - cy.contains('Point').should('be.visible'); - }); - }); - - it('Tests location coords option', () => { - e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { '__feature.tableNextGen': false, editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); - e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); - - e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); - e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}'); - e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible'); - e2e.components.Transforms.SpatialOperations.locationLabel().type('Coords{enter}'); - - e2e.components.Transforms.SpatialOperations.location.coords.latitudeFieldLabel().should('be.visible'); - e2e.components.Transforms.SpatialOperations.location.coords.latitudeFieldLabel().type('Lat{enter}'); - - e2e.components.Transforms.SpatialOperations.location.coords.longitudeFieldLabel().should('be.visible'); - e2e.components.Transforms.SpatialOperations.location.coords.longitudeFieldLabel().type('Lng{enter}'); - - e2e.components.PanelEditor.toggleTableView().click({ force: true }); - e2e.components.Panels.Visualization.Table.header() - .should('be.visible') - .within(() => { - cy.contains('Point').should('be.visible'); - }); - }); - - it('Tests geoshash field column appears in table view', () => { - e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { '__feature.tableNextGen': false, editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); - e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); - - e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); - e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}'); - e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible'); - e2e.components.Transforms.SpatialOperations.locationLabel().type('Geohash{enter}'); - - e2e.components.Transforms.SpatialOperations.location.geohash - .geohashFieldLabel() - .should('be.visible') - .type('State{enter}'); - - e2e.components.PanelEditor.toggleTableView().click({ force: true }); - e2e.components.Panels.Visualization.Table.header() - .should('be.visible') - .within(() => { - cy.contains('State 1').should('be.visible'); - }); - }); - - it('Tests location lookup option', () => { - e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { '__feature.tableNextGen': false, editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); - e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); - - e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); - e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}'); - e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible'); - e2e.components.Transforms.SpatialOperations.locationLabel().type('Lookup{enter}'); - - e2e.components.Transforms.SpatialOperations.location.lookup.lookupFieldLabel().should('be.visible'); - e2e.components.Transforms.SpatialOperations.location.lookup.lookupFieldLabel().type('State{enter}'); - - e2e.components.Transforms.SpatialOperations.location.lookup.gazetteerFieldLabel().should('be.visible'); - e2e.components.Transforms.SpatialOperations.location.lookup.gazetteerFieldLabel().type('USA States{enter}'); - - e2e.components.PanelEditor.toggleTableView().click({ force: true }); - e2e.components.Panels.Visualization.Table.header() - .should('be.visible') - .within(() => { - cy.contains('Geometry').should('be.visible'); - }); - }); -}); diff --git a/e2e/panels-suite/panelEdit_base.spec.ts b/e2e/panels-suite/panelEdit_base.spec.ts deleted file mode 100644 index d48879e5a8d..00000000000 --- a/e2e/panels-suite/panelEdit_base.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { selectors } from '@grafana/e2e-selectors'; - -import { e2e } from '../utils'; - -const PANEL_UNDER_TEST = 'Lines 500 data points'; - -describe('Panel edit tests', () => { - beforeEach(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); - }); - - it('Tests various Panel edit scenarios', () => { - cy.intercept({ - pathname: '/api/ds/query', - }).as('query'); - e2e.flows.openDashboard({ uid: 'TkZXxlNG3', queryParams: { '__feature.tableNextGen': false } }); - cy.wait('@query'); - - e2e.components.Panels.Panel.title('Lines 500 data points') - .should('be.visible') - .within(() => { - e2e.components.Panels.Panel.loadingBar().should('not.exist'); - cy.get('[data-testid="uplot-main-div"]').first().should('be.visible'); - }); - - e2e.flows.openPanelMenuItem(e2e.flows.PanelMenuItems.Edit, PANEL_UNDER_TEST); - - // New panel editor opens when navigating from Panel menu - e2e.components.PanelEditor.General.content().should('be.visible'); - - // Queries tab is rendered and open by default - e2e.components.PanelEditor.DataPane.content() - .scrollIntoView() - .should('be.visible') - .within(() => { - e2e.components.Tab.title('Queries').should('be.visible'); - // data should be the active tab - e2e.components.Tab.active().within((li: JQuery) => { - expect(li.text()).equals('Queries1'); // there's already a query so therefore Query + 1 - }); - cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('be.visible'); - e2e.components.TransformTab.content().should('not.exist'); - e2e.components.AlertTab.content().should('not.exist'); - e2e.components.PanelAlertTabContent.content().should('not.exist'); - - // Bottom pane tabs - // Can change to Transform tab - e2e.components.Tab.title('Transformations').should('be.visible').click(); - e2e.components.Tab.active().within((li: JQuery) => { - expect(li.text()).equals('Transformations0'); // there's no transform so therefore Transform + 0 - }); - e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible'); - cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('not.exist'); - e2e.components.AlertTab.content().should('not.exist'); - e2e.components.PanelAlertTabContent.content().should('not.exist'); - - // Can change to Alerts tab (graph panel is the default vis so the alerts tab should be rendered) - e2e.components.Tab.title('Alert').scrollIntoView().should('be.visible').click(); - e2e.components.Tab.active().should('have.text', 'Alert0'); // there's no alert so therefore Alert + 0 - - // Needs to be disabled until Grafana EE turns unified alerting on by default - // e2e.components.AlertTab.content().should('not.exist'); - - cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('not.exist'); - e2e.components.TransformTab.content().should('not.exist'); - - // Needs to be disabled until Grafana EE turns unified alerting on by default - // e2e.components.PanelAlertTabContent.content().should('exist'); - // e2e.components.PanelAlertTabContent.content().should('be.visible'); - - e2e.components.Tab.title('Queries').should('be.visible').click(); - }); - - // Check that Time series is chosen - e2e.components.PanelEditor.toggleVizPicker().click(); - e2e.components.PluginVisualization.item('Time series').should('be.visible'); - e2e.components.PluginVisualization.current().should((e) => expect(e).to.contain('Time series')); - - // Check that table view works - e2e.components.Panels.Panel.loadingBar().should('not.exist'); - e2e.components.PanelEditor.toggleTableView().click({ force: true }); - e2e.components.Panels.Visualization.Table.header() - .should('be.visible') - .within(() => { - cy.contains('A-series').should('be.visible'); - }); - - // Change to Text panel - e2e.components.PluginVisualization.item('Text').scrollIntoView().should('be.visible').click(); - e2e.components.PanelEditor.toggleVizPicker().should((e) => expect(e).to.contain('Text')); - - // Data pane should not be rendered - e2e.components.PanelEditor.DataPane.content().should('not.exist'); - - // Change to Table panel - e2e.components.PanelEditor.toggleVizPicker().click(); - e2e.components.PluginVisualization.item('Table').scrollIntoView().should('be.visible').click(); - e2e.components.PanelEditor.toggleVizPicker().should((e) => expect(e).to.contain('Table')); - - // Data pane should be rendered - e2e.components.PanelEditor.DataPane.content().should('be.visible'); - - // Field & Overrides tabs (need to switch to React based vis, i.e. Table) - e2e.components.PanelEditor.toggleTableView().click({ force: true }).click({ force: true }); - - e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Show table header').should('be.visible'); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Column width').should('be.visible'); - }); -}); diff --git a/e2e/panels-suite/panelEdit_queries.spec.ts b/e2e/panels-suite/panelEdit_queries.spec.ts deleted file mode 100644 index 1700a00ea9f..00000000000 --- a/e2e/panels-suite/panelEdit_queries.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { e2e } from '../utils'; - -const flakyTimeout = 10000; - -describe('Panel edit tests - queries', () => { - beforeEach(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); - }); - - it('Tests various Panel edit queries scenarios', () => { - e2e.flows.openDashboard({ uid: '5SdHCadmz', queryParams: { editPanel: 3 } }); - - // New panel editor opens when navigating from Panel menu - e2e.components.PanelEditor.General.content().should('be.visible'); - - // Queries tab is rendered and open by default - e2e.components.PanelEditor.DataPane.content().should('be.visible'); - - // We expect row with refId A to exist and be visible - e2e.components.QueryEditorRows.rows().within((rows) => { - expect(rows.length).equals(1); - }); - - // Add query button should be visible and clicking on it should create a new row - e2e.components.QueryTab.addQuery().scrollIntoView().should('be.visible').click(); - - // We expect row with refId A and B to exist and be visible - e2e.components.QueryEditorRows.rows({ timeout: flakyTimeout }).should('have.length', 2); - - // Remove refId A - e2e.components.QueryEditorRow.actionButton('Remove query').eq(0).scrollIntoView(); - e2e.components.QueryEditorRow.actionButton('Remove query').eq(0).should('be.visible').click(); - - // We expect row with refId B to exist and be visible - e2e.components.QueryEditorRows.rows({ timeout: flakyTimeout }).should('have.length', 1); - - // Duplicate refId B - e2e.components.QueryEditorRow.actionButton('Duplicate query').eq(0).should('be.visible').click(); - - // We expect row with refId Band and A to exist and be visible - e2e.components.QueryEditorRows.rows().should('have.length', 2); - - // Change to CSV Metric Values scenario for A - e2e.components.DataSource.TestData.QueryTab.scenarioSelectContainer() - .first() - .should('be.visible') - .within(() => { - cy.get('input[id*="test-data-scenario-select-"]').eq(0).should('be.visible').click(); - }); - - cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').eq(0).click(); - - // Disable / enable row - expectInspectorResultAndClose((keys) => { - const length = keys.length; - const resultIds = new Set([ - keys[length - 2].innerText, // last 2 - keys[length - 1].innerText, // last 2 - ]); - - expect(resultIds.has('A:')).equals(true); - expect(resultIds.has('B:')).equals(true); - }); - - // Hide response for row with refId A - e2e.components.QueryEditorRow.actionButton('Hide response').eq(1).should('be.visible').click(); - - expectInspectorResultAndClose((keys) => { - const length = keys.length; - expect(keys[length - 1].innerText).equals('B:'); - }); - - // Show response for row with refId A - e2e.components.QueryEditorRow.actionButton('Hide response').eq(1).should('be.visible').click(); - - expectInspectorResultAndClose((keys) => { - const length = keys.length; - const resultIds = new Set([ - keys[length - 2].innerText, // last 2 - keys[length - 1].innerText, // last 2 - ]); - - expect(resultIds.has('A:')).equals(true); - expect(resultIds.has('B:')).equals(true); - }); - }); -}); - -const expectInspectorResultAndClose = (expectCallBack: (keys: JQuery) => void) => { - e2e.components.QueryTab.queryInspectorButton().should('be.visible').click(); - - e2e.components.PanelInspector.Query.refreshButton().should('be.visible').click(); - - e2e.components.PanelInspector.Query.jsonObjectKeys({ timeout: flakyTimeout }) - .should('be.visible') - .should((keys) => expectCallBack(keys)); - - e2e.components.Drawer.General.close().should('be.visible').click(); -}; diff --git a/e2e/panels-suite/panelEdit_transforms.spec.ts b/e2e/panels-suite/panelEdit_transforms.spec.ts deleted file mode 100644 index 963991a22e7..00000000000 --- a/e2e/panels-suite/panelEdit_transforms.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { e2e } from '../utils'; - -describe('Panel edit tests - transformations', () => { - beforeEach(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); - }); - - it('Tests transformations editor', () => { - e2e.flows.openDashboard({ uid: 'TkZXxlNG3', queryParams: { editPanel: 47 } }); - - e2e.components.Tab.title('Transformations').should('be.visible').click(); - e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); - e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); - e2e.components.Transforms.Reduce.calculationsLabel().scrollIntoView().should('be.visible'); - e2e.components.Transforms.Reduce.modeLabel().should('be.visible'); - }); - - it('Tests case where transformations can be disabled and not clear out panel data', () => { - e2e.flows.openDashboard({ uid: 'TkZXxlNG3', queryParams: { editPanel: 47 } }); - - e2e.components.Tab.title('Transformations').should('be.visible').click(); - e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); - e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); - e2e.components.Transforms.disableTransformationButton().should('be.visible').click(); - - e2e.components.Panels.Panel.PanelDataErrorMessage().should('not.exist'); - }); -}); From 8bfaac5ec5d7d372ff22087e91d10e3078921a45 Mon Sep 17 00:00:00 2001 From: Josh Kirkwood Date: Wed, 30 Jul 2025 18:15:34 +1000 Subject: [PATCH 027/126] Docs: Updated 'Run Grafana Docker image' with minor cosmetic changes. (#108831) * Update index.md Adjusted codeblock formatting from yaml syntax rather than bash. * Ran 'yarn prettier write' in index.md to ensure formatting was correct after completing prior changes. --- .../setup-grafana/installation/docker/index.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/sources/setup-grafana/installation/docker/index.md b/docs/sources/setup-grafana/installation/docker/index.md index c1f4b2fa917..99201814d1c 100644 --- a/docs/sources/setup-grafana/installation/docker/index.md +++ b/docs/sources/setup-grafana/installation/docker/index.md @@ -220,14 +220,14 @@ To run the latest stable version of Grafana using Docker Compose, complete the f For example: - ```bash + ```yaml services: grafana: image: grafana/grafana-enterprise container_name: grafana restart: unless-stopped ports: - - '3000:3000' + - '3000:3000' ``` 1. To run `docker-compose.yaml`, run the following command: @@ -347,19 +347,19 @@ To use bind mounts, complete the following steps: The following example runs the latest stable version of Grafana, listening on port 3000, with the container named `grafana`, persistent storage in the `grafana-storage` docker volume, the server root URL set, and the official [clock panel](/grafana/plugins/grafana-clock-panel/) plugin installed. -```bash +```yaml services: grafana: image: grafana/grafana-enterprise container_name: grafana restart: unless-stopped environment: - - GF_SERVER_ROOT_URL=http://my.grafana.server/ - - GF_PLUGINS_PREINSTALL=grafana-clock-panel + - GF_SERVER_ROOT_URL=http://my.grafana.server/ + - GF_PLUGINS_PREINSTALL=grafana-clock-panel ports: - - '3000:3000' + - '3000:3000' volumes: - - 'grafana_storage:/var/lib/grafana' + - 'grafana_storage:/var/lib/grafana' volumes: grafana_storage: {} ``` From 79a9981a0b611e80332e919c1beb5e8f6c8daeca Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 08:21:53 +0000 Subject: [PATCH 028/126] I18n: Download translations from Crowdin (#108882) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/de-DE/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/es-ES/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/fr-FR/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/hu-HU/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/id-ID/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/it-IT/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/ja-JP/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/ko-KR/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/nl-NL/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/pl-PL/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/pt-BR/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/pt-PT/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/ru-RU/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/sv-SE/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/tr-TR/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/zh-Hans/grafana.json | 51 ++++++++++++++++++++++++++--- public/locales/zh-Hant/grafana.json | 51 ++++++++++++++++++++++++++--- 18 files changed, 828 insertions(+), 90 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 8a43756a876..8bda1a81127 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -3508,6 +3508,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Zrušit", "cannot-move-folders": "Složky nelze přesunout", "confirmation-text": "Odstranit", @@ -3541,6 +3542,10 @@ "this-folder-is-empty": "Tato složka je prázdná" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_few": "", "failed-alert_many": "", @@ -3550,15 +3555,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} pravidlo výstrahy", @@ -4605,6 +4617,7 @@ "edit-actions": { "add": "Přidat {{typeName}}", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5395,6 +5408,20 @@ "tags-expected-array": "očekávané pole tagů", "tags-expected-strings": "očekávané pole řetězců tagů" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Obnovit na verzi {{version}}", "label-view-json-diff": "Zobrazit rozdíl JSON", @@ -8316,6 +8343,9 @@ "card": { "option": "možnost" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Vymazat výběr" }, @@ -8328,6 +8358,7 @@ "aria-label-selected-color": "{{colorLabel}} barva" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Zrušit", "confirmText-delete": "" }, @@ -8754,6 +8785,7 @@ "footer-add-annotation": "Přidat vysvětlivku", "footer-click-to-action": "Klikněte pro {{actionTitle}}", "footer-click-to-navigate": "Klikněte pro otevření {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "Časová známka" }, "week-start-picker": { @@ -11296,6 +11328,7 @@ "starting": "Spouštění…", "status": { "title-error-running-job": "Chyba při spouštění úlohy", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Shrnutí", @@ -11444,6 +11477,7 @@ "label-current-step": "Aktuální krok", "label-pending-step": "Nevyřízený krok" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11476,6 +11510,12 @@ "alert-title": "Důležité: Nebudou ztracena žádná data ani konfigurace, ale nástěnky budou na několik minut dočasně nedostupné.", "button-next": "Dokončit", "button-start": "Zahájit synchronizaci", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Vyberte, co chcete synchronizovat", "step-connect": "Připojit", "step-finish": "Vybrat další nastavení", @@ -11490,6 +11530,7 @@ "wizard-content": { "button-cancel": "Zrušit", "button-cancelling": "Probíhá zrušení…", + "button-previous": "", "button-submitting": "Odesílání…", "error-instance-repository-exists": "Instance úložiště už existuje" } diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 2606fa04808..f3b222b2c46 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -3482,6 +3482,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Abbrechen", "cannot-move-folders": "Ordner können nicht verschoben werden", "confirmation-text": "Löschen", @@ -3515,6 +3516,10 @@ "this-folder-is-empty": "Dieser Ordner ist leer" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_other": "" }, @@ -3522,15 +3527,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} Alarmierungsregel", @@ -4567,6 +4579,7 @@ "edit-actions": { "add": "{{typeName}} hinzufügen", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5355,6 +5368,20 @@ "tags-expected-array": "Array der jeweiligen Tags", "tags-expected-strings": "String-Array der jeweiligen Tags" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Auf Version {{version}} wiederherstellen", "label-view-json-diff": "JSON-Diff anzeigen", @@ -8272,6 +8299,9 @@ "card": { "option": "Option" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Auswahl löschen" }, @@ -8284,6 +8314,7 @@ "aria-label-selected-color": "{{colorLabel}} Farbe" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Abbrechen", "confirmText-delete": "" }, @@ -8710,6 +8741,7 @@ "footer-add-annotation": "Anmerkung hinzufügen", "footer-click-to-action": "Klicken, um zu {{actionTitle}}", "footer-click-to-navigate": "Zum Öffnen von {{linkTitle}} klicken", + "footer-filter-for-value": "", "timestamp": "Zeitstempel" }, "week-start-picker": { @@ -11224,6 +11256,7 @@ "starting": "Wird gestartet ...", "status": { "title-error-running-job": "Fehler bei der Ausführung des Auftrags", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Zusammenfassung", @@ -11372,6 +11405,7 @@ "label-current-step": "Aktueller Schritt", "label-pending-step": "Ausstehender Schritt" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11404,6 +11438,12 @@ "alert-title": "Wichtig: Es gehen keine Daten oder Konfigurationen verloren, aber die Dashboards sind für einige Minuten vorübergehend nicht verfügbar.", "button-next": "Fertigstellen", "button-start": "Synchronisierung starten", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Wählen Sie aus, was synchronisiert wird", "step-connect": "Verbinden", "step-finish": "Zusätzliche Einstellungen auswählen", @@ -11418,6 +11458,7 @@ "wizard-content": { "button-cancel": "Abbrechen", "button-cancelling": "Abbrechen ...", + "button-previous": "", "button-submitting": "Übertragen …", "error-instance-repository-exists": "Das Instanz-Repository existiert bereits" } diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 8102b5419d5..ab32ab5c265 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -3482,6 +3482,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Cancelar", "cannot-move-folders": "Las carpetas no se pueden mover", "confirmation-text": "Eliminar", @@ -3515,6 +3516,10 @@ "this-folder-is-empty": "Esta carpeta está vacía" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_other": "" }, @@ -3522,15 +3527,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} regla de alerta", @@ -4567,6 +4579,7 @@ "edit-actions": { "add": "Añadir {{typeName}}", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5355,6 +5368,20 @@ "tags-expected-array": "etiquetas: matriz prevista", "tags-expected-strings": "etiquetas: matriz de cadenas prevista" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Restaurar a la versión {{version}} ", "label-view-json-diff": "Ver diferencia de JSON", @@ -8272,6 +8299,9 @@ "card": { "option": "opción" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Eliminar selección" }, @@ -8284,6 +8314,7 @@ "aria-label-selected-color": "{{colorLabel}} color" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Cancelar", "confirmText-delete": "" }, @@ -8710,6 +8741,7 @@ "footer-add-annotation": "Añadir anotación", "footer-click-to-action": "Haz clic para {{actionTitle}}", "footer-click-to-navigate": "Haz clic para abrir {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "Marca de tiempo" }, "week-start-picker": { @@ -11224,6 +11256,7 @@ "starting": "Empezando...", "status": { "title-error-running-job": "Error al ejecutar el trabajo", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Resumen", @@ -11372,6 +11405,7 @@ "label-current-step": "Paso actual", "label-pending-step": "Paso pendiente" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11404,6 +11438,12 @@ "alert-title": "Importante: No se perderán datos ni configuraciones, pero los dashboards no estarán disponibles temporalmente durante unos minutos.", "button-next": "Terminar", "button-start": "Iniciar la sincronización", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Elegir qué sincronizar", "step-connect": "Conectarse", "step-finish": "Elegir ajustes adicionales", @@ -11418,6 +11458,7 @@ "wizard-content": { "button-cancel": "Cancelar", "button-cancelling": "Cancelando... ", + "button-previous": "", "button-submitting": "Enviando...", "error-instance-repository-exists": "El repositorio de instancias ya existe" } diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 627306638b1..4a38e05d966 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -3482,6 +3482,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Annuler", "cannot-move-folders": "Les dossiers ne peuvent pas être déplacés", "confirmation-text": "Supprimer", @@ -3515,6 +3516,10 @@ "this-folder-is-empty": "Ce dossier est vide" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_other": "" }, @@ -3522,15 +3527,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} règle d'alerte", @@ -4567,6 +4579,7 @@ "edit-actions": { "add": "Ajouter {{typeName}}", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5355,6 +5368,20 @@ "tags-expected-array": "balises attendues tableau", "tags-expected-strings": "balises attendues tableau de chaînes" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Restaurer la version {{version}}", "label-view-json-diff": "Afficher les différences du JSON", @@ -8272,6 +8299,9 @@ "card": { "option": "option" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Effacer la sélection" }, @@ -8284,6 +8314,7 @@ "aria-label-selected-color": "Couleur {{colorLabel}}" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Annuler", "confirmText-delete": "" }, @@ -8710,6 +8741,7 @@ "footer-add-annotation": "Ajouter une annotation", "footer-click-to-action": "Cliquez pour {{actionTitle}}", "footer-click-to-navigate": "Cliquez pour ouvrir {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "Horodatage" }, "week-start-picker": { @@ -11224,6 +11256,7 @@ "starting": "Démarrage...", "status": { "title-error-running-job": "Erreur lors de l’exécution de la mission", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Synthèse", @@ -11372,6 +11405,7 @@ "label-current-step": "Étape actuelle", "label-pending-step": "Étape en attente" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11404,6 +11438,12 @@ "alert-title": "Important : aucune donnée ou configuration ne sera perdue, mais les tableaux de bord seront temporairement indisponibles pendant quelques minutes.", "button-next": "Terminer", "button-start": "Commencer la synchronisation", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Choisir ce qui doit être synchronisé", "step-connect": "Connecter", "step-finish": "Choisir des paramètres supplémentaires", @@ -11418,6 +11458,7 @@ "wizard-content": { "button-cancel": "Annuler", "button-cancelling": "Annulation...", + "button-previous": "", "button-submitting": "Envoi…", "error-instance-repository-exists": "Le référentiel d’instances existe déjà" } diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 5f8898eca53..94b862a9048 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -3482,6 +3482,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Mégse", "cannot-move-folders": "A mappák nem helyezhetők át", "confirmation-text": "Törlés", @@ -3515,6 +3516,10 @@ "this-folder-is-empty": "A mappa üres" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_other": "" }, @@ -3522,15 +3527,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} riasztási szabály", @@ -4567,6 +4579,7 @@ "edit-actions": { "add": "{{typeName}} hozzáadása", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5355,6 +5368,20 @@ "tags-expected-array": "címke tömböt várt", "tags-expected-strings": "címke karakterláncok tömbjét várta" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Visszaállítás erre a verzióra: {{version}}", "label-view-json-diff": "JSON-diff megtekintése", @@ -8272,6 +8299,9 @@ "card": { "option": "opció" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Kijelölés törlése" }, @@ -8284,6 +8314,7 @@ "aria-label-selected-color": "{{colorLabel}} szín" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Mégse", "confirmText-delete": "" }, @@ -8710,6 +8741,7 @@ "footer-add-annotation": "Jegyzet hozzáadása", "footer-click-to-action": "Kattintson ehhez: {{actionTitle}}", "footer-click-to-navigate": "Kattintson a következő megnyitásához: {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "Időbélyeg" }, "week-start-picker": { @@ -11224,6 +11256,7 @@ "starting": "Kezdés…", "status": { "title-error-running-job": "Hiba történt a feladat futtatásakor", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Összegzés", @@ -11372,6 +11405,7 @@ "label-current-step": "Jelenlegi lépés", "label-pending-step": "Függőben lévő lépés" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11404,6 +11438,12 @@ "alert-title": "Fontos: Egyetlen adat vagy konfiguráció sem fog elveszni, de az irányítópultok néhány percig átmenetileg nem lesznek elérhetők.", "button-next": "Befejezés", "button-start": "Szinkronizálás indítása", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Válassza ki, mit szeretne szinkronizálni", "step-connect": "Kapcsolódás", "step-finish": "Válassza ki a további beállításokat", @@ -11418,6 +11458,7 @@ "wizard-content": { "button-cancel": "Mégse", "button-cancelling": "Megszakítás…", + "button-previous": "", "button-submitting": "Küldés…", "error-instance-repository-exists": "A példánytár már létezik" } diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 6a95edf4292..52e1b7bd2f0 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -3469,6 +3469,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Batalkan", "cannot-move-folders": "Folder tidak dapat dipindahkan", "confirmation-text": "Hapus", @@ -3502,21 +3503,32 @@ "this-folder-is-empty": "Folder ini kosong" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_other": "" }, "bulk-delete-resources-form": { "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_other": "{{count}} aturan peringatan", @@ -4548,6 +4560,7 @@ "edit-actions": { "add": "Tambahkan {{typeName}}", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5335,6 +5348,20 @@ "tags-expected-array": "tag array yang diharapkan", "tags-expected-strings": "tag array yang diharapkan dari string" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Pulihkan ke versi {{version}}", "label-view-json-diff": "Lihat diff JSON", @@ -8250,6 +8277,9 @@ "card": { "option": "opsi" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Hapus pilihan" }, @@ -8262,6 +8292,7 @@ "aria-label-selected-color": "warna {{colorLabel}}" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Batalkan", "confirmText-delete": "" }, @@ -8688,6 +8719,7 @@ "footer-add-annotation": "Tambahkan anotasi", "footer-click-to-action": "Klik untuk {{actionTitle}} ", "footer-click-to-navigate": "Klik untuk membuka {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "Stempel Waktu" }, "week-start-picker": { @@ -11188,6 +11220,7 @@ "starting": "Memulai...", "status": { "title-error-running-job": "Kesalahan saat menjalankan pekerjaan", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Ringkasan", @@ -11336,6 +11369,7 @@ "label-current-step": "Langkah saat ini", "label-pending-step": "Langkah tertunda" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11368,6 +11402,12 @@ "alert-title": "Penting: Tidak ada data atau konfigurasi yang akan hilang, tetapi dasbor tidak akan tersedia sementara selama beberapa menit.", "button-next": "Selesai", "button-start": "Mulai sinkronisasi", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Pilih item yang akan disinkronkan", "step-connect": "Hubungkan", "step-finish": "Pilih pengaturan tambahan", @@ -11382,6 +11422,7 @@ "wizard-content": { "button-cancel": "Batalkan", "button-cancelling": "Membatalkan...", + "button-previous": "", "button-submitting": "Mengirim...", "error-instance-repository-exists": "Repositori instans sudah ada" } diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 25ee68ca994..cb88ba60b9b 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -3482,6 +3482,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Annulla", "cannot-move-folders": "Non è possibile spostare le cartelle", "confirmation-text": "Elimina", @@ -3515,6 +3516,10 @@ "this-folder-is-empty": "Questa cartella è vuota" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_other": "" }, @@ -3522,15 +3527,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} regola di avviso", @@ -4567,6 +4579,7 @@ "edit-actions": { "add": "", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5355,6 +5368,20 @@ "tags-expected-array": "array con tag previsti", "tags-expected-strings": "array di stringhe con tag previsti" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Ripristina alla versione {{version}} ", "label-view-json-diff": "Visualizza differenza JSON", @@ -8272,6 +8299,9 @@ "card": { "option": "opzione" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Cancella selezione" }, @@ -8284,6 +8314,7 @@ "aria-label-selected-color": "Colore {{colorLabel}}" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Annulla", "confirmText-delete": "" }, @@ -8710,6 +8741,7 @@ "footer-add-annotation": "Aggiungi annotazione", "footer-click-to-action": "Fai clic per {{actionTitle}}", "footer-click-to-navigate": "Fai clic per aprire {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "Indicazione data/ora" }, "week-start-picker": { @@ -11224,6 +11256,7 @@ "starting": "Avvio in corso...", "status": { "title-error-running-job": "Errore durante l'esecuzione dell'attività", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Indice", @@ -11372,6 +11405,7 @@ "label-current-step": "Passaggio corrente", "label-pending-step": "Passaggio in sospeso" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11404,6 +11438,12 @@ "alert-title": "Importante: nessun dato o configurazione andrà perso, ma le dashboard non saranno disponibili per alcuni minuti.", "button-next": "Fine", "button-start": "Inizia la sincronizzazione", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Scegli cosa sincronizzare", "step-connect": "Connetti", "step-finish": "Scegli impostazioni aggiuntive", @@ -11418,6 +11458,7 @@ "wizard-content": { "button-cancel": "Annulla", "button-cancelling": "Annullamento in corso...", + "button-previous": "", "button-submitting": "Invio in corso...", "error-instance-repository-exists": "Il repository delle istanze esiste già" } diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index e0bbfddb7c7..c20aa149d2f 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -3469,6 +3469,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "キャンセル", "cannot-move-folders": "フォルダは移動できません", "confirmation-text": "削除", @@ -3502,21 +3503,32 @@ "this-folder-is-empty": "このフォルダは空です" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_other": "" }, "bulk-delete-resources-form": { "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_other": "{{count}}件のアラートルール", @@ -4548,6 +4560,7 @@ "edit-actions": { "add": "{{typeName}}を追加", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5335,6 +5348,20 @@ "tags-expected-array": "タグは配列を期待しています", "tags-expected-strings": "タグは文字列の配列を期待しています" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "バージョン、{{version}}に復元", "label-view-json-diff": "JSON差分を表示", @@ -8250,6 +8277,9 @@ "card": { "option": "オプション" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "選択をクリア" }, @@ -8262,6 +8292,7 @@ "aria-label-selected-color": "{{colorLabel}}色" }, "confirm-button": { + "aria-label-delete": "", "cancel": "キャンセル", "confirmText-delete": "" }, @@ -8688,6 +8719,7 @@ "footer-add-annotation": "注釈を追加", "footer-click-to-action": "クリックして{{actionTitle}}", "footer-click-to-navigate": "クリックして{{linkTitle}}を開く", + "footer-filter-for-value": "", "timestamp": "タイムスタンプ" }, "week-start-picker": { @@ -11188,6 +11220,7 @@ "starting": "開始中...", "status": { "title-error-running-job": "実行エラー", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "概要", @@ -11336,6 +11369,7 @@ "label-current-step": "現在のステップ", "label-pending-step": "保留中のステップ" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11368,6 +11402,12 @@ "alert-title": "重要:データや設定は失われませんが、ダッシュボードは数分間一時的に利用できなくなります。", "button-next": "完了", "button-start": "同期を開始", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "同期する内容を選択", "step-connect": "接続", "step-finish": "追加設定を選択", @@ -11382,6 +11422,7 @@ "wizard-content": { "button-cancel": "キャンセル", "button-cancelling": "キャンセル中…", + "button-previous": "", "button-submitting": "送信中…", "error-instance-repository-exists": "インスタンスリポジトリはすでに存在します" } diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 82ee0e19045..f105ff1a09c 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -3469,6 +3469,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "취소", "cannot-move-folders": "폴더는 이동할 수 없습니다.", "confirmation-text": "삭제", @@ -3502,21 +3503,32 @@ "this-folder-is-empty": "이 폴더는 비어 있습니다" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_other": "" }, "bulk-delete-resources-form": { "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_other": "경고 규칙 {{count}}개", @@ -4548,6 +4560,7 @@ "edit-actions": { "add": "{{typeName}} 추가", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5335,6 +5348,20 @@ "tags-expected-array": "태그로 배열을 입력해야 합니다.", "tags-expected-strings": "태그로 문자열 배열을 입력해야 합니다." }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "{{version}} 버전으로 복구", "label-view-json-diff": "JSON 차이 보기", @@ -8250,6 +8277,9 @@ "card": { "option": "옵션" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "선택 초기화" }, @@ -8262,6 +8292,7 @@ "aria-label-selected-color": "{{colorLabel}} 색상" }, "confirm-button": { + "aria-label-delete": "", "cancel": "취소", "confirmText-delete": "" }, @@ -8688,6 +8719,7 @@ "footer-add-annotation": "주석 추가", "footer-click-to-action": "클릭하여 {{actionTitle}} 작업 수행", "footer-click-to-navigate": "클릭하여 {{linkTitle}} 열기", + "footer-filter-for-value": "", "timestamp": "시간 기록" }, "week-start-picker": { @@ -11188,6 +11220,7 @@ "starting": "시작하는 중...", "status": { "title-error-running-job": "작업 실행 중 오류 발생", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "요약", @@ -11336,6 +11369,7 @@ "label-current-step": "현재 단계", "label-pending-step": "보류 중인 단계" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11368,6 +11402,12 @@ "alert-title": "중요: 데이터나 구성은 손실되지 않지만 대시보드를 몇 분 동안 일시적으로 사용할 수 없습니다.", "button-next": "완료", "button-start": "동기화 시작", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "동기화할 항목 선택", "step-connect": "연결", "step-finish": "추가 설정 선택", @@ -11382,6 +11422,7 @@ "wizard-content": { "button-cancel": "취소", "button-cancelling": "취소 중...", + "button-previous": "", "button-submitting": "제출 중…", "error-instance-repository-exists": "인스턴스 리포지토리가 이미 존재합니다" } diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index b7b087bea04..13dea31f267 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -3482,6 +3482,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Annuleren", "cannot-move-folders": "Mappen kunnen niet worden verplaatst", "confirmation-text": "Verwijderen", @@ -3515,6 +3516,10 @@ "this-folder-is-empty": "Deze map is leeg" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_other": "" }, @@ -3522,15 +3527,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} waarschuwingsregel", @@ -4567,6 +4579,7 @@ "edit-actions": { "add": "{{typeName}} toevoegen", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5355,6 +5368,20 @@ "tags-expected-array": "tags expected array", "tags-expected-strings": "tags expected array van strings" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Herstellen naar versie {{version}}", "label-view-json-diff": "JSON diff weergeven", @@ -8272,6 +8299,9 @@ "card": { "option": "optie" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Selectie wissen" }, @@ -8284,6 +8314,7 @@ "aria-label-selected-color": "Kleur {{colorLabel}}" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Annuleren", "confirmText-delete": "" }, @@ -8710,6 +8741,7 @@ "footer-add-annotation": "Annotatie toevoegen", "footer-click-to-action": "Klik om te {{actionTitle}}", "footer-click-to-navigate": "Klik om {{linkTitle}} te openen", + "footer-filter-for-value": "", "timestamp": "Tijdstempel" }, "week-start-picker": { @@ -11224,6 +11256,7 @@ "starting": "Starten...", "status": { "title-error-running-job": "Er is een fout opgetreden bij het uitvoeren van de taak", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Samenvatting", @@ -11372,6 +11405,7 @@ "label-current-step": "Huidige stap", "label-pending-step": "Stap in afwachting" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11404,6 +11438,12 @@ "alert-title": "Belangrijk: er zullen geen gegevens of configuratie verloren gaan, maar dashboards zullen tijdelijk een paar minuten niet beschikbaar zijn.", "button-next": "Voltooien", "button-start": "Synchronisatie starten", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Kies wat je wilt synchroniseren", "step-connect": "Verbinden", "step-finish": "Kies aanvullende instellingen", @@ -11418,6 +11458,7 @@ "wizard-content": { "button-cancel": "Annuleren", "button-cancelling": "Annuleren…", + "button-previous": "", "button-submitting": "Indienen...", "error-instance-repository-exists": "Er bestaat al een instantierepository" } diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 447a6e05fa1..42b4fbd5c2b 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -3508,6 +3508,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Anuluj", "cannot-move-folders": "Nie można przenosić folderów", "confirmation-text": "Usuń", @@ -3541,6 +3542,10 @@ "this-folder-is-empty": "Ten folder jest pusty" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_few": "", "failed-alert_many": "", @@ -3550,15 +3555,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} reguła alertu", @@ -4605,6 +4617,7 @@ "edit-actions": { "add": "Dodaj: {{typeName}}", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5395,6 +5408,20 @@ "tags-expected-array": "znaczniki oczekiwały tablicy", "tags-expected-strings": "znaczniki oczekiwały tablicy łańcuchów" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Przywróć do wersji {{version}}", "label-view-json-diff": "Wyświetl plik diff JSON", @@ -8316,6 +8343,9 @@ "card": { "option": "opcja" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Usuń zaznaczone" }, @@ -8328,6 +8358,7 @@ "aria-label-selected-color": "Kolor {{colorLabel}}" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Anuluj", "confirmText-delete": "" }, @@ -8754,6 +8785,7 @@ "footer-add-annotation": "Dodaj komentarz", "footer-click-to-action": "Kliknij, aby podjąć działanie: {{actionTitle}}", "footer-click-to-navigate": "Kliknij, aby otworzyć {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "Znacznik czasu" }, "week-start-picker": { @@ -11296,6 +11328,7 @@ "starting": "Uruchamianie…", "status": { "title-error-running-job": "Błąd uruchamiania zadania", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Podsumowanie", @@ -11444,6 +11477,7 @@ "label-current-step": "Aktualny krok", "label-pending-step": "Oczekujący krok" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11476,6 +11510,12 @@ "alert-title": "Ważne: dane ani konfiguracja nie zostaną utracone, ale pulpity będą niedostępne przez kilka minut.", "button-next": "Zakończ", "button-start": "Rozpocznij synchronizację", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Wybierz, co chcesz zsynchronizować", "step-connect": "Połącz", "step-finish": "Wybierz dodatkowe ustawienia", @@ -11490,6 +11530,7 @@ "wizard-content": { "button-cancel": "Anuluj", "button-cancelling": "Anulowanie…", + "button-previous": "", "button-submitting": "Zgłaszanie…", "error-instance-repository-exists": "Repozytorium instancji już istnieje" } diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 84796993b13..0fa35e4fbf5 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -3482,6 +3482,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Cancelar", "cannot-move-folders": "As pastas não podem ser movidas", "confirmation-text": "Excluir", @@ -3515,6 +3516,10 @@ "this-folder-is-empty": "Esta pasta está vazia" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_other": "" }, @@ -3522,15 +3527,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} regra de alerta", @@ -4567,6 +4579,7 @@ "edit-actions": { "add": "Adicionar {{typeName}}", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5355,6 +5368,20 @@ "tags-expected-array": "matriz de tags esperada", "tags-expected-strings": "matriz de strings de tags esperada" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Restaurar para a versão {{version}}", "label-view-json-diff": "Visualizar diferença de JSON", @@ -8272,6 +8299,9 @@ "card": { "option": "opção" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Limpar seleção" }, @@ -8284,6 +8314,7 @@ "aria-label-selected-color": "cor {{colorLabel}}" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Cancelar", "confirmText-delete": "" }, @@ -8710,6 +8741,7 @@ "footer-add-annotation": "Adicionar anotação", "footer-click-to-action": "Clique para {{actionTitle}}", "footer-click-to-navigate": "Clique para abrir {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "Registro de data/hora" }, "week-start-picker": { @@ -11224,6 +11256,7 @@ "starting": "Iniciando…", "status": { "title-error-running-job": "Erro ao executar a tarefa", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Resumo", @@ -11372,6 +11405,7 @@ "label-current-step": "Etapa atual", "label-pending-step": "Etapa pendente" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11404,6 +11438,12 @@ "alert-title": "Importante: nenhum dado ou configuração será perdido, mas os painéis ficarão indisponíveis por alguns minutos.", "button-next": "Finalizar", "button-start": "Iniciar sincronização", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Escolha o que será sincronizado", "step-connect": "Conectar", "step-finish": "Escolha configurações adicionais", @@ -11418,6 +11458,7 @@ "wizard-content": { "button-cancel": "Cancelar", "button-cancelling": "Cancelando… ", + "button-previous": "", "button-submitting": "Enviando…", "error-instance-repository-exists": "O repositório de instâncias já existe" } diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 8ab44e83200..e24b5589e56 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -3482,6 +3482,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Cancelar", "cannot-move-folders": "Não é possível mover as pastas", "confirmation-text": "Eliminar", @@ -3515,6 +3516,10 @@ "this-folder-is-empty": "Esta pasta está vazia" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_other": "" }, @@ -3522,15 +3527,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} regra de alerta", @@ -4567,6 +4579,7 @@ "edit-actions": { "add": "Adicionar {{typeName}}", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5355,6 +5368,20 @@ "tags-expected-array": "matriz de etiquetas esperada", "tags-expected-strings": "matriz de strings de etiquetas esperada" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Restaurar para a versão {{version}}", "label-view-json-diff": "Visualizar diferenças JSON", @@ -8272,6 +8299,9 @@ "card": { "option": "opção" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Limpar a seleção" }, @@ -8284,6 +8314,7 @@ "aria-label-selected-color": "cor {{colorLabel}} " }, "confirm-button": { + "aria-label-delete": "", "cancel": "Cancelar", "confirmText-delete": "" }, @@ -8710,6 +8741,7 @@ "footer-add-annotation": "Adicionar anotação", "footer-click-to-action": "Clique para {{actionTitle}}", "footer-click-to-navigate": "Clique para abrir {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "Registo de data e hora" }, "week-start-picker": { @@ -11224,6 +11256,7 @@ "starting": "A iniciar...", "status": { "title-error-running-job": "Erro ao executar tarefa", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Resumo", @@ -11372,6 +11405,7 @@ "label-current-step": "Passo atual", "label-pending-step": "Passo pendente" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11404,6 +11438,12 @@ "alert-title": "Importante: não serão perdidos dados ou configurações, mas os painéis de controlo ficarão temporariamente indisponíveis durante alguns minutos.", "button-next": "Concluir", "button-start": "Iniciar sincronização", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Escolha o que sincronizar", "step-connect": "Conectar ", "step-finish": "Escolha as definições adicionais", @@ -11418,6 +11458,7 @@ "wizard-content": { "button-cancel": "Cancelar", "button-cancelling": "A cancelar... ", + "button-previous": "", "button-submitting": "A enviar...", "error-instance-repository-exists": "O repositório de instâncias já existe" } diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 8b0ee0b1c2f..ca39347f187 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -3508,6 +3508,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Отмена", "cannot-move-folders": "Папки нельзя перемещать", "confirmation-text": "Удалить", @@ -3541,6 +3542,10 @@ "this-folder-is-empty": "Папка пуста" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_few": "", "failed-alert_many": "", @@ -3550,15 +3555,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} правило оповещения", @@ -4605,6 +4617,7 @@ "edit-actions": { "add": "Добавить {{typeName}}", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5395,6 +5408,20 @@ "tags-expected-array": "ожидаемый массив тегов", "tags-expected-strings": "ожидаемый массив строк тегов" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Восстановить до версии {{version}} ", "label-view-json-diff": "Просмотр JSON Diff", @@ -8316,6 +8343,9 @@ "card": { "option": "параметр" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Очистить выбор" }, @@ -8328,6 +8358,7 @@ "aria-label-selected-color": "{{colorLabel}} цвет" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Отмена", "confirmText-delete": "" }, @@ -8754,6 +8785,7 @@ "footer-add-annotation": "Добавить аннотацию", "footer-click-to-action": "Нажмите, чтобы {{actionTitle}}", "footer-click-to-navigate": "Нажмите, чтобы открыть {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "Метка времени" }, "week-start-picker": { @@ -11296,6 +11328,7 @@ "starting": "Запуск...", "status": { "title-error-running-job": "Ошибка при выполнении задания", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Сводка", @@ -11444,6 +11477,7 @@ "label-current-step": "Текущий шаг", "label-pending-step": "Предстоящий шаг" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11476,6 +11510,12 @@ "alert-title": "Важно. Данные и конфигурация не будут потеряны, однако дашборды будут недоступны в течение нескольких минут.", "button-next": "Готово", "button-start": "Начать синхронизацию", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Выберите, что синхронизировать", "step-connect": "Подключить", "step-finish": "Выберите дополнительные параметры", @@ -11490,6 +11530,7 @@ "wizard-content": { "button-cancel": "Отмена", "button-cancelling": "Отмена...", + "button-previous": "", "button-submitting": "Отправка...", "error-instance-repository-exists": "Репозиторий экземпляров уже существует" } diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index f3f07e904bb..357d3900787 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -3482,6 +3482,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "Avbryt", "cannot-move-folders": "Mapparna kan inte flyttas", "confirmation-text": "Ta bort", @@ -3515,6 +3516,10 @@ "this-folder-is-empty": "Denna mapp är tom" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_other": "" }, @@ -3522,15 +3527,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} varningsregel", @@ -4567,6 +4579,7 @@ "edit-actions": { "add": "Lägg till {{typeName}}", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5355,6 +5368,20 @@ "tags-expected-array": "taggar förväntade matris", "tags-expected-strings": "taggar förväntade strängmatris" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "Återställ till version {{version}}", "label-view-json-diff": "Visa JSON-skillnader", @@ -8272,6 +8299,9 @@ "card": { "option": "alternativ" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Rensa val" }, @@ -8284,6 +8314,7 @@ "aria-label-selected-color": "{{colorLabel}} färg" }, "confirm-button": { + "aria-label-delete": "", "cancel": "Avbryt", "confirmText-delete": "" }, @@ -8710,6 +8741,7 @@ "footer-add-annotation": "Lägg till kommentar", "footer-click-to-action": "Klicka för att {{actionTitle}}", "footer-click-to-navigate": "Klicka för att öppna {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "Tidsstämpel" }, "week-start-picker": { @@ -11224,6 +11256,7 @@ "starting": "Startar …", "status": { "title-error-running-job": "Ett fel uppstod när ett jobb kördes", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Sammanfattning", @@ -11372,6 +11405,7 @@ "label-current-step": "Nuvarande steg", "label-pending-step": "Väntande steg" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11404,6 +11438,12 @@ "alert-title": "Viktigt: Inga data eller konfigurationer kommer att förloras, men instrumentpaneler kommer att vara tillfälligt otillgängliga under några minuter.", "button-next": "Slutför", "button-start": "Påbörja synkronisering", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Välj vad du vill synkronisera", "step-connect": "Anslut", "step-finish": "Välj ytterligare inställningar", @@ -11418,6 +11458,7 @@ "wizard-content": { "button-cancel": "Avbryt", "button-cancelling": "Avbryter …", + "button-previous": "", "button-submitting": "Skickar …", "error-instance-repository-exists": "Instanslagringsplatsen finns redan" } diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 7efaa00fee2..cbcae4254be 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -3482,6 +3482,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "İptal", "cannot-move-folders": "Klasörler taşınamaz", "confirmation-text": "Sil", @@ -3515,6 +3516,10 @@ "this-folder-is-empty": "Klasör boş" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_one": "", "failed-alert_other": "" }, @@ -3522,15 +3527,22 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_one": "{{count}} uyarı kuralı", @@ -4567,6 +4579,7 @@ "edit-actions": { "add": "{{typeName}} ekle", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5355,6 +5368,20 @@ "tags-expected-array": "etiketler dizi olmalı", "tags-expected-strings": "etiklet dizi hâlinde metinlerden oluşmalı" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "{{version}} sürümüne geri yükle", "label-view-json-diff": "JSON farkını görüntüle", @@ -8272,6 +8299,9 @@ "card": { "option": "seçenek" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "Seçimi temizle" }, @@ -8284,6 +8314,7 @@ "aria-label-selected-color": "{{colorLabel}} rengi" }, "confirm-button": { + "aria-label-delete": "", "cancel": "İptal", "confirmText-delete": "" }, @@ -8710,6 +8741,7 @@ "footer-add-annotation": "Ek açıklama ekle", "footer-click-to-action": "{{actionTitle}} için tıklayın", "footer-click-to-navigate": "{{linkTitle}} ögesini açmak için tıklayın", + "footer-filter-for-value": "", "timestamp": "Zaman damgası" }, "week-start-picker": { @@ -11224,6 +11256,7 @@ "starting": "Başlatılıyor...", "status": { "title-error-running-job": "İş çalıştırılırken hata oluştu", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "Özet", @@ -11372,6 +11405,7 @@ "label-current-step": "Mevcut adım", "label-pending-step": "Bekleyen adım" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11404,6 +11438,12 @@ "alert-title": "Önemli: Veri veya yapılandırma kaybı yaşanmayacak ancak panolar birkaç dakika boyunca geçici olarak kullanılamayacaktır.", "button-next": "Sonlandır", "button-start": "Senkronizasyonu başlat", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "Nelerin senkronize edileceğini seçin", "step-connect": "Bağlan", "step-finish": "Ek ayarlar seçin", @@ -11418,6 +11458,7 @@ "wizard-content": { "button-cancel": "İptal", "button-cancelling": "İptal ediliyor...", + "button-previous": "", "button-submitting": "Gönderiliyor...", "error-instance-repository-exists": "Örnek deposu zaten var" } diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index fafcf272b03..e4fa606b72d 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -3469,6 +3469,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "取消", "cannot-move-folders": "文件夹无法移动", "confirmation-text": "删除", @@ -3502,21 +3503,32 @@ "this-folder-is-empty": "此文件夹为空" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_other": "" }, "bulk-delete-resources-form": { "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_other": "{{count}} 条警告规则", @@ -4548,6 +4560,7 @@ "edit-actions": { "add": "添加{{typeName}}", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5335,6 +5348,20 @@ "tags-expected-array": "标签预期数组", "tags-expected-strings": "标签预期字符串数组" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "恢复到版本 {{version}}", "label-view-json-diff": "查看 JSON 差异", @@ -8250,6 +8277,9 @@ "card": { "option": "选项" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "清除选择" }, @@ -8262,6 +8292,7 @@ "aria-label-selected-color": "{{colorLabel}} 颜色" }, "confirm-button": { + "aria-label-delete": "", "cancel": "取消", "confirmText-delete": "" }, @@ -8688,6 +8719,7 @@ "footer-add-annotation": "添加注释", "footer-click-to-action": "点击以 {{actionTitle}}", "footer-click-to-navigate": "点击打开 {{linkTitle}}", + "footer-filter-for-value": "", "timestamp": "时间戳" }, "week-start-picker": { @@ -11188,6 +11220,7 @@ "starting": "正在启动...", "status": { "title-error-running-job": "运行作业时出错", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "摘要", @@ -11336,6 +11369,7 @@ "label-current-step": "当前步骤", "label-pending-step": "待处理步骤" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11368,6 +11402,12 @@ "alert-title": "重要提示:数据或配置不会丢失,但数据面板将在几分钟内暂时不可用。", "button-next": "完成", "button-start": "开始同步", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "选择要同步的内容", "step-connect": "连接", "step-finish": "选择附加设置", @@ -11382,6 +11422,7 @@ "wizard-content": { "button-cancel": "取消", "button-cancelling": "正在取消...", + "button-previous": "", "button-submitting": "正在提交...", "error-instance-repository-exists": "实例存储库已存在" } diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 9c5d92bcfd8..a73bab83765 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -3469,6 +3469,7 @@ "browse-dashboards": { "action": { "bulk-delete-provisioned-resources": "", + "bulk-move-provisioned-resources": "", "cancel-button": "取消", "cannot-move-folders": "資料夾無法移動", "confirmation-text": "刪除", @@ -3502,21 +3503,32 @@ "this-folder-is-empty": "此資料夾為空" }, "bulk-action-resources-form": { + "all-deleted": "", + "all-moved": "", + "button-done": "", + "progress-title": "", "failed-alert_other": "" }, "bulk-delete-resources-form": { "button-cancel": "", "button-delete": "", "button-deleting": "", - "button-done": "", "delete-warning": "", - "error-path-not-found": "", - "progress-title": "", - "success-message": "" + "error-path-not-found": "" }, "bulk-move-resources-form": { + "button-cancel": "", + "button-move": "", + "button-moving": "", "deleting": "", - "progress": "" + "error-file-content-not-found": "", + "error-path-not-found": "", + "error-target-folder-path-missing": "", + "error-title": "", + "move-warning": "", + "moving": "", + "progress": "", + "target-folder": "" }, "counts": { "alertRule_other": "{{count}} 個警報規則", @@ -4548,6 +4560,7 @@ "edit-actions": { "add": "新增 {{typeName}}", "add-conditional-rule": "", + "duplicate-panel": "", "edit-query-result-rule": "", "edit-template-variable-rule": "", "edit-time-range-rule": "", @@ -5335,6 +5348,20 @@ "tags-expected-array": "標籤預期陣列", "tags-expected-strings": "標籤預期字串陣列" }, + "variable": { + "description": { + "action": "" + }, + "hide": { + "action": "" + }, + "label": { + "action": "" + }, + "name": { + "action": "" + } + }, "version-history-comparison": { "button-restore": "還原至版本 {{version}}", "label-view-json-diff": "檢視 JSON diff", @@ -8250,6 +8277,9 @@ "card": { "option": "選項" }, + "carousel": { + "aria-label-open-image": "" + }, "cascader": { "clear-button": "清空選項" }, @@ -8262,6 +8292,7 @@ "aria-label-selected-color": "{{colorLabel}} 顏色" }, "confirm-button": { + "aria-label-delete": "", "cancel": "取消", "confirmText-delete": "" }, @@ -8688,6 +8719,7 @@ "footer-add-annotation": "新增注釋", "footer-click-to-action": "點選以{{actionTitle}}", "footer-click-to-navigate": "點選以開啟「{{linkTitle}}」", + "footer-filter-for-value": "", "timestamp": "時間戳記" }, "week-start-picker": { @@ -11188,6 +11220,7 @@ "starting": "正在開始…", "status": { "title-error-running-job": "執行作業時發生錯誤", + "title-success-running-job": "", "title-warning-running-job": "" }, "summary": "摘要", @@ -11336,6 +11369,7 @@ "label-current-step": "目前步驟", "label-pending-step": "待處理步驟" }, + "success-title-default": "", "sync-job": { "error-no-job-id": "", "error-no-repository-name": "", @@ -11368,6 +11402,12 @@ "alert-title": "重要事項:不會遺失任何資料或設定,但儀表板將在幾分鐘內暫時無法使用。", "button-next": "結束", "button-start": "開始同步處理", + "discard-modal": { + "body": "", + "confirm": "", + "dismiss": "", + "title": "" + }, "step-bootstrap": "選擇要同步的內容", "step-connect": "連接", "step-finish": "選擇附加設定", @@ -11382,6 +11422,7 @@ "wizard-content": { "button-cancel": "取消", "button-cancelling": "正在取消…", + "button-previous": "", "button-submitting": "正在提交…", "error-instance-repository-exists": "執行個體儲存庫已存在" } From f4335fc5ce763c61890257cf6d11dfc33c1471de Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:26:36 +0100 Subject: [PATCH 029/126] Update dependency kbar to v0.1.0-beta.48 (#108844) 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 d038cc0962d..79bbdd20cbe 100644 --- a/package.json +++ b/package.json @@ -352,7 +352,7 @@ "json-markup": "^1.1.0", "json-source-map": "0.6.1", "jsurl": "^0.1.5", - "kbar": "0.1.0-beta.45", + "kbar": "0.1.0-beta.48", "leven": "^4.0.0", "lodash": "4.17.21", "logfmt": "^1.3.2", diff --git a/yarn.lock b/yarn.lock index d567616539b..52441f3f4a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18374,7 +18374,7 @@ __metadata: json-markup: "npm:^1.1.0" json-source-map: "npm:0.6.1" jsurl: "npm:^0.1.5" - kbar: "npm:0.1.0-beta.45" + kbar: "npm:0.1.0-beta.48" lerna: "npm:8.2.3" leven: "npm:^4.0.0" lodash: "npm:4.17.21" @@ -21529,9 +21529,9 @@ __metadata: languageName: node linkType: hard -"kbar@npm:0.1.0-beta.45": - version: 0.1.0-beta.45 - resolution: "kbar@npm:0.1.0-beta.45" +"kbar@npm:0.1.0-beta.48": + version: 0.1.0-beta.48 + resolution: "kbar@npm:0.1.0-beta.48" dependencies: "@radix-ui/react-portal": "npm:^1.0.1" fast-equals: "npm:^2.0.3" @@ -21539,9 +21539,9 @@ __metadata: react-virtual: "npm:^2.8.2" tiny-invariant: "npm:^1.2.0" peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: 10/16d161b8563400cc5bff15cf1ba881eb7a00e7bb8b5bd8b67c878fdb60a3db7380ee36f2ac29f15361ece0b214c7d5322d02c954381036d3f72a5227d8d3eb47 + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/3f618f905fa2d2b6a171ad62c3412d932585dd93122a6b594ff416e1419e1ec114f6d48b7bf6c3a56cf6c2d744fb72a817a5d37489c2fdbf499758f72bd18134 languageName: node linkType: hard From f4e45bf3bc56cfb2ba07370fe44f745fd4b8e336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Wed, 30 Jul 2025 10:30:24 +0200 Subject: [PATCH 030/126] Provisioning: bulk move by name (#108869) * Add ResourceRef to move job spec * Implement move by name * Use map to struct --- pkg/apis/provisioning/v0alpha1/jobs.go | 6 + .../v0alpha1/zz_generated.deepcopy.go | 5 + .../v0alpha1/zz_generated.openapi.go | 16 + ...enerated.openapi_violation_exceptions.list | 1 + .../apis/provisioning/jobs/delete/worker.go | 6 +- .../apis/provisioning/jobs/move/worker.go | 94 ++++- .../provisioning/jobs/move/worker_test.go | 369 +++++++++++++++++- pkg/registry/apis/provisioning/register.go | 2 +- .../provisioning.grafana.app-v0alpha1.json | 12 + .../apis/provisioning/provisioning_test.go | 256 ++++++++++++ .../provisioning/v0alpha1/endpoints.gen.ts | 2 + 11 files changed, 747 insertions(+), 22 deletions(-) diff --git a/pkg/apis/provisioning/v0alpha1/jobs.go b/pkg/apis/provisioning/v0alpha1/jobs.go index 771085861d6..4379d2365c1 100644 --- a/pkg/apis/provisioning/v0alpha1/jobs.go +++ b/pkg/apis/provisioning/v0alpha1/jobs.go @@ -178,6 +178,12 @@ type MoveJobOptions struct { // Destination path for the move (e.g. "new-location/") TargetPath string `json:"targetPath,omitempty"` + + // Resources to move + // This option has been created because currently the frontend does not use + // standarized app platform APIs. For performance and API consistency reasons, the preferred option + // is it to use the paths. + Resources []ResourceRef `json:"resources,omitempty"` } // The job status diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go index ce5320bc548..c5438fccd0e 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go @@ -512,6 +512,11 @@ func (in *MoveJobOptions) DeepCopyInto(out *MoveJobOptions) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]ResourceRef, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index aeedcedf468..e6e9e4e9ab5 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -1150,9 +1150,25 @@ func schema_pkg_apis_provisioning_v0alpha1_MoveJobOptions(ref common.ReferenceCa Format: "", }, }, + "resources": { + SchemaProps: spec.SchemaProps{ + Description: "Resources to move This option has been created because currently the frontend does not use standarized app platform APIs. For performance and API consistency reasons, the preferred option is it to use the paths.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceRef"), + }, + }, + }, + }, + }, }, }, }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceRef"}, } } diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list index 67010afc96f..49a2170ce4e 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -7,6 +7,7 @@ API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provis API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,JobStatus,Summary API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ManagerStats,Stats API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,MoveJobOptions,Paths +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,MoveJobOptions,Resources API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RefList,Items API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryList,Items API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,Workflows diff --git a/pkg/registry/apis/provisioning/jobs/delete/worker.go b/pkg/registry/apis/provisioning/jobs/delete/worker.go index 006d296d393..c1f6739405e 100644 --- a/pkg/registry/apis/provisioning/jobs/delete/worker.go +++ b/pkg/registry/apis/provisioning/jobs/delete/worker.go @@ -167,12 +167,12 @@ func deduplicatePaths(paths []string) []string { return paths } - seen := make(map[string]bool, len(paths)) + seen := make(map[string]struct{}, len(paths)) result := make([]string, 0, len(paths)) for _, path := range paths { - if !seen[path] { - seen[path] = true + if _, exists := seen[path]; !exists { + seen[path] = struct{}{} result = append(result, path) } } diff --git a/pkg/registry/apis/provisioning/jobs/move/worker.go b/pkg/registry/apis/provisioning/jobs/move/worker.go index 9dd25aab41e..5c2bba7e4e4 100644 --- a/pkg/registry/apis/provisioning/jobs/move/worker.go +++ b/pkg/registry/apis/provisioning/jobs/move/worker.go @@ -7,21 +7,26 @@ import ( "path/filepath" "time" + "k8s.io/apimachinery/pkg/runtime/schema" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" ) type Worker struct { - syncWorker jobs.Worker - wrapFn repository.WrapWithStageFn + syncWorker jobs.Worker + wrapFn repository.WrapWithStageFn + resourcesFactory resources.RepositoryResourcesFactory } -func NewWorker(syncWorker jobs.Worker, wrapFn repository.WrapWithStageFn) *Worker { +func NewWorker(syncWorker jobs.Worker, wrapFn repository.WrapWithStageFn, resourcesFactory resources.RepositoryResourcesFactory) *Worker { return &Worker{ - syncWorker: syncWorker, - wrapFn: wrapFn, + syncWorker: syncWorker, + wrapFn: wrapFn, + resourcesFactory: resourcesFactory, } } @@ -45,7 +50,8 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr } paths := opts.Paths - progress.SetTotal(ctx, len(paths)) + + progress.SetTotal(ctx, len(paths)+len(opts.Resources)) progress.StrictMaxErrors(1) // Fail fast on any error during move fn := func(repo repository.Repository, _ bool) error { @@ -54,6 +60,18 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr return errors.New("move job submitted targeting repository that is not a ReaderWriter") } + // Resolve ResourceRef entries to file paths using RepositoryResources + if len(opts.Resources) > 0 { + resolvedPaths, err := w.resolveResourcesToPaths(ctx, rw, progress, opts.Resources) + if err != nil { + return err + } + paths = append(paths, resolvedPaths...) + } + + // Deduplicate paths to avoid attempting to move the same file multiple times + paths = deduplicatePaths(paths) + return w.moveFiles(ctx, rw, progress, opts, paths...) } @@ -125,3 +143,67 @@ func (w *Worker) constructTargetPath(jobTargetPath, sourcePath string) string { // For files, just append the filename return jobTargetPath + fileName } + +// resolveResourcesToPaths converts ResourceRef entries to file paths, recording errors for individual resources +func (w *Worker) resolveResourcesToPaths(ctx context.Context, rw repository.ReaderWriter, progress jobs.JobProgressRecorder, resources []provisioning.ResourceRef) ([]string, error) { + if len(resources) == 0 { + return nil, nil + } + + progress.SetMessage(ctx, "Resolving resource paths") + repositoryResources, err := w.resourcesFactory.Client(ctx, rw) + if err != nil { + return nil, fmt.Errorf("create repository resources client: %w", err) + } + + resolvedPaths := make([]string, 0, len(resources)) + for _, resource := range resources { + result := jobs.JobResourceResult{ + Name: resource.Name, + Group: resource.Group, + Action: repository.FileActionRenamed, // Will be used for move later + } + + gvk := schema.GroupVersionKind{ + Group: resource.Group, + Kind: resource.Kind, + // Version is left empty so ForKind will use the preferred version + } + + progress.SetMessage(ctx, fmt.Sprintf("Finding path for resource %s/%s/%s", resource.Group, resource.Kind, resource.Name)) + resourcePath, err := repositoryResources.FindResourcePath(ctx, resource.Name, gvk) + if err != nil { + result.Error = fmt.Errorf("find path for resource %s/%s/%s: %w", resource.Group, resource.Kind, resource.Name, err) + progress.Record(ctx, result) + // Continue with next resource instead of failing fast + if err := progress.TooManyErrors(); err != nil { + return resolvedPaths, err + } + continue + } + + result.Path = resourcePath + resolvedPaths = append(resolvedPaths, resourcePath) + } + + return resolvedPaths, nil +} + +// deduplicatePaths removes duplicate file paths from the slice while preserving order +func deduplicatePaths(paths []string) []string { + if len(paths) <= 1 { + return paths + } + + seen := make(map[string]struct{}, len(paths)) + result := make([]string, 0, len(paths)) + + for _, path := range paths { + if _, exists := seen[path]; !exists { + seen[path] = struct{}{} + result = append(result, path) + } + } + + return result +} diff --git a/pkg/registry/apis/provisioning/jobs/move/worker_test.go b/pkg/registry/apis/provisioning/jobs/move/worker_test.go index 2a6720f5964..3be6d2d4619 100644 --- a/pkg/registry/apis/provisioning/jobs/move/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/move/worker_test.go @@ -3,15 +3,18 @@ package move import ( "context" "errors" + "fmt" "path/filepath" "testing" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -72,7 +75,7 @@ func TestMoveWorker_IsSupported(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - worker := NewWorker(nil, nil) + worker := NewWorker(nil, nil, nil) result := worker.IsSupported(context.Background(), tt.job) require.Equal(t, tt.expected, result) }) @@ -86,7 +89,7 @@ func TestMoveWorker_ProcessMissingMoveSettings(t *testing.T) { }, } - worker := NewWorker(nil, nil) + worker := NewWorker(nil, nil, nil) err := worker.Process(context.Background(), nil, job, nil) require.EqualError(t, err, "missing move settings") } @@ -101,7 +104,7 @@ func TestMoveWorker_ProcessMissingTargetPath(t *testing.T) { }, } - worker := NewWorker(nil, nil) + worker := NewWorker(nil, nil, nil) err := worker.Process(context.Background(), nil, job, nil) require.EqualError(t, err, "target path is required for move operation") } @@ -117,7 +120,7 @@ func TestMoveWorker_ProcessInvalidTargetPath(t *testing.T) { }, } - worker := NewWorker(nil, nil) + worker := NewWorker(nil, nil, nil) err := worker.Process(context.Background(), nil, job, nil) require.EqualError(t, err, "target path must be a directory (should end with '/')") } @@ -149,7 +152,7 @@ func TestMoveWorker_ProcessNotReaderWriter(t *testing.T) { mockProgress.On("SetTotal", mock.Anything, 1).Return() mockProgress.On("StrictMaxErrors", 1).Return() - worker := NewWorker(nil, mockWrapFn.Execute) + worker := NewWorker(nil, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "move files in repository: move job submitted targeting repository that is not a ReaderWriter") } @@ -173,7 +176,7 @@ func TestMoveWorker_ProcessWrapFnError(t *testing.T) { mockProgress.On("SetTotal", mock.Anything, 1).Return() mockProgress.On("StrictMaxErrors", 1).Return() - worker := NewWorker(nil, mockWrapFn.Execute) + worker := NewWorker(nil, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "move files in repository: stage failed") } @@ -220,7 +223,7 @@ func TestMoveWorker_ProcessMoveFilesSuccess(t *testing.T) { return result.Path == "test/path2" && result.Action == repository.FileActionRenamed && result.Error == nil })).Return() - worker := NewWorker(nil, mockWrapFn.Execute) + worker := NewWorker(nil, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -259,7 +262,7 @@ func TestMoveWorker_ProcessMoveFilesWithError(t *testing.T) { })).Return() mockProgress.On("TooManyErrors").Return(errors.New("too many errors")) - worker := NewWorker(nil, mockWrapFn.Execute) + worker := NewWorker(nil, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "move files in repository: too many errors") } @@ -304,7 +307,7 @@ func TestMoveWorker_ProcessWithSyncWorker(t *testing.T) { return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental }), mockProgress).Return(nil) - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -345,7 +348,7 @@ func TestMoveWorker_ProcessSyncWorkerError(t *testing.T) { syncError := errors.New("sync failed") mockSyncWorker.On("Process", mock.Anything, mockRepo, mock.Anything, mockProgress).Return(syncError) - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "pull resources: sync failed") } @@ -426,7 +429,7 @@ func TestMoveWorker_moveFiles(t *testing.T) { } } - worker := NewWorker(nil, nil) + worker := NewWorker(nil, nil, nil) err := worker.moveFiles(context.Background(), mockRepo, mockProgress, opts, tt.paths...) if tt.expectedError != "" { @@ -482,9 +485,351 @@ func TestMoveWorker_constructTargetPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - worker := NewWorker(nil, nil) + worker := NewWorker(nil, nil, nil) result := worker.constructTargetPath(tt.jobTargetPath, tt.sourcePath) require.Equal(t, tt.expectedTarget, result) }) } } + +func TestMoveWorker_ProcessWithResourceReferences(t *testing.T) { + job := provisioning.Job{ + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Move: &provisioning.MoveJobOptions{ + Paths: []string{"test/path1"}, + TargetPath: "new/location/", + Resources: []provisioning.ResourceRef{ + { + Name: "dashboard-uid", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + }, + } + + mockRepo := &mockReaderWriter{ + MockRepository: repository.NewMockRepository(t), + } + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockWrapFn := repository.NewMockWrapWithStageFn(t) + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + mockRepoResources := resources.NewMockRepositoryResources(t) + mockSyncWorker := jobs.NewMockWorker(t) + + mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(mockRepo, false) + }) + + mockProgress.On("SetTotal", mock.Anything, 2).Return() // 1 path + 1 resource + mockProgress.On("StrictMaxErrors", 1).Return() + mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/dashboard-uid").Return() + mockProgress.On("SetMessage", mock.Anything, "Moving test/path1 to new/location/path1").Return() + mockProgress.On("SetMessage", mock.Anything, "Moving dashboard/file.yaml to new/location/file.yaml").Return() + mockProgress.On("TooManyErrors").Return(nil).Times(2) + + mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepoResources, nil) + mockRepoResources.On("FindResourcePath", mock.Anything, "dashboard-uid", schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }).Return("dashboard/file.yaml", nil) + + mockRepo.On("Move", mock.Anything, "test/path1", "new/location/path1", "", "Move test/path1 to new/location/path1").Return(nil) + mockRepo.On("Move", mock.Anything, "dashboard/file.yaml", "new/location/file.yaml", "", "Move dashboard/file.yaml to new/location/file.yaml").Return(nil) + + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Path == "test/path1" && result.Action == repository.FileActionRenamed && result.Error == nil + })).Return() + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Path == "dashboard/file.yaml" && result.Action == repository.FileActionRenamed && result.Error == nil + })).Return() + + // Add expectations for sync worker (called when ref is empty) + mockProgress.On("ResetResults").Return() + mockProgress.On("SetMessage", mock.Anything, "pull resources").Return() + mockSyncWorker.On("Process", mock.Anything, mockRepo, mock.MatchedBy(func(syncJob provisioning.Job) bool { + return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental + }), mockProgress).Return(nil) + + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory) + err := worker.Process(context.Background(), mockRepo, job, mockProgress) + require.NoError(t, err) +} + +func TestMoveWorker_ProcessResourceReferencesError(t *testing.T) { + job := provisioning.Job{ + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Move: &provisioning.MoveJobOptions{ + TargetPath: "new/location/", + Resources: []provisioning.ResourceRef{ + { + Name: "non-existent-uid", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + }, + } + + mockRepo := &mockReaderWriter{ + MockRepository: repository.NewMockRepository(t), + } + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockWrapFn := repository.NewMockWrapWithStageFn(t) + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + mockRepoResources := resources.NewMockRepositoryResources(t) + + mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(mockRepo, false) + }) + + mockProgress.On("SetTotal", mock.Anything, 1).Return() // 1 resource + mockProgress.On("StrictMaxErrors", 1).Return() + mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() + mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/non-existent-uid").Return() + mockProgress.On("TooManyErrors").Return(nil) + + mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepoResources, nil) + resourceError := errors.New("resource not found") + mockRepoResources.On("FindResourcePath", mock.Anything, "non-existent-uid", schema.GroupVersionKind{ + Group: "dashboard.grafana.app", + Kind: "Dashboard", + }).Return("", resourceError) + + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Name == "non-existent-uid" && result.Group == "dashboard.grafana.app" && + result.Action == repository.FileActionRenamed && + result.Error != nil && result.Error.Error() == "find path for resource dashboard.grafana.app/Dashboard/non-existent-uid: resource not found" + })).Return() + + // Add expectations for sync worker (called when ref is empty) + mockSyncWorker := jobs.NewMockWorker(t) + mockProgress.On("ResetResults").Return() + mockProgress.On("SetMessage", mock.Anything, "pull resources").Return() + mockSyncWorker.On("Process", mock.Anything, mockRepo, mock.MatchedBy(func(syncJob provisioning.Job) bool { + return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental + }), mockProgress).Return(nil) + + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory) + err := worker.Process(context.Background(), mockRepo, job, mockProgress) + require.NoError(t, err) // Should continue despite individual resource errors +} + +func TestMoveWorker_ProcessResourcesFactoryError(t *testing.T) { + job := provisioning.Job{ + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Move: &provisioning.MoveJobOptions{ + TargetPath: "new/location/", + Resources: []provisioning.ResourceRef{ + { + Name: "dashboard-uid", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + }, + } + + mockRepo := &mockReaderWriter{ + MockRepository: repository.NewMockRepository(t), + } + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockWrapFn := repository.NewMockWrapWithStageFn(t) + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + + mockWrapFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(mockRepo, false) + }) + + mockProgress.On("SetTotal", mock.Anything, 1).Return() // 1 resource + mockProgress.On("StrictMaxErrors", 1).Return() + mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() + + factoryError := errors.New("failed to create resources client") + mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(nil, factoryError) + + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + err := worker.Process(context.Background(), mockRepo, job, mockProgress) + require.EqualError(t, err, "move files in repository: create repository resources client: failed to create resources client") +} + +func TestMoveWorker_resolveResourcesToPaths(t *testing.T) { + tests := []struct { + name string + resources []provisioning.ResourceRef + resourcePaths map[string]string + resourceErrors map[string]error + tooManyErrors error + expectedPaths []string + expectedError string + expectContinue bool + }{ + { + name: "single resource success", + resources: []provisioning.ResourceRef{ + { + Name: "dashboard-uid", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + resourcePaths: map[string]string{ + "dashboard-uid": "dashboards/test.json", + }, + expectedPaths: []string{"dashboards/test.json"}, + }, + { + name: "multiple resources success", + resources: []provisioning.ResourceRef{ + { + Name: "dashboard1", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + { + Name: "dashboard2", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + resourcePaths: map[string]string{ + "dashboard1": "dashboards/dash1.json", + "dashboard2": "dashboards/dash2.json", + }, + expectedPaths: []string{"dashboards/dash1.json", "dashboards/dash2.json"}, + }, + { + name: "resource not found continues", + resources: []provisioning.ResourceRef{ + { + Name: "non-existent", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + { + Name: "existing", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + resourcePaths: map[string]string{ + "existing": "dashboards/existing.json", + }, + resourceErrors: map[string]error{ + "non-existent": errors.New("not found"), + }, + expectedPaths: []string{"dashboards/existing.json"}, + expectContinue: true, + }, + { + name: "empty resources list", + resources: []provisioning.ResourceRef{}, + expectedPaths: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRepo := &mockReaderWriter{ + MockRepository: repository.NewMockRepository(t), + } + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) + mockRepoResources := resources.NewMockRepositoryResources(t) + + if len(tt.resources) > 0 { + mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() + mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepoResources, nil) + + for _, resource := range tt.resources { + mockProgress.On("SetMessage", mock.Anything, fmt.Sprintf("Finding path for resource %s/%s/%s", resource.Group, resource.Kind, resource.Name)).Return() + + gvk := schema.GroupVersionKind{ + Group: resource.Group, + Kind: resource.Kind, + } + + if path, ok := tt.resourcePaths[resource.Name]; ok { + mockRepoResources.On("FindResourcePath", mock.Anything, resource.Name, gvk).Return(path, nil) + } else if err, ok := tt.resourceErrors[resource.Name]; ok { + mockRepoResources.On("FindResourcePath", mock.Anything, resource.Name, gvk).Return("", err) + mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Name == resource.Name && result.Group == resource.Group && + result.Action == repository.FileActionRenamed && result.Error != nil + })).Return() + if tt.tooManyErrors != nil { + mockProgress.On("TooManyErrors").Return(tt.tooManyErrors) + } else { + mockProgress.On("TooManyErrors").Return(nil) + } + } + } + } + + worker := NewWorker(nil, nil, mockResourcesFactory) + paths, err := worker.resolveResourcesToPaths(context.Background(), mockRepo, mockProgress, tt.resources) + + if tt.expectedError != "" { + require.EqualError(t, err, tt.expectedError) + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.expectedPaths, paths) + + mockResourcesFactory.AssertExpectations(t) + if len(tt.resources) > 0 { + mockRepoResources.AssertExpectations(t) + } + mockProgress.AssertExpectations(t) + }) + } +} + +func TestMoveWorker_deduplicatePaths(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + name: "no duplicates", + input: []string{"path1", "path2", "path3"}, + expected: []string{"path1", "path2", "path3"}, + }, + { + name: "with duplicates", + input: []string{"path1", "path2", "path1", "path3", "path2"}, + expected: []string{"path1", "path2", "path3"}, + }, + { + name: "empty slice", + input: []string{}, + expected: []string{}, + }, + { + name: "single item", + input: []string{"path1"}, + expected: []string{"path1"}, + }, + { + name: "all duplicates", + input: []string{"path1", "path1", "path1"}, + expected: []string{"path1"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := deduplicatePaths(tt.input) + require.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 6d87177b247..9bccd675dd5 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -619,7 +619,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH ) deleteWorker := deletepkg.NewWorker(syncWorker, stageIfPossible, b.repositoryResources) - moveWorker := movepkg.NewWorker(syncWorker, stageIfPossible) + moveWorker := movepkg.NewWorker(syncWorker, stageIfPossible, b.repositoryResources) workers := []jobs.Worker{ deleteWorker, exportWorker, diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index 006294e1795..4aa160ab2e9 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -3253,6 +3253,18 @@ "description": "Ref to the branch or commit hash that should move", "type": "string" }, + "resources": { + "description": "Resources to move This option has been created because currently the frontend does not use standarized app platform APIs. For performance and API consistency reasons, the preferred option is it to use the paths.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ResourceRef" + } + ] + } + }, "targetPath": { "description": "Destination path for the move (e.g. \"new-location/\")", "type": "string" diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index 2bf6a9fba70..a7462a2498c 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -1568,6 +1568,200 @@ func TestIntegrationProvisioning_MoveJob(t *testing.T) { assert.Equal(collect, "error", state, "move job should have failed due to missing target path") }, time.Second*10, time.Millisecond*100, "Expected move job to fail with error state") }) + + t.Run("move by resource reference", func(t *testing.T) { + // Create a unique repository for resource reference testing to avoid contamination + const refRepo = "move-ref-test-repo" + localRefTmp := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{ + "Name": refRepo, + "SyncEnabled": true, + "SyncTarget": "folder", + }) + _, err := helper.Repositories.Resource.Create(ctx, localRefTmp, metav1.CreateOptions{}) + require.NoError(t, err) + + // Create modified test files with unique UIDs for ResourceRef testing + allPanelsContent := helper.LoadFile("testdata/all-panels.json") + textOptionsContent := helper.LoadFile("testdata/text-options.json") + timelineDemoContent := helper.LoadFile("testdata/timeline-demo.json") + + // Modify UIDs to be unique for ResourceRef tests + allPanelsModified := strings.Replace(string(allPanelsContent), `"uid": "n1jR8vnnz"`, `"uid": "moveref1"`, 1) + textOptionsModified := strings.Replace(string(textOptionsContent), `"uid": "WZ7AhQiVz"`, `"uid": "moveref2"`, 1) + timelineDemoModified := strings.Replace(string(timelineDemoContent), `"uid": "mIJjFy8Kz"`, `"uid": "moveref3"`, 1) + + // Create temporary files and copy them to the provisioning path + tmpDir := t.TempDir() + tmpFile1 := filepath.Join(tmpDir, "move-ref-test-1.json") + tmpFile2 := filepath.Join(tmpDir, "move-ref-test-2.json") + tmpFile3 := filepath.Join(tmpDir, "move-ref-test-3.json") + + require.NoError(t, os.WriteFile(tmpFile1, []byte(allPanelsModified), 0644)) + require.NoError(t, os.WriteFile(tmpFile2, []byte(textOptionsModified), 0644)) + require.NoError(t, os.WriteFile(tmpFile3, []byte(timelineDemoModified), 0644)) + + // Copy files to provisioning path to set up test - use refRepo's path + helper.CopyToProvisioningPath(t, tmpFile1, "move-source-1.json") + helper.CopyToProvisioningPath(t, tmpFile2, "move-source-2.json") + helper.CopyToProvisioningPath(t, tmpFile3, "move-source-3.json") + + // Sync to populate resources in Grafana + helper.SyncAndWait(t, refRepo, nil) + + t.Run("move single dashboard by resource reference", func(t *testing.T) { + // Create move job for single dashboard using ResourceRef + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(refRepo). + SubResource("jobs"). + Body(asJSON(&provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Move: &provisioning.MoveJobOptions{ + TargetPath: "moved-by-ref/", + Resources: []provisioning.ResourceRef{ + { + Name: "moveref1", // UID from modified all-panels.json + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + })). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error(), "should be able to create move job with ResourceRef") + + // Wait for job to complete + helper.AwaitJobs(t, refRepo) + + // Verify corresponding file is moved in repository + _, err = helper.Repositories.Resource.Get(ctx, refRepo, metav1.GetOptions{}, "files", "moved-by-ref", "move-source-1.json") + require.NoError(t, err, "file should be moved to new location in repository") + + // Verify original file is deleted from repository + _, err = helper.Repositories.Resource.Get(ctx, refRepo, metav1.GetOptions{}, "files", "move-source-1.json") + require.Error(t, err, "original file should be deleted from repository") + require.True(t, apierrors.IsNotFound(err), "should be not found error") + + // Verify dashboard still exists in Grafana (should be updated after sync) + helper.SyncAndWait(t, refRepo, nil) + _, err = helper.DashboardsV1.Resource.Get(ctx, "moveref1", metav1.GetOptions{}) + require.NoError(t, err, "dashboard should still exist in Grafana after move") + + // Verify other resource still exists in original location + _, err = helper.Repositories.Resource.Get(ctx, refRepo, metav1.GetOptions{}, "files", "move-source-2.json") + require.NoError(t, err, "other files should still exist in original location") + }) + + t.Run("move multiple resources by reference", func(t *testing.T) { + // Create move job for remaining resource using ResourceRef + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(refRepo). + SubResource("jobs"). + Body(asJSON(&provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Move: &provisioning.MoveJobOptions{ + TargetPath: "archived-by-ref/", + Resources: []provisioning.ResourceRef{ + { + Name: "moveref2", // UID from modified text-options.json + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + })). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error(), "should be able to create move job with ResourceRef") + + // Wait for job to complete + helper.AwaitJobs(t, refRepo) + + // Verify file is moved in repository + _, err = helper.Repositories.Resource.Get(ctx, refRepo, metav1.GetOptions{}, "files", "archived-by-ref", "move-source-2.json") + require.NoError(t, err, "file should be moved to new location") + + // Verify original file is deleted from repository + _, err = helper.Repositories.Resource.Get(ctx, refRepo, metav1.GetOptions{}, "files", "move-source-2.json") + require.Error(t, err, "original file should be deleted from repository") + require.True(t, apierrors.IsNotFound(err), "should be not found error") + + // Verify dashboard still exists in Grafana after sync + helper.SyncAndWait(t, refRepo, nil) + _, err = helper.DashboardsV1.Resource.Get(ctx, "moveref2", metav1.GetOptions{}) + require.NoError(t, err, "dashboard should still exist in Grafana after move") + }) + + t.Run("mixed move - paths and resources", func(t *testing.T) { + // Setup fresh resources for mixed test + tmpMixed1 := filepath.Join(tmpDir, "mixed-move-1.json") + tmpMixed2 := filepath.Join(tmpDir, "mixed-move-2.json") + + allPanelsMixed := strings.Replace(string(allPanelsContent), `"uid": "n1jR8vnnz"`, `"uid": "mixedmove1"`, 1) + textOptionsMixed := strings.Replace(string(textOptionsContent), `"uid": "WZ7AhQiVz"`, `"uid": "mixedmove2"`, 1) + + require.NoError(t, os.WriteFile(tmpMixed1, []byte(allPanelsMixed), 0644)) + require.NoError(t, os.WriteFile(tmpMixed2, []byte(textOptionsMixed), 0644)) + + helper.CopyToProvisioningPath(t, tmpMixed1, "mixed-move-1.json") // UID: mixedmove1 + helper.CopyToProvisioningPath(t, tmpMixed2, "mixed-move-2.json") // UID: mixedmove2 + + helper.SyncAndWait(t, refRepo, nil) + + // Create move job that combines both paths and resource references + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(refRepo). + SubResource("jobs"). + Body(asJSON(&provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Move: &provisioning.MoveJobOptions{ + TargetPath: "mixed-target/", + Paths: []string{"mixed-move-1.json"}, // Move by path + Resources: []provisioning.ResourceRef{ + { + Name: "mixedmove2", // Move by resource reference + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + })). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error(), "should be able to create mixed move job") + + // Wait for job to complete + helper.AwaitJobs(t, refRepo) + + // Verify both targeted resources are moved in repository + _, err = helper.Repositories.Resource.Get(ctx, refRepo, metav1.GetOptions{}, "files", "mixed-target", "mixed-move-1.json") + require.NoError(t, err, "file moved by path should exist at new location") + + _, err = helper.Repositories.Resource.Get(ctx, refRepo, metav1.GetOptions{}, "files", "mixed-target", "mixed-move-2.json") + require.NoError(t, err, "file moved by resource ref should exist at new location") + + // Verify files are deleted from original locations + _, err = helper.Repositories.Resource.Get(ctx, refRepo, metav1.GetOptions{}, "files", "mixed-move-1.json") + require.Error(t, err, "file moved by path should be deleted from original location") + + _, err = helper.Repositories.Resource.Get(ctx, refRepo, metav1.GetOptions{}, "files", "mixed-move-2.json") + require.Error(t, err, "file moved by resource ref should be deleted from original location") + + // Verify dashboards still exist in Grafana after sync + helper.SyncAndWait(t, refRepo, nil) + _, err = helper.DashboardsV1.Resource.Get(ctx, "mixedmove1", metav1.GetOptions{}) + require.NoError(t, err, "dashboard moved by path should still exist in Grafana") + + _, err = helper.DashboardsV1.Resource.Get(ctx, "mixedmove2", metav1.GetOptions{}) + require.NoError(t, err, "dashboard moved by resource ref should still exist in Grafana") + }) + }) } func TestIntegrationProvisioning_MoveResources(t *testing.T) { @@ -1850,4 +2044,66 @@ func TestIntegrationProvisioning_MoveResources(t *testing.T) { require.Error(t, result.Error(), "should fail when source file doesn't exist") }) }) + + t.Run("move non-existent resource by reference", func(t *testing.T) { + // Create move job for non-existent resource + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(asJSON(&provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Move: &provisioning.MoveJobOptions{ + TargetPath: "moved-nonexistent/", + Resources: []provisioning.ResourceRef{ + { + Name: "non-existent-move-uid", + Kind: "Dashboard", + Group: "dashboard.grafana.app", + }, + }, + }, + })). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error(), "should be able to create move job") + + // Wait for job to complete - should record error but continue + require.EventuallyWithT(t, func(collect *assert.CollectT) { + list := &unstructured.UnstructuredList{} + err := helper.AdminREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Do(ctx).Into(list) + assert.NoError(collect, err, "should be able to list jobs") + assert.NotEmpty(collect, list.Items, "expect at least one job") + + // Find the most recent move job + var moveJob *unstructured.Unstructured + for _, elem := range list.Items { + assert.Equal(collect, repo, elem.GetLabels()["provisioning.grafana.app/repository"], "should have repo label") + + action := mustNestedString(elem.Object, "spec", "action") + if action == "move" { + // Get the most recent one (they should be ordered by creation time) + moveJob = &elem + } + } + if !assert.NotNil(collect, moveJob, "should find a move job") { + return + } + + state := mustNestedString(moveJob.Object, "status", "state") + // The job should complete but record errors for individual resource resolution failures + if state == "error" || state == "completed" || state == "success" { + // Any of these states is acceptable - the key is that resource resolution errors are recorded + // and don't fail the entire job due to error-tolerant implementation + return + } + assert.Fail(collect, "job should complete or error, but got state: %s", state) + }, time.Second*10, time.Millisecond*100, "Expected move job to handle non-existent resource") + }) } diff --git a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts index 0b239287e85..63f4e3af895 100644 --- a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts @@ -785,6 +785,8 @@ export type MoveJobOptions = { paths?: string[]; /** Ref to the branch or commit hash that should move */ ref?: string; + /** Resources to move This option has been created because currently the frontend does not use standarized app platform APIs. For performance and API consistency reasons, the preferred option is it to use the paths. */ + resources?: ResourceRef[]; /** Destination path for the move (e.g. "new-location/") */ targetPath?: string; }; From a81c28b1533e76c400f7c650f01101e24832af05 Mon Sep 17 00:00:00 2001 From: Bruno Abrantes Date: Wed, 30 Jul 2025 10:37:10 +0200 Subject: [PATCH 031/126] chore: include stackId when marking a dashboard as rejected during migration (#108887) Signed-off-by: Bruno Abrantes --- pkg/registry/apis/dashboard/legacy/migrate.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/registry/apis/dashboard/legacy/migrate.go b/pkg/registry/apis/dashboard/legacy/migrate.go index bb938f7e532..3da3a01aab8 100644 --- a/pkg/registry/apis/dashboard/legacy/migrate.go +++ b/pkg/registry/apis/dashboard/legacy/migrate.go @@ -32,8 +32,9 @@ type MigrateOptions struct { LargeObjects apistore.LargeObjectSupport BlobStore resourcepb.BlobStoreClient Resources []schema.GroupResource - WithHistory bool // only applies to dashboards - OnlyCount bool // just count the values + WithHistory bool // only applies to dashboards + OnlyCount bool // just count the values + StackID string // stack identifier for logging Progress func(count int, msg string) } @@ -135,6 +136,7 @@ func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) ( // Now run each migration blobStore := BlobStoreInfo{} + opts.StackID = fmt.Sprintf("%d", info.StackID) // Pass stack ID through options a.log.Info("start migrating legacy resources", "namespace", opts.Namespace, "orgId", info.OrgID, "stackId", info.StackID) for _, m := range migratorFuncs { blobs, err := m(ctx, info.OrgID, opts, stream) @@ -310,6 +312,7 @@ func (a *dashboardSqlAccess) migrateDashboards(ctx context.Context, orgId int64, "dashboard", row.Dash.Name, "uid", row.Dash.UID, "id", id, + "stackId", opts.StackID, "namespace", opts.Namespace, ) opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name)) From b6819a22124d8e1bc0c0afe4ee43e195677c7db4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 08:58:57 +0000 Subject: [PATCH 032/126] Update dependency react-i18next to v15.6.1 (#108890) 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 52441f3f4a4..dab33c9e9bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26874,8 +26874,8 @@ __metadata: linkType: hard "react-i18next@npm:^15.0.0": - version: 15.6.0 - resolution: "react-i18next@npm:15.6.0" + version: 15.6.1 + resolution: "react-i18next@npm:15.6.1" dependencies: "@babel/runtime": "npm:^7.27.6" html-parse-stringify: "npm:^3.0.1" @@ -26890,7 +26890,7 @@ __metadata: optional: true typescript: optional: true - checksum: 10/d8549c364eb32ea078be0e87df35ee23b8c9d6a30af4fe2a41099c12f62b5ce4268f9997132a9b5abcb4556e30fb37f4ba7f5548429333548b5b9856627c6a50 + checksum: 10/f8da83078eabbd4eadf848754881fb65a5c97de72f778b473a4a64add18d4b39794a1da9c95cce827ce30d5236328807228dfbe566dbc355cd0b46d3cbb110c3 languageName: node linkType: hard From 6c3233e7754f895b2133dab96a690768241d8b8a Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:01:27 +0200 Subject: [PATCH 033/126] Scopes: Proper usage of checkbox label (#108892) * Proper use of label for checkbox * Cleanup --- .../app/features/scopes/selector/ScopesTreeItem.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/public/app/features/scopes/selector/ScopesTreeItem.tsx b/public/app/features/scopes/selector/ScopesTreeItem.tsx index 211991b353b..9640c4f7e85 100644 --- a/public/app/features/scopes/selector/ScopesTreeItem.tsx +++ b/public/app/features/scopes/selector/ScopesTreeItem.tsx @@ -63,7 +63,7 @@ export function ScopesTreeItem({ id={treeNode.scopeNodeId} name={treeNode.scopeNodeId} checked={selected} - label="" + label={isExpandable ? '' : scopeNode.spec.title} data-testid={`scopes-tree-${treeNode.scopeNodeId}-radio`} onClick={() => { selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId); @@ -73,6 +73,7 @@ export function ScopesTreeItem({ { selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId); }} @@ -80,7 +81,7 @@ export function ScopesTreeItem({ ) ) : null} - {isExpandable ? ( + {isExpandable && (
@@ -130,8 +129,10 @@ const getStyles = (theme: GrafanaTheme2) => { lineHeight: theme.typography.pxToRem(22), padding: theme.spacing(0.5, 0), - '& > label': css({ - gap: 0, + '& > label :last-child': css({ + fontSize: theme.typography.pxToRem(14), + lineHeight: theme.typography.pxToRem(22), + fontWeight: theme.typography.fontWeightRegular, }), }), titlePadding: css({ From e10063b0c7f879e15d7ee9857af68f9a537efe2e Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Wed, 30 Jul 2025 11:16:33 +0200 Subject: [PATCH 034/126] Azure: Bump Azure SDK (#108859) * Bump Azure SDK * Workspace sync --- apps/advisor/go.mod | 10 +++++----- apps/advisor/go.sum | 24 ++++++++++++------------ apps/alerting/notifications/go.mod | 2 +- apps/alerting/notifications/go.sum | 4 ++-- apps/dashboard/go.mod | 2 +- apps/dashboard/go.sum | 4 ++-- apps/investigations/go.mod | 4 ++-- apps/investigations/go.sum | 20 ++++++++++---------- apps/playlist/go.mod | 2 +- apps/playlist/go.sum | 4 ++-- apps/secret/go.mod | 2 +- apps/secret/go.sum | 4 ++-- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ go.work.sum | 14 ++++++++++++++ pkg/aggregator/go.mod | 2 +- pkg/aggregator/go.sum | 4 ++-- pkg/apimachinery/go.mod | 2 +- pkg/apimachinery/go.sum | 4 ++-- pkg/apiserver/go.mod | 2 +- pkg/apiserver/go.sum | 4 ++-- pkg/build/go.mod | 2 +- pkg/build/go.sum | 4 ++-- pkg/promlib/go.mod | 7 ++++--- pkg/promlib/go.sum | 16 ++++++++-------- 25 files changed, 97 insertions(+), 82 deletions(-) diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 09013e3c916..4559d1045d9 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -27,8 +27,8 @@ require ( cloud.google.com/go/compute/metadata v0.7.0 // indirect dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect @@ -113,7 +113,7 @@ require ( github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.3 // indirect github.com/golang-migrate/migrate/v4 v4.7.0 // indirect github.com/golang/mock v1.7.0-rc.1 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -130,7 +130,7 @@ require ( github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 // indirect github.com/grafana/grafana-aws-sdk v1.0.4 // indirect - github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect + github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // indirect github.com/grafana/grafana/pkg/apiserver v0.0.0-20250627191313-2f1a6ae1712b // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect @@ -267,7 +267,7 @@ require ( gonum.org/v1/gonum v0.16.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.0 // indirect + google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 1547cfd7ddf..b83026226ad 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -78,10 +78,10 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= @@ -518,8 +518,8 @@ github.com/gogo/status v1.1.1 h1:DuHXlSFHNKqTQ+/ACf5Vs6r4X/dH2EgIzR9Vr+H65kg= github.com/gogo/status v1.1.1/go.mod h1:jpG3dM5QPcqu19Hg8lkUhBFBa3TcLs1DG7+2Jqci7oU= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo= @@ -669,8 +669,8 @@ github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0 github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana-aws-sdk v1.0.4 h1:D14UAehsOqpjliHmHzveRQ1p43KCsMzdmb7GovWj+SY= github.com/grafana/grafana-aws-sdk v1.0.4/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= -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-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA= +github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0/go.mod h1:H9sVh9A4yg5egMGZeh0mifxT1Q/uqwKe1LBjBJU6pN8= github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho= github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= github.com/grafana/grafana/apps/dashboard v0.0.0-20250716132114-6fd75ebc5441 h1:+TSbaxCXBZrKkdROWBzdWna8uStE1f9LYd7GiqjVfz8= @@ -1054,8 +1054,8 @@ github.com/prometheus/prometheus v0.303.1/go.mod h1:WEq2ogBPZoLjj9x5K67VEk7ECR0n github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= 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= @@ -1821,8 +1821,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index ac5f43ee6bd..8fd63458f4f 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -93,7 +93,7 @@ require ( google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.0 // indirect + google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 8f46c0338ee..e3c83758737 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -333,8 +333,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 423dfbc0986..8bbf2e9f607 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -122,7 +122,7 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.0 // indirect + google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 95d9b4b63a0..4f725783309 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -370,8 +370,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1: google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 7ec24ff7bb8..33eb4e290ac 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -84,7 +84,7 @@ require ( github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 // indirect github.com/grafana/grafana-app-sdk/logging v0.40.0 // indirect github.com/grafana/grafana-aws-sdk v1.0.4 // indirect - github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect + github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // indirect github.com/grafana/grafana-plugin-sdk-go v0.278.0 // indirect github.com/grafana/grafana/pkg/apiserver v0.0.0-20250627191313-2f1a6ae1712b // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect @@ -201,7 +201,7 @@ require ( gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.0 // indirect + google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 5e621d587ec..88dfe75aef3 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -8,10 +8,10 @@ dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= @@ -269,8 +269,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -347,8 +347,8 @@ github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0 github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana-aws-sdk v1.0.4 h1:D14UAehsOqpjliHmHzveRQ1p43KCsMzdmb7GovWj+SY= github.com/grafana/grafana-aws-sdk v1.0.4/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= -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-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA= +github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0/go.mod h1:H9sVh9A4yg5egMGZeh0mifxT1Q/uqwKe1LBjBJU6pN8= github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho= github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= github.com/grafana/grafana/apps/dashboard v0.0.0-20250716132114-6fd75ebc5441 h1:+TSbaxCXBZrKkdROWBzdWna8uStE1f9LYd7GiqjVfz8= @@ -927,8 +927,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go. google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 0198da5b976..2cf20613fc8 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -75,7 +75,7 @@ require ( gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.0 // indirect + google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 5d053231eb3..b061911f7b2 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -197,8 +197,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1: google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/apps/secret/go.mod b/apps/secret/go.mod index b47cd2748cc..7f0517a504a 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -6,7 +6,7 @@ require ( github.com/grafana/grafana-app-sdk v0.40.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.10.0 - google.golang.org/grpc v1.74.0 + google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.33.3 diff --git a/apps/secret/go.sum b/apps/secret/go.sum index eb2dfb8c73e..78d210e14a9 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -162,8 +162,8 @@ 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= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go.mod b/go.mod index b59ccb1643d..dc72d4b349b 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,8 @@ require ( filippo.io/age v1.2.1 // @grafana/identity-access-team github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2 // @grafana/grafana-search-and-storage github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // @grafana/partner-datasources - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // @grafana/identity-access-team - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // @grafana/grafana-backend-group + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // @grafana/identity-access-team + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // @grafana/grafana-backend-group github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // @grafana/grafana-backend-group github.com/Azure/azure-storage-blob-go v0.15.0 // @grafana/grafana-backend-group github.com/Azure/go-autorest/autorest v0.11.29 // @grafana/grafana-backend-group @@ -98,7 +98,7 @@ require ( github.com/grafana/grafana-app-sdk v0.40.1 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-app-sdk/logging v0.40.0 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-aws-sdk v1.0.4 // @grafana/aws-datasources - github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // @grafana/partner-datasources + github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.4.1 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group @@ -158,7 +158,7 @@ require ( github.com/prometheus/common v0.65.0 // @grafana/alerting-backend github.com/prometheus/prometheus v0.303.1 // @grafana/alerting-backend github.com/prometheus/sigv4 v0.1.2 // @grafana/alerting-backend - github.com/redis/go-redis/v9 v9.7.3 // @grafana/alerting-backend + github.com/redis/go-redis/v9 v9.8.0 // @grafana/alerting-backend github.com/robfig/cron/v3 v3.0.1 // @grafana/grafana-backend-group github.com/rs/cors v1.11.1 // @grafana/identity-access-team github.com/russellhaering/goxmldsig v1.4.0 // @grafana/grafana-backend-group @@ -206,7 +206,7 @@ require ( golang.org/x/tools v0.35.0 // indirect; @grafana/grafana-as-code gonum.org/v1/gonum v0.16.0 // @grafana/oss-big-tent google.golang.org/api v0.235.0 // @grafana/grafana-backend-group - google.golang.org/grpc v1.74.0 // @grafana/plugins-platform-backend + google.golang.org/grpc v1.74.2 // @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.6 // @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 @@ -390,7 +390,7 @@ require ( github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/status v1.1.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.3 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect diff --git a/go.sum b/go.sum index 936fd459dcc..46799064e05 100644 --- a/go.sum +++ b/go.sum @@ -648,10 +648,10 @@ github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl github.com/Azure/azure-sdk-for-go v23.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= @@ -1385,8 +1385,8 @@ github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= @@ -1606,8 +1606,8 @@ github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0 github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana-aws-sdk v1.0.4 h1:D14UAehsOqpjliHmHzveRQ1p43KCsMzdmb7GovWj+SY= github.com/grafana/grafana-aws-sdk v1.0.4/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= -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-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA= +github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0/go.mod h1:H9sVh9A4yg5egMGZeh0mifxT1Q/uqwKe1LBjBJU6pN8= github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 h1:JOzchPgptwJdruYoed7x28lFDwhzs7kssResYsnC0iI= github.com/grafana/grafana-cloud-migration-snapshot v1.9.0/go.mod h1:nOHgq4Oa829qmBKA5KIXw5Ipo3rhLs0d6A8UI9Nw8Zk= github.com/grafana/grafana-google-sdk-go v0.4.1 h1:QdHmgDzlV3RzBTvIxd+WuxER1+afnFzUmEivbwDo27E= @@ -2278,8 +2278,8 @@ github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d/go.mod h1 github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/redis/rueidis v1.0.56 h1:DwPjFIgas1OMU/uCqBELOonu9TKMYt3MFPq6GtwEWNY= github.com/redis/rueidis v1.0.56/go.mod h1:g660/008FMYmAF46HG4lmcpcgFNj+jCjCAZUUM+wEbs= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -3493,8 +3493,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/go.work.sum b/go.work.sum index 3eb2ae972fc..4cf90b4d4d2 100644 --- a/go.work.sum +++ b/go.work.sum @@ -515,6 +515,9 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk= github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.1 h1:o/Ws6bEqMeKZUfj1RRm3mQ51O8JGU5w+Qdg2AhHib6A= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.1/go.mod h1:6QAMYBAbQeeKX+REFJMZ1nFWu9XLw/PPcjYpuc9RDFs= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 h1:JNgM3Tz592fUHU2vgwgvOgKxo5s9Ki0y2wicBeckn70= @@ -826,6 +829,7 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/expr-lang/expr v1.17.0/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= @@ -1270,6 +1274,7 @@ github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8 github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 h1:xoIK0ctDddBMnc74udxJYBqlo9Ylnsp1waqjLsnef20= github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= @@ -1287,6 +1292,7 @@ github.com/relvacode/iso8601 v1.6.0 h1:eFXUhMJN3Gz8Rcq82f9DTMW0svjtAVuIEULglM7QH github.com/relvacode/iso8601 v1.6.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/richardartoul/molecule v1.0.0 h1:+LFA9cT7fn8KF39zy4dhOnwcOwRoqKiBkPqKqya+8+U= github.com/richardartoul/molecule v1.0.0/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= @@ -1598,7 +1604,9 @@ go.opentelemetry.io/otel/bridge/opentracing v1.35.0/go.mod h1:p5CbIL4v7uQz7mnQD6 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= go.opentelemetry.io/otel/exporters/prometheus v0.58.0/go.mod h1:7qo/4CLI+zYSNbv0GMNquzuss2FVZo3OYrGh96n4HNc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= @@ -1627,6 +1635,7 @@ go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= @@ -1643,6 +1652,7 @@ golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0Y golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 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= @@ -1686,6 +1696,7 @@ golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -1708,6 +1719,7 @@ golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= @@ -1797,6 +1809,8 @@ google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index a6f393daf08..c601d3a8891 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -152,7 +152,7 @@ require ( google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.0 // indirect + google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index ec1d4f04729..a402844e7b9 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -499,8 +499,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index bdfcc22c5af..d1fa3b6f5e0 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -49,7 +49,7 @@ require ( golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.0 // indirect + google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 63fa5245e75..df48bc9bb77 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -156,8 +156,8 @@ 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= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index a40429d1f8d..24e2c11626c 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -94,7 +94,7 @@ require ( google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.0 // indirect + google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 2b015943a28..5547406bf98 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -345,8 +345,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index a10b831a97a..012923fe3c1 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -32,7 +32,7 @@ require ( golang.org/x/text v0.27.0 // indirect; @grafana/grafana-backend-group golang.org/x/time v0.11.0 // indirect; @grafana/grafana-backend-group google.golang.org/api v0.235.0 // @grafana/grafana-backend-group - google.golang.org/grpc v1.74.0 // indirect; @grafana/plugins-platform-backend + google.golang.org/grpc v1.74.2 // indirect; @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.6 // indirect; @grafana/plugins-platform-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend ) diff --git a/pkg/build/go.sum b/pkg/build/go.sum index ead0b353d86..d49747c61fb 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -388,8 +388,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index b30b3016532..e2355d03c5f 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -17,8 +17,8 @@ require ( ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/apache/arrow-go/v18 v18.3.0 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect @@ -47,6 +47,7 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.3 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/gnostic-models v0.6.9 // indirect @@ -127,7 +128,7 @@ require ( google.golang.org/api v0.235.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.0 // indirect + google.golang.org/grpc v1.74.2 // 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 e348ab465d9..5973252c3ba 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -5,10 +5,10 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= @@ -105,8 +105,8 @@ github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0 github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -415,8 +415,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1: google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From d5bcc606b0c87ba3817c32d6f585f96bc8b4c82e Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Wed, 30 Jul 2025 10:20:39 +0100 Subject: [PATCH 035/126] Trace View: Header redesign (#108473) * Improve trace view header design * Added plugin extension for more actions * Translations * Tweaks based on feedback * lint * Limit to 2 links per plugin * Add datasource to context so LLM has less to figure out * Fix conflict :) * Remove unused file * i18n extract * Updated tests * lint * Re-add the feedback link * i18n-extract * fix tests --------- Co-authored-by: Joey --- .../alerting/v0alpha1/receiver_schema_gen.go | 2 +- .../v0alpha1/templategroup_schema_gen.go | 2 +- .../v0alpha1/timeinterval_schema_gen.go | 2 +- .../pkg/apis/alerting_manifest.go | 2 - apps/dashboard/pkg/apis/dashboard_manifest.go | 2 - apps/folder/pkg/apis/folder_manifest.go | 2 - apps/secret/pkg/apis/secret_manifest.go | 2 - .../src/types/pluginExtensions.ts | 1 + .../features/explore/TraceView/TraceView.tsx | 3 + .../Actions/TracePageActions.tsx | 107 ---- .../TracePageHeader/TracePageHeader.test.tsx | 443 ++++++++++++++- .../TracePageHeader/TracePageHeader.tsx | 519 ++++++++++++------ .../TraceView/components/types/trace.ts | 9 + public/locales/en-US/grafana.json | 28 +- 14 files changed, 817 insertions(+), 307 deletions(-) delete mode 100644 public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_schema_gen.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_schema_gen.go index acda5c7d1c9..314d36b602a 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_schema_gen.go @@ -13,7 +13,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &Receiver{}, &ReceiverList{}, resource.WithKind("Receiver"), - resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{ + resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ FieldSelector: "spec.title", FieldValueFunc: func(o resource.Object) (string, error) { cast, ok := o.(*Receiver) diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_schema_gen.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_schema_gen.go index 339d0bd6d04..509bfc4aeca 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_schema_gen.go @@ -13,7 +13,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( schemaTemplateGroup = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TemplateGroup{}, &TemplateGroupList{}, resource.WithKind("TemplateGroup"), - resource.WithPlural("templategroups"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{ + resource.WithPlural("templategroups"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ FieldSelector: "spec.title", FieldValueFunc: func(o resource.Object) (string, error) { cast, ok := o.(*TemplateGroup) diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_schema_gen.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_schema_gen.go index a6c809e0d3b..8c5459f0059 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_schema_gen.go @@ -13,7 +13,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( schemaTimeInterval = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TimeInterval{}, &TimeIntervalList{}, resource.WithKind("TimeInterval"), - resource.WithPlural("timeintervals"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{ + resource.WithPlural("timeintervals"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ FieldSelector: "spec.name", FieldValueFunc: func(o resource.Object) (string, error) { cast, ok := o.(*TimeInterval) diff --git a/apps/alerting/notifications/pkg/apis/alerting_manifest.go b/apps/alerting/notifications/pkg/apis/alerting_manifest.go index 40f05004004..bde16bfb81d 100644 --- a/apps/alerting/notifications/pkg/apis/alerting_manifest.go +++ b/apps/alerting/notifications/pkg/apis/alerting_manifest.go @@ -15,8 +15,6 @@ import ( v0alpha1 "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" ) -var () - var appManifestData = app.ManifestData{ AppName: "alerting", Group: "notifications.alerting.grafana.app", diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index 2a3c0408c29..591ce095303 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -18,8 +18,6 @@ import ( v2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" ) -var () - var appManifestData = app.ManifestData{ AppName: "dashboard", Group: "dashboard.grafana.app", diff --git a/apps/folder/pkg/apis/folder_manifest.go b/apps/folder/pkg/apis/folder_manifest.go index 8628efe6537..e6231165862 100644 --- a/apps/folder/pkg/apis/folder_manifest.go +++ b/apps/folder/pkg/apis/folder_manifest.go @@ -15,8 +15,6 @@ import ( v1beta1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" ) -var () - var appManifestData = app.ManifestData{ AppName: "folder", Group: "folder.grafana.app", diff --git a/apps/secret/pkg/apis/secret_manifest.go b/apps/secret/pkg/apis/secret_manifest.go index e8ff6dbca3a..512e381f77d 100644 --- a/apps/secret/pkg/apis/secret_manifest.go +++ b/apps/secret/pkg/apis/secret_manifest.go @@ -15,8 +15,6 @@ import ( v1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" ) -var () - var appManifestData = app.ManifestData{ AppName: "secret", Group: "secret.grafana.app", diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index e64bf16cfdb..32764409a8d 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -197,6 +197,7 @@ export enum PluginExtensionPoints { ExploreToolbarAction = 'grafana/explore/toolbar/action', UserProfileTab = 'grafana/user/profile/tab', TraceViewDetails = 'grafana/traceview/details', + TraceViewHeaderActions = 'grafana/traceview/header/actions', QueryEditorRowAdaptiveTelemetryV1 = 'grafana/query-editor-row/adaptivetelemetry/v1', TraceViewResourceAttributes = 'grafana/traceview/resource-attributes', LogsViewResourceAttributes = 'grafana/logsview/resource-attributes', diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index 19203ec818a..28a47518b81 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -167,6 +167,7 @@ export function TraceView(props: Props) { ); const timeZone = useSelector((state) => getTimeZone(state.user)); const datasourceType = datasource ? datasource?.type : 'unknown'; + const datasourceName = datasource ? datasource?.name : 'unknown'; const datasourceUid = datasource ? datasource?.uid : ''; const scrollElement = props.scrollElement ? props.scrollElement @@ -189,6 +190,8 @@ export function TraceView(props: Props) { setFocusedSpanIdForSearch={setFocusedSpanIdForSearch} spanFilterMatches={spanFilterMatches} datasourceType={datasourceType} + datasourceName={datasourceName} + datasourceUid={datasourceUid} setHeaderHeight={setHeaderHeight} app={exploreId ? CoreApp.Explore : CoreApp.Unknown} /> diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx deleted file mode 100644 index e1448a77f69..00000000000 --- a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { css } from '@emotion/css'; -import { useState } from 'react'; - -import { GrafanaTheme2, CoreApp, DataFrame } from '@grafana/data'; -import { Trans, t } from '@grafana/i18n'; -import { reportInteraction } from '@grafana/runtime'; -import { Icon, useTheme2 } from '@grafana/ui'; - -import { config } from '../../../../../../core/config'; -import { downloadTraceAsJson } from '../../../../../inspector/utils/download'; - -import ActionButton from './ActionButton'; - -export const getStyles = (theme: GrafanaTheme2) => { - return { - TracePageActions: css({ - label: 'TracePageActions', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - gap: '4px', - marginBottom: '10px', - }), - feedbackContainer: css({ - color: theme.colors.text.link, - }), - feedback: css({ - margin: '6px', - color: theme.colors.text.link, - fontSize: theme.typography.bodySmall.fontSize, - '&:hover': { - textDecoration: 'underline', - }, - }), - }; -}; - -export type TracePageActionsProps = { - traceId: string; - data: DataFrame; - app?: CoreApp; -}; - -export default function TracePageActions(props: TracePageActionsProps) { - const { traceId, data, app } = props; - const theme = useTheme2(); - - const styles = getStyles(theme); - const [copyTraceIdClicked, setCopyTraceIdClicked] = useState(false); - - const copyTraceId = () => { - navigator.clipboard.writeText(traceId); - setCopyTraceIdClicked(true); - setTimeout(() => { - setCopyTraceIdClicked(false); - }, 5000); - }; - - const exportTrace = () => { - const traceFormat = downloadTraceAsJson(data, 'Trace-' + traceId.substring(traceId.length - 6)); - reportInteraction('grafana_traces_download_traces_clicked', { - app, - grafana_version: config.buildInfo.version, - trace_format: traceFormat, - location: 'trace-view', - }); - }; - - return ( -
- {config.feedbackLinksEnabled && ( - - )} - - - -
- ); -} diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx index 95d78fff092..97fc884d659 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx @@ -12,15 +12,86 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { getByText, render } from '@testing-library/react'; +import { fireEvent, getByText, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; -import { MutableDataFrame } from '@grafana/data'; +import { + IconName, + MutableDataFrame, + PluginExtensionLink, + PluginExtensionPoints, + PluginExtensionTypes, +} from '@grafana/data'; +import { usePluginLinks } from '@grafana/runtime'; import { DEFAULT_SPAN_FILTERS } from 'app/features/explore/state/constants'; +import { TraceViewPluginExtensionContext } from '../types/trace'; + import { TracePageHeader } from './TracePageHeader'; import { trace } from './mocks'; -const setup = () => { +// Mock @grafana/runtime +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + usePluginLinks: jest.fn(), + reportInteraction: jest.fn(), +})); + +// Mock useAppNotification +jest.mock('app/core/copy/appNotification', () => ({ + useAppNotification: jest.fn(() => ({ + success: jest.fn(), + warning: jest.fn(), + error: jest.fn(), + })), +})); + +// Mock config +jest.mock('../../../../../core/config', () => ({ + config: { + feedbackLinksEnabled: false, // Default to false to avoid interference with tests + }, +})); + +// Mock navigator.clipboard +Object.assign(navigator, { + clipboard: { + writeText: jest.fn().mockResolvedValue(undefined), + }, +}); + +// Mock window.open +const mockWindowOpen = jest.fn(); +Object.defineProperty(window, 'open', { + value: mockWindowOpen, + writable: true, +}); + +// Helper function to create properly typed mock plugin extension links +const createMockExtension = ( + id: string, + title: string, + description = '', + options: { + icon?: string; + path?: string; + onClick?: () => void; + } = {} +): PluginExtensionLink => ({ + id, + type: PluginExtensionTypes.link, + title, + description, + pluginId: 'test-plugin', + icon: options.icon as IconName, + path: options.path, + onClick: options.onClick, +}); + +const setup = (pluginLinks: { links: PluginExtensionLink[]; isLoading: boolean } = { links: [], isLoading: false }) => { + const mockUsePluginLinks = usePluginLinks as jest.MockedFunction; + mockUsePluginLinks.mockReturnValue(pluginLinks); + const defaultProps = { trace, timeZone: '', @@ -37,12 +108,22 @@ const setup = () => { datasourceType: 'tempo', setHeaderHeight: jest.fn(), data: new MutableDataFrame(), + datasourceName: 'test-datasource', + datasourceUid: 'test-datasource-uid', }; - return render(); + return { + ...render(), + mockUsePluginLinks, + }; }; describe('TracePageHeader test', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockWindowOpen.mockClear(); + }); + it('should render the new trace header', () => { setup(); @@ -51,13 +132,359 @@ describe('TracePageHeader test', () => { const status = getByText(header!, '200'); const url = getByText(header!, '/v2/gamma/792edh2w897y2huehd2h89'); const duration = getByText(header!, '2.36s'); - const timestampPart1 = getByText(header!, '2023-02-05 08:50'); - const timestampPart2 = getByText(header!, ':56.289'); + const timestampElement = getByText(header!, '2023-02-05 08:50:56.289'); expect(method).toBeInTheDocument(); expect(status).toBeInTheDocument(); expect(url).toBeInTheDocument(); expect(duration).toBeInTheDocument(); - expect(timestampPart1).toBeInTheDocument(); - expect(timestampPart2).toBeInTheDocument(); + expect(timestampElement).toBeInTheDocument(); + }); + + describe('Plugin Extensions', () => { + it('should call usePluginLinks with correct parameters including datasource context', () => { + const { mockUsePluginLinks } = setup(); + + expect(mockUsePluginLinks).toHaveBeenCalledWith({ + extensionPointId: PluginExtensionPoints.TraceViewHeaderActions, + context: { + ...trace, + datasource: { + name: 'test-datasource', + uid: 'test-datasource-uid', + type: 'tempo', + }, + }, + limitPerPlugin: 2, + }); + }); + + it('should not render plugin extension buttons when no extensions are available', () => { + setup({ links: [], isLoading: false }); + + const extensionButtons = screen.queryByTestId('plugin-extension-button'); + expect(extensionButtons).not.toBeInTheDocument(); + }); + + it('should render plugin extension buttons when extensions are available', () => { + const mockExtensions: PluginExtensionLink[] = [ + createMockExtension('test-extension-1', 'Test Extension 1', 'Test extension description', { + icon: 'external-link-alt', + path: 'https://example.com', + onClick: jest.fn(), + }), + createMockExtension('test-extension-2', 'Test Extension 2', 'Another test extension', { + icon: 'cloud', + onClick: jest.fn(), + }), + ]; + + setup({ links: mockExtensions, isLoading: false }); + + expect(screen.getByText('Test Extension 1')).toBeInTheDocument(); + expect(screen.getByText('Test Extension 2')).toBeInTheDocument(); + }); + + it('should display tooltips for extension buttons', async () => { + const user = userEvent.setup(); + const mockExtensions: PluginExtensionLink[] = [ + createMockExtension('test-extension-1', 'Test Extension', 'This is a test extension description', { + icon: 'external-link-alt', + onClick: jest.fn(), + }), + ]; + + setup({ links: mockExtensions, isLoading: false }); + + const button = screen.getByText('Test Extension'); + await user.hover(button); + + await waitFor(() => { + expect(screen.getByRole('tooltip')).toBeInTheDocument(); + expect(screen.getByText('This is a test extension description')).toBeInTheDocument(); + }); + }); + + it('should use title as tooltip when description is not provided', async () => { + const user = userEvent.setup(); + const mockExtensions: PluginExtensionLink[] = [ + createMockExtension('test-extension-1', 'Test Extension Title', 'Test Extension Title', { + icon: 'external-link-alt', + onClick: jest.fn(), + }), + ]; + + setup({ links: mockExtensions, isLoading: false }); + + const button = screen.getByRole('button', { name: /Test Extension Title/i }); + await user.hover(button); + + await waitFor(() => { + expect(screen.getByRole('tooltip')).toBeInTheDocument(); + expect(screen.getByRole('tooltip')).toHaveTextContent('Test Extension Title'); + }); + }); + + it('should handle extension button clicks with onClick handler', async () => { + const user = userEvent.setup(); + const mockOnClick = jest.fn(); + const mockExtensions: PluginExtensionLink[] = [ + createMockExtension('test-extension-1', 'Test Extension', 'Test extension', { + icon: 'external-link-alt', + onClick: mockOnClick, + }), + ]; + + setup({ links: mockExtensions, isLoading: false }); + + const button = screen.getByText('Test Extension'); + await user.click(button); + + expect(mockOnClick).toHaveBeenCalledTimes(1); + expect(mockOnClick).toHaveBeenCalledWith(expect.any(Object)); + }); + + it('should handle extension button clicks with path navigation', async () => { + const user = userEvent.setup(); + const mockExtensions: PluginExtensionLink[] = [ + createMockExtension('test-extension-1', 'Test Extension', 'Test extension', { + icon: 'external-link-alt', + path: 'https://example.com/trace-details', + }), + ]; + + setup({ links: mockExtensions, isLoading: false }); + + const button = screen.getByText('Test Extension'); + await user.click(button); + + expect(mockWindowOpen).toHaveBeenCalledTimes(1); + expect(mockWindowOpen).toHaveBeenCalledWith('https://example.com/trace-details', '_blank'); + }); + + it('should handle extension with both path and onClick', async () => { + const user = userEvent.setup(); + const mockOnClick = jest.fn(); + const mockExtensions: PluginExtensionLink[] = [ + createMockExtension('test-extension-1', 'Test Extension', 'Test extension', { + icon: 'external-link-alt', + path: 'https://example.com/trace-details', + onClick: mockOnClick, + }), + ]; + + setup({ links: mockExtensions, isLoading: false }); + + const button = screen.getByText('Test Extension'); + await user.click(button); + + expect(mockWindowOpen).toHaveBeenCalledTimes(1); + expect(mockWindowOpen).toHaveBeenCalledWith('https://example.com/trace-details', '_blank'); + expect(mockOnClick).toHaveBeenCalledTimes(1); + }); + + it('should render extension buttons with correct styling', () => { + const mockExtensions: PluginExtensionLink[] = [ + createMockExtension('test-extension-1', 'Test Extension', 'Test extension', { + icon: 'external-link-alt', + onClick: jest.fn(), + }), + ]; + + setup({ links: mockExtensions, isLoading: false }); + + const button = screen.getByRole('button', { name: /Test Extension/i }); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('css-7byezq-button'); // Grafana button primary class + }); + + it('should render extension icons when provided', () => { + const mockExtensions: PluginExtensionLink[] = [ + createMockExtension('test-extension-1', 'Test Extension', 'Test extension', { + icon: 'external-link-alt', + onClick: jest.fn(), + }), + ]; + + setup({ links: mockExtensions, isLoading: false }); + + const button = screen.getByRole('button', { name: /Test Extension/i }); + const iconElement = button.querySelector('svg'); + expect(iconElement).toBeInTheDocument(); + }); + + it('should handle multiple extensions correctly', () => { + const mockExtensions: PluginExtensionLink[] = [ + createMockExtension('test-extension-1', 'Extension 1', 'First extension', { + icon: 'external-link-alt', + onClick: jest.fn(), + }), + createMockExtension('test-extension-2', 'Extension 2', 'Second extension', { + icon: 'cloud', + path: 'https://example.com', + }), + createMockExtension('test-extension-3', 'Extension 3', 'Third extension', { + icon: 'apps', + onClick: jest.fn(), + }), + ]; + + setup({ links: mockExtensions, isLoading: false }); + + expect(screen.getByText('Extension 1')).toBeInTheDocument(); + expect(screen.getByText('Extension 2')).toBeInTheDocument(); + expect(screen.getByText('Extension 3')).toBeInTheDocument(); + }); + + it('should maintain extension context with trace data and datasource information', () => { + const { mockUsePluginLinks } = setup(); + + const [callArgs] = mockUsePluginLinks.mock.calls; + expect(callArgs[0]).toEqual({ + extensionPointId: PluginExtensionPoints.TraceViewHeaderActions, + context: { + ...trace, + datasource: { + name: 'test-datasource', + uid: 'test-datasource-uid', + type: 'tempo', + }, + }, + limitPerPlugin: 2, + }); + + // Verify the context contains the expected trace properties + expect(callArgs[0].context).toHaveProperty('traceID', trace.traceID); + expect(callArgs[0].context).toHaveProperty('spans'); + expect(callArgs[0].context).toHaveProperty('duration', trace.duration); + expect(callArgs[0].context).toHaveProperty('startTime', trace.startTime); + + // Verify the context contains the datasource information + expect(callArgs[0].context).toHaveProperty('datasource'); + const contextWithDatasource = callArgs[0].context as TraceViewPluginExtensionContext; + expect(contextWithDatasource.datasource).toEqual({ + name: 'test-datasource', + uid: 'test-datasource-uid', + type: 'tempo', + }); + }); + + it('should handle loading state gracefully', () => { + setup({ links: [], isLoading: true }); + + // Should not crash when loading and should not show any extension buttons + const extensionButtons = screen.queryByTestId('plugin-extension-button'); + expect(extensionButtons).not.toBeInTheDocument(); + }); + + it('should handle extensions without icons', () => { + const mockExtensions: PluginExtensionLink[] = [ + createMockExtension('test-extension-1', 'Extension Without Icon', 'Extension without icon', { + onClick: jest.fn(), + }), + ]; + + setup({ links: mockExtensions, isLoading: false }); + + const button = screen.getByText('Extension Without Icon'); + expect(button).toBeInTheDocument(); + // Should render the button even without an icon + }); + + it('should handle extension click without event parameter', async () => { + const mockOnClick = jest.fn(); + const mockExtensions: PluginExtensionLink[] = [ + createMockExtension('test-extension-1', 'Test Extension', 'Test extension', { + onClick: mockOnClick, + }), + ]; + + setup({ links: mockExtensions, isLoading: false }); + + const button = screen.getByText('Test Extension'); + + // Simulate a click that might not pass event + fireEvent.click(button); + + expect(mockOnClick).toHaveBeenCalledTimes(1); + }); + + it('should provide datasource context to plugin extensions', () => { + const { mockUsePluginLinks } = setup(); + + const contextArg = mockUsePluginLinks.mock.calls[0][0].context as TraceViewPluginExtensionContext; + + // Verify that plugin extensions receive datasource information in context + expect(contextArg.datasource).toBeDefined(); + expect(contextArg.datasource.name).toBe('test-datasource'); + expect(contextArg.datasource.uid).toBe('test-datasource-uid'); + expect(contextArg.datasource.type).toBe('tempo'); + + // Verify that trace data is still available + expect(contextArg.traceID).toBe(trace.traceID); + expect(contextArg.spans).toBe(trace.spans); + }); + }); + + describe('Feedback Button', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should not render feedback button when feedbackLinksEnabled is false', () => { + // config.feedbackLinksEnabled is already mocked to false + setup(); + + const feedbackButton = screen.queryByText('Feedback'); + expect(feedbackButton).not.toBeInTheDocument(); + }); + + it('should render feedback button when feedbackLinksEnabled is true', () => { + // Mock config with feedbackLinksEnabled = true + const mockConfig = require('../../../../../core/config'); + mockConfig.config.feedbackLinksEnabled = true; + + setup(); + + const feedbackButton = screen.getByText('Feedback'); + expect(feedbackButton).toBeInTheDocument(); + expect(feedbackButton.closest('a')).toHaveAttribute('href', 'https://forms.gle/RZDEx8ScyZNguDoC8'); + expect(feedbackButton.closest('a')).toHaveAttribute('target', '_blank'); + }); + + it('should display tooltip for feedback button', async () => { + const user = userEvent.setup(); + + // Mock config with feedbackLinksEnabled = true + const mockConfig = require('../../../../../core/config'); + mockConfig.config.feedbackLinksEnabled = true; + + setup(); + + const feedbackButton = screen.getByText('Feedback'); + await user.hover(feedbackButton); + + await waitFor(() => { + expect(screen.getByRole('tooltip')).toBeInTheDocument(); + expect(screen.getByText('Share your thoughts about tracing in Grafana.')).toBeInTheDocument(); + }); + }); + + it('should render feedback button with correct styling and icon', () => { + // Mock config with feedbackLinksEnabled = true + const mockConfig = require('../../../../../core/config'); + mockConfig.config.feedbackLinksEnabled = true; + + setup(); + + const feedbackButton = screen.getByText('Feedback'); + const buttonElement = feedbackButton.closest('a'); + + expect(buttonElement).toBeInTheDocument(); + expect(buttonElement).toHaveClass('css-125ehy6-button'); // Secondary variant class + + // Check for icon + const iconElement = buttonElement?.querySelector('svg'); + expect(iconElement).toBeInTheDocument(); + }); }); }); diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx index 72d3a035401..f75fda8b582 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx @@ -12,24 +12,43 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { css } from '@emotion/css'; -import cx from 'classnames'; -import { memo, useEffect, useMemo } from 'react'; +import { css, cx } from '@emotion/css'; +import { memo, useEffect, useMemo, useState } from 'react'; import * as React from 'react'; -import { TraceSearchProps, CoreApp, DataFrame, dateTimeFormat, GrafanaTheme2 } from '@grafana/data'; +import { + CoreApp, + TraceSearchProps, + DataFrame, + dateTimeFormat, + dateTimeFormatTimeAgo, + GrafanaTheme2, + PluginExtensionPoints, +} from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; +import { reportInteraction, usePluginLinks } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; -import { Badge, BadgeColor, Tooltip, useStyles2 } from '@grafana/ui'; +import { + Badge, + BadgeColor, + Button, + ButtonGroup, + Dropdown, + Icon, + LinkButton, + Menu, + Tooltip, + useStyles2, + useTheme2, +} from '@grafana/ui'; +import { useAppNotification } from 'app/core/copy/appNotification'; -import ExternalLinks from '../common/ExternalLinks'; -import TraceName from '../common/TraceName'; -import { getTraceLinks } from '../model/link-patterns'; +import { config } from '../../../../../core/config'; +import { downloadTraceAsJson } from '../../../../inspector/utils/download'; import { getHeaderTags, getTraceName } from '../model/trace-viewer'; -import { Trace } from '../types/trace'; +import { Trace, TraceViewPluginExtensionContext } from '../types/trace'; import { formatDuration } from '../utils/date'; -import TracePageActions from './Actions/TracePageActions'; import { SpanFilters } from './SpanFilters/SpanFilters'; export type TracePageHeaderProps = { @@ -44,6 +63,8 @@ export type TracePageHeaderProps = { setFocusedSpanIdForSearch: React.Dispatch>; spanFilterMatches: Set | undefined; datasourceType: string; + datasourceName: string; + datasourceUid: string; setHeaderHeight: (height: number) => void; }; @@ -60,47 +81,51 @@ export const TracePageHeader = memo((props: TracePageHeaderProps) => { setFocusedSpanIdForSearch, spanFilterMatches, datasourceType, + datasourceName, + datasourceUid, setHeaderHeight, } = props; - const styles = useStyles2(getNewStyles); + + const styles = useStyles2(getStyles); + const theme = useTheme2(); + const notifyApp = useAppNotification(); + const [copyTraceIdClicked, setCopyTraceIdClicked] = useState(false); useEffect(() => { setHeaderHeight(document.querySelector('.' + styles.header)?.scrollHeight ?? 0); }, [setHeaderHeight, showSpanFilters, styles.header]); - const links = useMemo(() => { - if (!trace) { - return []; - } - return getTraceLinks(trace); - }, [trace]); - if (!trace) { return null; } - const timestamp = (trace: Trace, timeZone: TimeZone) => { - // Convert date from micro to milli seconds - const dateStr = dateTimeFormat(trace.startTime / 1000, { timeZone, defaultWithMS: true }); - const match = dateStr.match(/^(.+)(:\d\d\.\d+)$/); - return match ? ( - - {match[1]} - {match[2]} - - ) : ( - dateStr - ); + const { method, status, url } = getHeaderTags(trace.spans); + const traceName = getTraceName(trace.spans); + + // Convert date from micro to milli seconds + const formattedTimestamp = dateTimeFormat(trace.startTime / 1000, { timeZone, defaultWithMS: true }); + + // Memoize service count to avoid recomputing on every render + const serviceCount = useMemo(() => { + return new Set(trace.spans.map((span) => span.process?.serviceName)).size; + }, [trace.spans]); + + // Get plugin extensions for trace view header actions + const traceContext: TraceViewPluginExtensionContext = { + ...trace, + datasource: { + name: datasourceName, + uid: datasourceUid, + type: datasourceType, + }, }; - const title = ( -

- - {formatDuration(trace.duration)} -

- ); + const { links: extensionLinks } = usePluginLinks({ + extensionPointId: PluginExtensionPoints.TraceViewHeaderActions, + context: traceContext, + limitPerPlugin: 2, + }); - const { method, status, url } = getHeaderTags(trace.spans); let statusColor: BadgeColor = 'green'; if (status && status.length > 0) { if (status[0].value.toString().charAt(0) === '4') { @@ -110,76 +135,198 @@ export const TracePageHeader = memo((props: TracePageHeaderProps) => { } } - const urlTooltip = (url: string) => { - return ( - <> -
- - {'{{url}}'} or {'{{target}}'} or {'{{path}}'} - -
-
({url})
- - ); + const copyTraceId = () => { + navigator.clipboard.writeText(trace.traceID); + setCopyTraceIdClicked(true); + setTimeout(() => { + setCopyTraceIdClicked(false); + }, 5000); }; + const exportTrace = () => { + const traceFormat = downloadTraceAsJson(data, 'Trace-' + trace.traceID.substring(trace.traceID.length - 6)); + reportInteraction('grafana_traces_download_traces_clicked', { + app, + grafana_version: config.buildInfo.version, + trace_format: traceFormat, + location: 'trace-view', + }); + }; + + const shareDropdownMenu = ( + + { + navigator.clipboard.writeText(window.location.href); + notifyApp.success(t('explore.trace-page-header.link-copied', 'Link copied to clipboard')); + }} + /> + { + exportTrace(); + notifyApp.success(t('explore.trace-page-header.export-started', 'Export started')); + }} + /> + + ); + return (
+ {/* Main title row */}
- {links && links.length > 0 && } - {title} - +
+

{traceName}

+
+ {method && method.length > 0 && } + {status && status.length > 0 && } +
+
+ + {/* Action buttons */} +
+ {/* Plugin extension actions */} + {extensionLinks.length > 0 && ( +
+ {extensionLinks.map((link) => ( + + + + ))} +
+ )} + + {config.feedbackLinksEnabled && ( + + + Feedback + + + )} + + + + + + + +
-
- {timestamp(trace, timeZone)} - - {data.meta?.custom?.partial && ( - - - - - - )} - {method && method.length > 0 && ( - - - - - - )} - {status && status.length > 0 && ( - - - - - - )} - {url && url.length > 0 && ( - - {url[0].value} - - )} - + {/* Metadata row */} +
+
+ {t('explore.trace-page-header.trace-id', 'Trace ID')} + + + +
+ +
+ {t('explore.trace-page-header.start-time', 'Start time')} + + {formattedTimestamp} + ({dateTimeFormatTimeAgo(trace.startTime / 1000)}) + +
+ +
+ {t('explore.trace-page-header.duration', 'Duration')} + {formatDuration(trace.duration)} +
+ +
+ {t('explore.trace-page-header.services', 'Services')} + {serviceCount} +
+ + {url && url.length > 0 && ( +
+ + {url[0].key === 'http.route' && t('explore.trace-page-header.route', 'Route')} + {url[0].key === 'http.url' && t('explore.trace-page-header.url', 'URL')} + {url[0].key === 'http.target' && t('explore.trace-page-header.target', 'Target')} + {url[0].key === 'http.path' && t('explore.trace-page-header.path', 'Path')} + + + +
+ + {'{{route}}'} or {'{{url}}'} or {'{{target}}'} or {'{{path}}'} + +
+
({url[0].value})
+
+ } + interactive={true} + > + {url[0].value} + + +
+ )}
{ TracePageHeader.displayName = 'TracePageHeader'; -const getNewStyles = (theme: GrafanaTheme2) => { +const getStyles = (theme: GrafanaTheme2) => { return { - TracePageHeaderBack: css({ - label: 'TracePageHeaderBack', - alignItems: 'center', - alignSelf: 'stretch', - backgroundColor: '#fafafa', - borderBottom: '1px solid #ddd', - borderRight: '1px solid #ddd', - color: 'inherit', - display: 'flex', - fontSize: '1.4rem', - padding: '0 1rem', - marginBottom: '-1px', - '&:hover': { - backgroundColor: '#f0f0f0', - borderColor: '#ccc', - }, - }), - TracePageHeaderOverviewItemValueDetail: cx( - css({ - label: 'TracePageHeaderOverviewItemValueDetail', - color: '#aaa', - }), - 'trace-item-value-detail' - ), - TracePageHeaderOverviewItemValue: css({ - label: 'TracePageHeaderOverviewItemValue', - '&:hover > .trace-item-value-detail': { - color: 'unset', - }, - }), header: css({ label: 'TracePageHeader', backgroundColor: theme.colors.background.primary, - padding: '0.5em 0 0 0', + padding: '0.5em', position: 'sticky', top: 0, zIndex: 5, textAlign: 'left', }), + titleRow: css({ - alignItems: 'flex-start', display: 'flex', - padding: '0 8px', - flexWrap: 'wrap', + alignItems: 'flex-start', + justifyContent: 'space-between', + marginBottom: theme.spacing(1), + gap: theme.spacing(2), }), + + titleSection: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(2), + flex: 1, + minWidth: 0, // Allow text truncation + }), + title: css({ - color: 'inherit', - flex: 1, - fontSize: '1.7em', - lineHeight: '1em', - marginBottom: 0, - minWidth: '200px', - }), - subtitle: css({ - flex: 1, - lineHeight: '1em', - margin: '-0.5em 0.5em 0.75em 0.5em', - }), - tag: css({ - margin: '0 0.5em 0 0', - }), - duration: css({ - color: '#aaa', - margin: '0 0.75em', - }), - timestamp: css({ - verticalAlign: 'middle', - }), - tagMeta: css({ - margin: '0 0.75em', - verticalAlign: 'text-top', - }), - url: css({ - margin: '-2.5px 0.3em', - height: '15px', + color: theme.colors.text.primary, + fontSize: theme.typography.h3.fontSize, + fontWeight: theme.typography.h3.fontWeight, + lineHeight: theme.typography.h3.lineHeight, + margin: 0, + minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', - maxWidth: '700px', - display: 'inline-block', - }), - TracePageHeaderTraceId: css({ - label: 'TracePageHeaderTraceId', whiteSpace: 'nowrap', + }), + + badges: css({ + display: 'flex', + gap: theme.spacing(1), + alignItems: 'center', + flexShrink: 0, + }), + + actions: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + flexShrink: 0, + }), + + metadataRow: css({ + display: 'flex', + alignItems: 'center', + columnGap: theme.spacing(3), + marginBottom: theme.spacing(1), + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.text.secondary, + flexWrap: 'wrap', + }), + + metadataItem: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.5), + }), + + metadataLabel: css({ + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.text.secondary, + }), + + metadataValue: css({ + color: theme.colors.text.primary, + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + }), + + traceIdButton: css({ + background: 'none', + border: 'none', + color: theme.colors.text.primary, + cursor: 'pointer', + textDecoration: 'underline', + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.5), + padding: 0, + font: 'inherit', + + '&:hover': { + color: theme.colors.emphasize(theme.colors.text.primary, 0.15), + }, + }), + + copyIcon: css({ + opacity: 0.7, + }), + + copiedText: css({ + color: theme.colors.success.text, + fontSize: theme.typography.bodySmall.fontSize, + fontWeight: theme.typography.fontWeightMedium, + }), + + timestampDetail: css({ + color: theme.colors.text.disabled, + }), + + url: css({ + maxWidth: '700px', + overflow: 'hidden', textOverflow: 'ellipsis', - maxWidth: '30%', + whiteSpace: 'nowrap', display: 'inline-block', + color: theme.colors.text.primary, }), }; }; diff --git a/public/app/features/explore/TraceView/components/types/trace.ts b/public/app/features/explore/TraceView/components/types/trace.ts index 3c7dc4733e5..63216f3519b 100644 --- a/public/app/features/explore/TraceView/components/types/trace.ts +++ b/public/app/features/explore/TraceView/components/types/trace.ts @@ -100,3 +100,12 @@ export type CriticalPathSection = { section_start: number; section_end: number; }; + +// Type for the plugin link context that includes trace data and datasource information +export type TraceViewPluginExtensionContext = Trace & { + datasource: { + name: string; + uid: string; + type: string; + }; +}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index bdb0859ba38..e5cbc8978e4 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -7275,18 +7275,24 @@ "split-tooltip": "Split the pane", "split-widen": "Widen pane" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "Copy Trace ID", - "ariaLabel-export-trace": "Export Trace", - "give-feedback": "Give feedback", - "label-copied": "Copied!", - "label-export": "Export", - "label-trace-id": "Trace ID", - "title-share-thoughts-about-tracing-grafana": "Share your thoughts about tracing in Grafana." - }, "trace-page-header": { - "text-partial-trace": "Partial trace", - "tooltip-url": "{{url}} or {{target}} or {{path}}" + "duration": "Duration", + "export-started": "Export started", + "give-feedback": "Feedback", + "link-copied": "Link copied to clipboard", + "path": "Path", + "route": "Route", + "services": "Services", + "share": "Share", + "share-copy-link": "Copy link", + "share-export-json": "Export as JSON", + "share-tooltip": "Share trace", + "start-time": "Start time", + "target": "Target", + "title-share-thoughts-about-tracing-grafana": "Share your thoughts about tracing in Grafana.", + "tooltip-url": "{{route}} or {{url}} or {{target}} or {{path}}", + "trace-id": "Trace ID", + "url": "URL" }, "trace-page-search-bar": { "aria-label-clear-filters": "Clear filters button", From 5ef320193958bc2520c8f1e4561303a957bb29ea Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 30 Jul 2025 11:44:03 +0200 Subject: [PATCH 036/126] Alerting: Paginate data source managed rules within groups (#108803) --- .../alerting/unified/hooks/usePagination.ts | 18 ++++++++ .../rule-list/DataSourceGroupLoader.tsx | 28 ++++++++++-- .../unified/rule-list/GrafanaGroupLoader.tsx | 45 +++++-------------- .../rule-list/components/ListSection.tsx | 2 + .../alerting/unified/utils/constants.ts | 2 + 5 files changed, 59 insertions(+), 36 deletions(-) diff --git a/public/app/features/alerting/unified/hooks/usePagination.ts b/public/app/features/alerting/unified/hooks/usePagination.ts index a57955fa5bb..d1d685c2df7 100644 --- a/public/app/features/alerting/unified/hooks/usePagination.ts +++ b/public/app/features/alerting/unified/hooks/usePagination.ts @@ -27,3 +27,21 @@ export function usePagination(items: T[], initialPage = 1, itemsPerPage: numb return { page, onPageChange, numberOfPages, pageItems, pageStart, pageEnd, nextPage, previousPage }; } + +export function useContinuousPagination(items: T[], itemsPerPage: number) { + const [pageIndex, setPageIndex] = useState(1); + + const hasMore = items.length > itemsPerPage * pageIndex; + const pageItems = items.slice(0, itemsPerPage * pageIndex); + + const loadMore = useCallback(() => { + setPageIndex((index) => index + 1); + }, []); + + return { + pageItems, + pageIndex, + loadMore, + hasMore, + }; +} diff --git a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.tsx b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.tsx index a9430c488e1..9842f651ec3 100644 --- a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.tsx @@ -1,9 +1,11 @@ +import { css } from '@emotion/css'; import { skipToken } from '@reduxjs/toolkit/query'; import { useMemo } from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { isFetchError } from '@grafana/runtime'; -import { Alert } from '@grafana/ui'; +import { Alert, useStyles2 } from '@grafana/ui'; import { DataSourceRuleGroupIdentifier } from 'app/types/unified-alerting'; import { PromRuleDTO, @@ -16,13 +18,15 @@ import { import { alertRuleApi } from '../api/alertRuleApi'; import { featureDiscoveryApi } from '../api/featureDiscoveryApi'; import { prometheusApi } from '../api/prometheusApi'; -import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants'; +import { useContinuousPagination } from '../hooks/usePagination'; +import { DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP, RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants'; import { hashRule } from '../utils/rule-id'; import { getRuleName, isCloudRulerGroup } from '../utils/rules'; import { DataSourceRuleListItem } from './DataSourceRuleListItem'; import { RuleOperationListItem } from './components/AlertRuleListItem'; import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader'; +import { LoadMoreButton } from './components/LoadMoreButton'; import { RuleActionsButtons } from './components/RuleActionsButtons.V2'; import { RuleOperation } from './components/RuleListIcon'; import { matchRulesGroup } from './ruleMatching'; @@ -174,15 +178,21 @@ export function RulerBasedGroupRules({ promGroup, rulerGroup, }: RulerBasedGroupRulesProps) { + const styles = useStyles2(getStyles); const { namespace, groupName } = groupIdentifier; const { matches, promOnlyRules } = useMemo(() => { return matchRulesGroup(rulerGroup, promGroup); }, [promGroup, rulerGroup]); + const { pageItems, hasMore, loadMore } = useContinuousPagination( + rulerGroup.rules, + DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP + ); + return ( <> - {rulerGroup.rules.map((rulerRule) => { + {pageItems.map((rulerRule) => { const promRule = matches.get(rulerRule); return promRule ? ( @@ -222,6 +232,18 @@ export function RulerBasedGroupRules({ showLocation={false} /> ))} + {hasMore && ( +
  • + +
  • + )} ); } + +const getStyles = (theme: GrafanaTheme2) => ({ + loadMoreWrapper: css({ + listStyle: 'none', + paddingTop: theme.spacing(1), + }), +}); diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx index 7d9a8d184de..a92b67822d2 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx @@ -3,17 +3,16 @@ import { useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { Alert, Pagination, Stack, useStyles2 } from '@grafana/ui'; +import { Alert, Stack, useStyles2 } from '@grafana/ui'; import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; import { prometheusApi } from '../api/prometheusApi'; -import { usePagination } from '../hooks/usePagination'; -import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants'; +import { useContinuousPagination } from '../hooks/usePagination'; +import { DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP, RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants'; import { GrafanaRuleListItem } from './GrafanaRuleListItem'; import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader'; - -const DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP_LIST_VIEW_V2 = 100; +import { LoadMoreButton } from './components/LoadMoreButton'; const { useGetGrafanaGroupsQuery } = prometheusApi; @@ -53,11 +52,7 @@ export function GrafanaGroupLoader({ return promResponse?.data.groups.at(0)?.rules ?? []; }, [promResponse]); - const { pageItems, page, numberOfPages, onPageChange } = usePagination( - rules, - 1, - DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP_LIST_VIEW_V2 - ); + const { pageItems, hasMore, loadMore } = useContinuousPagination(rules, DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP); if (isPromResponseLoading) { return ( @@ -101,34 +96,18 @@ export function GrafanaGroupLoader({ /> ); })} -
    - {numberOfPages > 1 && ( - - )} -
    + {hasMore && ( +
  • + +
  • + )} ); } const getStyles = (theme: GrafanaTheme2) => ({ - pagination: css({ - display: 'flex', - margin: 0, + loadMoreWrapper: css({ + listStyle: 'none', paddingTop: theme.spacing(1), - paddingBottom: theme.spacing(0.25), - justifyContent: 'center', - float: 'none', - }), - paginationWrapper: css({ - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'center', - marginLeft: theme.spacing(2.5), }), }); diff --git a/public/app/features/alerting/unified/rule-list/components/ListSection.tsx b/public/app/features/alerting/unified/rule-list/components/ListSection.tsx index 4feb8535036..8157525e1c6 100644 --- a/public/app/features/alerting/unified/rule-list/components/ListSection.tsx +++ b/public/app/features/alerting/unified/rule-list/components/ListSection.tsx @@ -65,6 +65,8 @@ const getStyles = (theme: GrafanaTheme2) => ({ // unfortunately we have to resort to this since we can't overwrite the styles of the list items individually // unless we clone the React Elements and modify className 'li[role=treeitem]': { + listStyle: 'none', + position: 'relative', paddingLeft: theme.spacing(6.5), '&:before': { diff --git a/public/app/features/alerting/unified/utils/constants.ts b/public/app/features/alerting/unified/utils/constants.ts index eaeeb23c590..1c901e56800 100644 --- a/public/app/features/alerting/unified/utils/constants.ts +++ b/public/app/features/alerting/unified/utils/constants.ts @@ -8,6 +8,8 @@ export const SILENCES_POLL_INTERVAL_MS = 20000; export const NOTIFICATIONS_POLL_INTERVAL_MS = 20000; export const CONTACT_POINTS_STATE_INTERVAL_MS = 20000; +export const DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP = 100; + export const TIMESERIES = 'timeseries'; export const TABLE = 'table'; export const STAT = 'stat'; From d14e794aa48dda2df5c98ae39f39272e9abdcde7 Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Wed, 30 Jul 2025 11:44:43 +0200 Subject: [PATCH 037/126] Plugins: Cards view for plugin versions in case of narrow screen (#108843) add card view for narrow screen for versions table --- .../plugins/admin/components/VersionList.tsx | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/public/app/features/plugins/admin/components/VersionList.tsx b/public/app/features/plugins/admin/components/VersionList.tsx index 75eba7b828e..b3ffb829d28 100644 --- a/public/app/features/plugins/admin/components/VersionList.tsx +++ b/public/app/features/plugins/admin/components/VersionList.tsx @@ -128,29 +128,51 @@ export const VersionList = ({ pluginId, versions = [], installedVersion, disable }; const getStyles = (theme: GrafanaTheme2) => ({ - container: css({ - padding: theme.spacing(2, 4, 3), - }), - currentVersion: css({ - fontWeight: theme.typography.fontWeightBold, - }), - spinner: css({ - marginLeft: theme.spacing(1), - }), + container: css({ padding: theme.spacing(2, 4, 3) }), + currentVersion: css({ fontWeight: theme.typography.fontWeightBold }), + spinner: css({ marginLeft: theme.spacing(1) }), table: css({ tableLayout: 'fixed', width: '100%', - 'td, th': { - padding: `${theme.spacing()} 0`, - }, - th: { - fontSize: theme.typography.h5.fontSize, - }, - td: { - wordBreak: 'break-word', - }, - 'tbody tr:nth-child(odd)': { - background: theme.colors.emphasize(theme.colors.background.primary, 0.02), + 'td, th': { padding: `${theme.spacing()} 0` }, + th: { fontSize: theme.typography.h5.fontSize }, + td: { wordBreak: 'break-word' }, + 'tbody tr:nth-child(odd)': { background: theme.colors.emphasize(theme.colors.background.primary, 0.02) }, + + // Display table as cards on narrow screens + [theme.breakpoints.down('md')]: { + tableLayout: 'auto', + thead: { display: 'none' }, + tbody: { display: 'block' }, + 'tbody tr': { + display: 'block', + marginBottom: theme.spacing(2), + padding: theme.spacing(2), + background: theme.colors.background.primary, + border: `1px solid ${theme.colors.border.weak}`, + borderRadius: theme.shape.radius.default, + boxShadow: theme.shadows.z1, + }, + 'tbody td': { + display: 'block', + padding: `${theme.spacing(0.5)} 0`, + borderBottom: `1px solid ${theme.colors.border.weak}`, + textAlign: 'left', + '&:last-child': { borderBottom: 'none' }, + '&:before': { + content: 'attr(data-label)', + display: 'inline-block', + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.text.secondary, + fontSize: theme.typography.size.sm, + marginRight: theme.spacing(1), + minWidth: '120px', + }, + }, + 'tbody td:nth-child(1)': { '&:before': { content: '"Version:"' } }, + 'tbody td:nth-child(2)': { '&:before': { content: '"Action:"' } }, + 'tbody td:nth-child(3)': { '&:before': { content: '"Release date:"' } }, + 'tbody td:nth-child(4)': { '&:before': { content: '"Dependency:"' } }, }, }), }); From 07a561d24e8be695365b520b4fe88b037fa18ff6 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 30 Jul 2025 12:10:10 +0200 Subject: [PATCH 038/126] Alerting: Remove incorrect usage of "title" for alerting page wrapper (#108180) --- .../alerting/unified/components/AlertingPageWrapper.tsx | 5 ++++- .../alerting/unified/group-details/GroupEditPage.tsx | 9 ++------- public/app/features/alerting/unified/home/Home.tsx | 6 +----- public/locales/en-US/grafana.json | 6 ++---- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx b/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx index e6dd4a486f8..77a6a75d650 100644 --- a/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx +++ b/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx @@ -11,8 +11,11 @@ import { NoAlertManagerWarning } from './NoAlertManagerWarning'; /** * This is the main alerting page wrapper, used by the alertmanager page wrapper and the alert rules list view + * + * NOTE: we're omitting "title" here because it's not actually rendering the title (it's the html attribute "title"). + * Use "renderTitle" instead for custom page titles. */ -type AlertingPageWrapperProps = Omit & { +type AlertingPageWrapperProps = Omit & { isLoading?: boolean; children?: ReactNode; }; diff --git a/public/app/features/alerting/unified/group-details/GroupEditPage.tsx b/public/app/features/alerting/unified/group-details/GroupEditPage.tsx index a68f230d8cb..7d34486d59f 100644 --- a/public/app/features/alerting/unified/group-details/GroupEditPage.tsx +++ b/public/app/features/alerting/unified/group-details/GroupEditPage.tsx @@ -99,7 +99,7 @@ function GroupEditPage() { if (!!dsFeatures && !dsFeatures.rulerConfig) { return ( - + This group belongs to a data source that does not support editing. @@ -124,12 +124,7 @@ function GroupEditPage() { }; return ( - + <> {Boolean(dsFeaturesError) && ( + diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index e5cbc8978e4..5e9f8d224bc 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1548,8 +1548,7 @@ "group-not-editable": "Selected group cannot be edited", "group-not-editable-description": "This group belongs to a data source that does not support editing.", "page-title": "Edit rule group", - "rule-group-error": "Error loading rule group", - "title": "Edit evaluation group" + "rule-group-error": "Error loading rule group" }, "group-loader": { "group-load-failed": "Failed to load rules from group {{ groupName }} in {{ namespaceName }}", @@ -1583,8 +1582,7 @@ "synthetic-monitoring-card-item-2": "Run ping, DNS, HTTP/S, and TCP checks at every network layer.", "synthetic-monitoring-card-item-3": "Use 20+ global probes or private probes behind your firewall.", "synthetic-monitoring-card-item-4": "Track SLOs with built-in Prometheus-style alerts — right from the UI.", - "synthetic-monitoring-card-title": "Synthetic Monitoring", - "title-alerting": "Alerting" + "synthetic-monitoring-card-title": "Synthetic Monitoring" }, "import-to-gma": { "action-button": "Import", From e87fba7dc129842c2cfc9099cef03756d5504318 Mon Sep 17 00:00:00 2001 From: Tom Glenn <289945+tomglenn@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:50:00 +0100 Subject: [PATCH 039/126] Tom/foundation sdk docs (#106283) Co-authored-by: Kim Nylander <104772500+knylander-grafana@users.noreply.github.com> --- .../foundation-sdk/_index.md | 302 ++++++++++++++---- .../foundation-sdk/dashboard-automation.md | 4 + 2 files changed, 251 insertions(+), 55 deletions(-) diff --git a/docs/sources/observability-as-code/foundation-sdk/_index.md b/docs/sources/observability-as-code/foundation-sdk/_index.md index a6fad7139fe..5464544417e 100644 --- a/docs/sources/observability-as-code/foundation-sdk/_index.md +++ b/docs/sources/observability-as-code/foundation-sdk/_index.md @@ -20,87 +20,279 @@ The [Grafana Foundation SDK](https://github.com/grafana/grafana-foundation-sdk) - **Enhance version control:** Track changes seamlessly using standard version control systems like Git. - **Automate deployments:** Integrate dashboard provisioning into your CI/CD pipelines for consistent and repeatable setups. -The SDK supports multiple programming languages, including Go, Java, PHP, Python, and TypeScript, allowing you to choose the one that best fits your development environment. +The SDK supports multiple programming languages, including Go, TypeScript, Python, PHP, and Java, so you can choose the one that best fits your development environment. + +{{< youtube id="_OKQoABmg0Q" >}} ## Before you begin Ensure you have the following prerequisites: -- **Programming environment:** Set up for your chosen language (for example, Node.js for TypeScript, Python 3.x for Python). +- **Programming environment:** Set up for your chosen language. For example: Go, Node.js for TypeScript, or Python 3.x for Python. - **Grafana instance:** A running Grafana instance compatible with the SDK version you’re using (refer to the [compatibility matrix](https://github.com/grafana/grafana-foundation-sdk#navigating-the-sdk)). -- **Package manager:** Appropriate for your language (for example, `npm` or `yarn` for JavaScript or TypeScript, `pip` for Python). +- **Package manager:** Appropriate for your language, for example, `npm` or `yarn` for TypeScript or `pip` for Python. + +To get started, clone the [intro-to-foundation-sdk repository](https://github.com/grafana/intro-to-foundation-sdk) to access examples and a `docker-compose` stack. ## Install the Grafana Foundation SDK -### TypeScript - -For TypeScript, install the SDK package via `npm`: - -```bash -npm install @grafana/grafana-foundation-sdk -``` - -Or use `yarn`: - -```bash -yarn add @grafana/grafana-foundation-sdk -``` - -### Go - -For Go, install the SDK package via `go get`: +Select the `go` or `typescript` tab to view instructions to install the SDK. +For other languages, refer to the Grafana Foundation SDK documentation for installation instructions. +{{< code >}} ```go go get github.com/grafana/grafana-foundation-sdk/go ``` -### Python - -For Python, install the SDK using `pip`: - -```bash -pip install grafana-foundation-sdk +```typescript +npm install @grafana/grafana-foundation-sdk ``` -For other languages, refer to the Grafana Foundation SDK documentation for detailed installation instructions. +{{< /code >}} + +## Grafana Foundation SDK Overview + +Here's a quick overview of how the Grafana Foundation SDK works: + +- **Builder pattern:** The SDK uses a chainable builder pattern to let you define dashboards fluently. You start with a `DashboardBuilder`, then add panels, queries, and other components step by step. +- **Strong typing:** Everything in the SDK is strongly typed. This gives you autocompletion in your IDE, catches mistakes early, and helps ensure you're always using valid configuration values. +- **Structured options:** When a configuration get complex (like data reduction or display settings), the SDK uses typed option builders to keep things readable and predictable. + +You'll see these concepts in action in the next example. These concepts are explained in more detail afterwards. ## Create a dashboard -The following example demonstrates how you can create a simple dashboard using TypeScript: +The following example demonstrates how you can create a simple dashboard: -```typescript -import { DashboardBuilder, RowBuilder } from '@grafana/grafana-foundation-sdk/dashboard'; -import { DataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus'; -import { PanelBuilder } from '@grafana/grafana-foundation-sdk/timeseries'; -const builder = new DashboardBuilder('Sample Dashboard') - .uid('sample-dashboard') - .tags(['example', 'typescript']) - .refresh('1m') - .time({ from: 'now-30m', to: 'now' }) - .timezone('browser') - .withRow(new RowBuilder('Overview')) - .withPanel( - new PanelBuilder() - .title('Network Received') - .unit('bps') - .min(0) - .withTarget( - new DataqueryBuilder() - .expr('rate(node_network_receive_bytes_total{job="example-job", device!="lo"}[$__rate_interval]) * 8') - .legendFormat('{{ device }}') - ) - ); -console.log(JSON.stringify(builder.build(), null, 2)); +{{< code >}} + +```go +package main + +// Import the appropriate Grafana Foundation SDK packages +import ( + "encoding/json" + "log" + + "github.com/grafana/grafana-foundation-sdk/go/cog" + "github.com/grafana/grafana-foundation-sdk/go/common" + "github.com/grafana/grafana-foundation-sdk/go/dashboard" + "github.com/grafana/grafana-foundation-sdk/go/stat" + "github.com/grafana/grafana-foundation-sdk/go/testdata" + "github.com/grafana/grafana-foundation-sdk/go/timeseries" +) + +func main() { + // Define a data source reference for our testdata data source + testdataRef := dashboard.DataSourceRef{ + Type: cog.ToPtr("grafana-testdata-datasource"), + Uid: cog.ToPtr("testdata"), + } + + // Define our dashboard as strongly typed code + builder := dashboard.NewDashboardBuilder("My Dashboard"). + WithPanel( + stat.NewPanelBuilder(). + Title("Version"). + Datasource(testdataRef). + ReduceOptions(common.NewReduceDataOptionsBuilder(). + Calcs([]string{"lastNotNull"}). + Fields("/.*/")). + WithTarget( + testdata.NewDataqueryBuilder(). + ScenarioId("csv_content"). + CsvContent("version\nv1.2.3"), + ), + ). + WithPanel( + timeseries.NewPanelBuilder(). + Title("Random Time Series"). + Datasource(testdataRef). + WithTarget( + testdata.NewDataqueryBuilder(). + ScenarioId("random_walk"), + ), + ) + + // Build the dashboard - errors in configuration will be thrown here + dashboard, err := builder.Build() + if err != nil { + log.Fatalf("failed to build dashboard: %v", err) + } + + // Output the generated dashboard as JSON + dashboardJson, err := json.MarshalIndent(dashboard, "", " ") + if err != nil { + log.Fatalf("failed to marshal dashboard: %v", err) + } + + log.Printf("Dashboard JSON:\n%s", dashboardJson) +} ``` -This code defines a dashboard titled “Sample Dashboard” with a single panel displaying data received on the network. +```typescript +// Import the appropriate Grafana Foundation SDK packages +import * as common from '@grafana/grafana-foundation-sdk/common'; +import * as dashboard from '@grafana/grafana-foundation-sdk/dashboard'; +import * as stat from '@grafana/grafana-foundation-sdk/stat'; +import * as testdata from '@grafana/grafana-foundation-sdk/testdata'; +import * as timeseries from '@grafana/grafana-foundation-sdk/timeseries'; + +// Define a data source reference for our testdata data source +const testDataRef: dashboard.DataSourceRef = { + type: 'grafana-testdata-datasource', + uid: 'testdata', +}; + +// Define our dashboard as strongly typed code +const builder = new dashboard.DashboardBuilder('My Dashboard') + .withPanel( + new stat.PanelBuilder() + .title('Version') + .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']).fields('/.*/')) + .datasource(testdataRef) + .withTarget(new testdata.DataqueryBuilder().scenarioId('csv_content').csvContent('version\nv1.2.3')) + ) + .withPanel( + new timeseries.PanelBuilder() + .title('Random Time Series') + .datasource(testdataRef) + .withTarget(new testdata.DataqueryBuilder().scenarioId('random_walk')) + ); + +// Build the dashboard - errors in configuration will be thrown here +const dashboard = builder.build(); + +// Output the generated dashboard as JSON +console.log(JSON.stringify(dashboard, null, 2)); +``` + +{{< /code >}} + +This code defines a dashboard titled “My Dashboard” with a two panels: + +- a simple stat panel displaying a version number, and +- a time series panel displaying randomized data from the `testdata` data source `random_walk` scenario. ## Export and use the JSON -The `build()` method generates a JSON representation of your dashboard, which you can: +After you've defined your dashboard as code, build the final dashboard representation using the dashboard builder (typically using the `build()` function depending on language choice) and output the result as a JSON. + +With the JSON payload, you can: - **Manually import:** Paste into Grafana’s dashboard import feature. -- **Automate:** Use Grafana’s API to programmatically upload the dashboard JSON. +- **Automate:** Use [Grafana’s API](../../developers/http_api/) or the [Grafana CLI](../grafana-cli/) to programmatically upload the dashboard JSON. + +## Concepts + +Now that you've seen how to define a basic dashboard using code, let's take a moment to explain how it all works behind the scenes. The Grafana Foundation SDK is built around a few core concepts that make your dashboards structured, reusable, and strongly typed. + +### Builders + +The SDK follows a builder pattern, which lets you compose dashboards step-by-step using chained method calls. +Almost every piece of the dashboard, including dashboards, panels, rows, queries, and variables, has its own `Builder` class. + +Here are a few you've already seen: + +- `DashboardBuilder` - Starts the dashboard definition and sets global configuration settings like title, UID, refresh interval, time range, etc. +- `PanelBuilder` - Creates individual visualizations like time series panels, stat panels, or log panels. +- `DataqueryBuilder` - Defines how a panel fetches data, for example, from Prometheus or the `testdata` plugin. + +Builders are chainable, so you can fluently compose dashboards in a readable, structured way: + +{{< code >}} + +```go +stat.NewPanelBuilder(). + Title("Version"). + Datasource(testdataRef). + ReduceOptions(common.NewReduceDataOptionsBuilder(). + Calcs([]string{"lastNotNull"}). + Fields("/.*/")). + WithTarget( + testdata.NewDataqueryBuilder(). + ScenarioId("csv_content"). + CsvContent("version\nv1.2.3"), + ) +``` + +```typescript +new stat.PanelBuilder() + .title('Version') + .reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']).fields('/.*/')) + .datasource(testdataRef) + .withTarget(new testdata.DataqueryBuilder().scenarioId('csv_content').csvContent('version\nv1.2.3')); +``` + +{{< /code >}} + +### Types + +The Grafana Foundation SDK uses strong types under the hood to help catch mistakes before you deploy a broken dashboard. + +For example: + +- When setting a unit, you'll get autocomplete suggestions for valid Grafana units like `"percent"` or `"bps"`. +- When defining a time range, you'll be guided to provide the correct structure, like `from` and `to` values. +- When referencing data sources, you'll use a structured `DataSourceRef` object with defined `type` and `uid` fields. + +This helps you: + +- Avoid typos or unsupported configuration values +- Get full autocomplete and inline documentation in your IDE +- Write dashboards that are less error-prone and easier to maintain + +Strong typing also makes it easier to build reusable patterns and components with confidence, especially in large codebases or teams. + +### Options + +Most builder methods accept simple values like strings or numbers, but others expect more structured option objects. These are used for things like: + +- `ReduceDataOptions` - How to reduce time series data into single values (e.g. last, avg). +- `VizLegendOptions` - Configure how the legend of a panel is displayed. +- `CanvasElementOptions` - Define how the the various components of a Canvas panel should be displayed. + +Example using options: + +{{< code >}} + +```go +stat.NewPanelBuilder(). + ReduceOptions(common.NewReduceDataOptionsBuilder(). + Calcs([]string{"lastNotNull"}). + Fields("/.*/")) + ) +``` + +```typescript +new stat.PanelBuilder().reduceOptions(new common.ReduceDataOptionsBuilder().calcs(['lastNotNull']).fields('/.*/')); +``` + +{{< /code >}} + +By using option builders, you don't need to manually construct deeply nested configuration objects. Instead, the SDK gives you a typed and guided API that mirrors a dashboards internal structure, making it easier to configure complex options without guesswork or referring back to the JSON schema. + +## Explore a real-world example + +If you want to explore further and see a more real-world example of using the Grafana Foundation SDK, watch the following walkthrough: + +{{< youtube id="ZjWdGVsrCiQ" >}} + +In this video, we generate a dashboard from code and deploy it using the Grafana API, covering patterns and practices you'd use in production environments. It also includes a working example of a web service that emits metrics and logs, and shows how to deploy a dashboard alongside it using Docker Compose. + +You can find the full source code for this example in the [intro-to-foundation-sdk repository](https://github.com/grafana/intro-to-foundation-sdk/tree/main/generate-and-deploy-example). + +## Summary + +The Grafana Foundation SDK is designed to make dashboard creation: + +- **Composable** through the use of chainable builders +- **Safe** with strong typing and clear APIs +- **Configurable** using structured options for fine control + +As you build more advanced dashboards, you’ll work with additional builders and types to support richer functionality. +The SDK supports not just panels and queries, but also variables, thresholds, field overrides, transformations, and more. +Refer to [the full API reference](https://grafana.github.io/grafana-foundation-sdk/) to explore what's possible. ## Next steps @@ -108,4 +300,4 @@ Now that you understand the basics of using the Grafana Foundation SDK, here are - **Explore more features:** Check out the [full API reference](https://grafana.github.io/grafana-foundation-sdk/) to learn about advanced dashboard configurations. - **Version control your dashboards:** Store your dashboard code in a Git repository to track changes over time. -- **Automate dashboard provisioning with CI/CD:** Integrate the SDK into your CI/CD pipeline to deploy dashboards automatically. +- **Automate dashboard provisioning with CI/CD:** [Integrate the SDK into your CI/CD pipeline](./dashboard-automation) to deploy dashboards automatically. diff --git a/docs/sources/observability-as-code/foundation-sdk/dashboard-automation.md b/docs/sources/observability-as-code/foundation-sdk/dashboard-automation.md index 6ae3821e58a..d4b98f46c80 100644 --- a/docs/sources/observability-as-code/foundation-sdk/dashboard-automation.md +++ b/docs/sources/observability-as-code/foundation-sdk/dashboard-automation.md @@ -29,6 +29,10 @@ This guide walks through: By the end, every change to your dashboard code will be automatically created or updated in your Grafana instance without manual intervention. +{{< youtube id="cFnO8kVOaAI" >}} + +You can find the full example source code in the [intro-to-foundation-sdk repository](https://github.com/grafana/intro-to-foundation-sdk/tree/main/github-actions-example). + ## 1. Generating the dashboard JSON Before deploying a dashboard, we need to define it in code using the Grafana Foundation SDK. We ran through an example of this in the Getting Started guide, however, in order to comply with the Kubernetes resource compatible API that Grafana exposes, we’ll make some changes to the code to output the dashboard JSON in the appropriate format. From afdb3d7c95d3f9148f8e28ebb0421c09df5a01ae Mon Sep 17 00:00:00 2001 From: Alexa Vargas <239999+axelavargas@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:27:22 +0200 Subject: [PATCH 040/126] Chore: Change Public Dashboards and Image Renderer AutoTriage to operator experience squad (#108910) Chore: Change Public Dashboards AutoTriage to operator experience squad --- .github/CODEOWNERS | 2 +- .github/commands.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e4c805b9850..b3ab77bcd19 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -598,7 +598,7 @@ /packages/grafana-schema/src/**/nodegraph @grafana/observability-traces-and-profiling @grafana/app-o11y-visualizations /packages/grafana-schema/src/**/parca @grafana/oss-big-tent /packages/grafana-schema/src/**/piechart @grafana/dataviz-squad -/packages/grafana-schema/src/**/publicdashboard @grafana/sharing-squad +/packages/grafana-schema/src/**/publicdashboard @grafana/grafana-operator-experience-squad /packages/grafana-schema/src/**/stat @grafana/dataviz-squad /packages/grafana-schema/src/**/statetimeline @grafana/dataviz-squad /packages/grafana-schema/src/**/statushistory @grafana/dataviz-squad diff --git a/.github/commands.json b/.github/commands.json index 68b88814090..06f91a56c59 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -584,7 +584,7 @@ "name": "area/public-dashboards", "action": "addToProject", "addToProject": { - "url": "https://github.com/orgs/grafana/projects/482" + "url": "https://github.com/orgs/grafana/projects/467" } }, { @@ -752,7 +752,7 @@ "name": "area/image-rendering", "action": "addToProject", "addToProject": { - "url": "https://github.com/orgs/grafana/projects/482" + "url": "https://github.com/orgs/grafana/projects/467" } }, { From b707cd28f1c08f9998c3558e8962c5eaff918ac8 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Wed, 30 Jul 2025 14:39:44 +0200 Subject: [PATCH 041/126] Frontend: Move toEmit jest matchers to shared workspace (#108610) * feat(test-utils): move the toEmitValue/s jest matchers to test-utils and add test script * chore(jest): update configs to use matchers from test-utils package * ci(frontend-tests): hook up packages tests * fix(test-utils): re-export matchers from index.ts so packages that include setupTests don't error * ci(pr-frontend-unit-tests): add frontend-packages-unit-tests to list of required unit tests * Update packages/grafana-test-utils/README.md Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> --------- Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> --- .github/workflows/pr-frontend-unit-tests.yml | 19 ++++++++++++ package.json | 1 + .../grafana-plugin-configs/jest/jest-setup.js | 4 +++ packages/grafana-test-utils/README.md | 21 ++++++++++++++ packages/grafana-test-utils/jest-setup.js | 3 ++ packages/grafana-test-utils/jest.config.js | 29 +++++++++++++++++++ packages/grafana-test-utils/package.json | 13 ++++++++- packages/grafana-test-utils/src/index.ts | 4 ++- .../grafana-test-utils/src}/matchers/index.ts | 2 +- .../src}/matchers/toEmitValues.test.ts | 0 .../src}/matchers/toEmitValues.ts | 0 .../src}/matchers/toEmitValuesWith.test.ts | 0 .../src}/matchers/toEmitValuesWith.ts | 0 .../grafana-test-utils/src/matchers/types.ts | 28 ++++++++++++++++++ .../grafana-test-utils/src}/matchers/utils.ts | 0 .../datasource/azuremonitor/jest-setup.js | 2 +- .../datasource/cloud-monitoring/jest-setup.js | 2 +- .../jest-setup.js | 2 +- .../jest-setup.js | 2 +- .../grafana-testdata-datasource/jest-setup.js | 2 +- .../plugins/datasource/jaeger/jest-setup.js | 2 +- .../plugins/datasource/mysql/jest-setup.js | 2 +- .../plugins/datasource/parca/jest-setup.js | 2 +- .../plugins/datasource/tempo/jest-setup.js | 2 +- .../plugins/datasource/zipkin/jest-setup.js | 2 +- public/test/matchers/types.ts | 11 ------- public/test/setupTests.ts | 4 +-- yarn.lock | 4 +++ 28 files changed, 137 insertions(+), 26 deletions(-) create mode 100644 packages/grafana-test-utils/jest-setup.js create mode 100644 packages/grafana-test-utils/jest.config.js rename {public/test => packages/grafana-test-utils/src}/matchers/index.ts (86%) rename {public/test => packages/grafana-test-utils/src}/matchers/toEmitValues.test.ts (100%) rename {public/test => packages/grafana-test-utils/src}/matchers/toEmitValues.ts (100%) rename {public/test => packages/grafana-test-utils/src}/matchers/toEmitValuesWith.test.ts (100%) rename {public/test => packages/grafana-test-utils/src}/matchers/toEmitValuesWith.ts (100%) create mode 100644 packages/grafana-test-utils/src/matchers/types.ts rename {public/test => packages/grafana-test-utils/src}/matchers/utils.ts (100%) delete mode 100644 public/test/matchers/types.ts diff --git a/.github/workflows/pr-frontend-unit-tests.yml b/.github/workflows/pr-frontend-unit-tests.yml index 25d2450e002..283a186c50d 100644 --- a/.github/workflows/pr-frontend-unit-tests.yml +++ b/.github/workflows/pr-frontend-unit-tests.yml @@ -107,6 +107,24 @@ jobs: - run: yarn install --immutable --check-cache - run: yarn run plugin:test:ci + frontend-packages-unit-tests: + needs: detect-changes + if: needs.detect-changes.outputs.changed == 'true' + runs-on: ubuntu-latest-8-cores + name: "Packages unit tests" + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + cache-dependency-path: 'yarn.lock' + - run: yarn install --immutable --check-cache + - run: yarn run packages:test:ci + + # This is the job that is actually required by rulesets. # We need to require EITHER the OSS or the Enterprise job to pass. # However, if one is skipped, GitHub won't flat-map the shards, @@ -116,6 +134,7 @@ jobs: - frontend-unit-tests - frontend-unit-tests-enterprise - frontend-decoupled-plugin-tests + - frontend-packages-unit-tests # always() is the best function here. # success() || failure() will skip this function if any need is also skipped. # That means conditional test suites will fail the entire requirement check. diff --git a/package.json b/package.json index 79bbdd20cbe..278ffb6efcc 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "packages:prepare": "lerna version --no-push --no-git-tag-version --force-publish --exact", "packages:pack": "mkdir -p ./npm-artifacts && lerna exec --no-private -- yarn pack --out \"../../npm-artifacts/%s-%v.tgz\"", "packages:typecheck": "nx run-many -t typecheck --projects='tag:scope:package'", + "packages:test:ci": "nx run-many -t test:ci --projects='tag:scope:package'", "prettier:check": "prettier --check --ignore-path .prettierignore --list-different=false --log-level=warn \"**/*.{ts,tsx,scss,md,mdx,json,js,cjs}\"", "prettier:checkDocs": "prettier --check --list-different=false --log-level=warn \"docs/**/*.md\" \"*.md\" \"packages/**/*.{ts,tsx,scss,md,mdx,json,js,cjs}\"", "prettier:write": "prettier --ignore-path .prettierignore --list-different \"**/*.{js,ts,tsx,scss,md,mdx,json,cjs}\" --write", diff --git a/packages/grafana-plugin-configs/jest/jest-setup.js b/packages/grafana-plugin-configs/jest/jest-setup.js index 00a84b42b68..05a2b03634c 100644 --- a/packages/grafana-plugin-configs/jest/jest-setup.js +++ b/packages/grafana-plugin-configs/jest/jest-setup.js @@ -1,6 +1,10 @@ import '@testing-library/jest-dom'; import { TextEncoder, TextDecoder } from 'util'; +import { matchers } from '@grafana/test-utils/matchers'; + +expect.extend(matchers); + Object.assign(global, { TextDecoder, TextEncoder }); // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom diff --git a/packages/grafana-test-utils/README.md b/packages/grafana-test-utils/README.md index 2e864d4ad7b..370987a8931 100644 --- a/packages/grafana-test-utils/README.md +++ b/packages/grafana-test-utils/README.md @@ -1,3 +1,24 @@ # Grafana test utils This package is a collection of test utils and a mock API (using MSW) for use with core Grafana UI development. + +## Matchers + +To add the matchers to your Jest config, import them then extend `expect`. This should be done in the `setupFilesAfterEnv` file declared in `jest.config.{js,ts}`. + +```ts +// setupTests.ts +import { matchers } from '@grafana/test-utils'; + +expect.extend(matchers); +``` + +Included in this package are the following matchers: + +### `toEmitValues` + +Tests that an Observable emits the expected values in the correct order. This matcher collects all emitted values (including errors) and compares them against the expected array using deep equality. + +### `toEmitValuesWith` + +Tests that an Observable emits values that satisfy custom expectations. This matcher collects all emitted values and passes them to a callback function where you can perform custom assertions. diff --git a/packages/grafana-test-utils/jest-setup.js b/packages/grafana-test-utils/jest-setup.js new file mode 100644 index 00000000000..57d87c0c2ad --- /dev/null +++ b/packages/grafana-test-utils/jest-setup.js @@ -0,0 +1,3 @@ +import { matchers } from '@grafana/test-utils/matchers'; + +expect.extend(matchers); diff --git a/packages/grafana-test-utils/jest.config.js b/packages/grafana-test-utils/jest.config.js new file mode 100644 index 00000000000..468cf6bae51 --- /dev/null +++ b/packages/grafana-test-utils/jest.config.js @@ -0,0 +1,29 @@ +process.env.TZ = 'Pacific/Easter'; // UTC-06:00 or UTC-05:00 depending on daylight savings + +export default { + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + setupFilesAfterEnv: ['/jest-setup.js'], + testMatch: ['/**/__tests__/**/*.{js,jsx,ts,tsx}', '/**/*.{spec,test,jest}.{js,jsx,ts,tsx}'], + testEnvironment: 'node', + transform: { + '^.+\\.(t|j)sx?$': [ + '@swc/jest', + { + sourceMaps: 'inline', + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + decorators: false, + dynamicImport: true, + }, + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], + }, +}; diff --git a/packages/grafana-test-utils/package.json b/packages/grafana-test-utils/package.json index 7f58c62ac0d..9a9f5998621 100644 --- a/packages/grafana-test-utils/package.json +++ b/packages/grafana-test-utils/package.json @@ -40,17 +40,28 @@ "./unstable": { "import": "./src/unstable.ts", "require": "./src/unstable.ts" + }, + "./matchers": { + "types": "./src/matchers/index.ts", + "import": "./src/matchers/index.ts", + "require": "./src/matchers/index.ts" } }, "scripts": { - "typecheck": "tsc --emitDeclarationOnly false --noEmit" + "typecheck": "tsc --emitDeclarationOnly false --noEmit", + "test": "jest --watch --onlyChanged", + "test:ci": "jest --maxWorkers 4" }, "dependencies": { "msw": "2.10.4" }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", + "@swc/core": "1.10.12", + "@swc/jest": "^0.2.26", + "@types/jest": "29.5.14", "@types/node": "22.16.5", + "jest": "29.7.0", "typescript": "5.8.3" } } diff --git a/packages/grafana-test-utils/src/index.ts b/packages/grafana-test-utils/src/index.ts index f5d42f308b0..55005c34d54 100644 --- a/packages/grafana-test-utils/src/index.ts +++ b/packages/grafana-test-utils/src/index.ts @@ -5,4 +5,6 @@ * @packageDocumentation */ -export {}; +// This is also exported as `@grafana/test-utils/matchers` but we cannot use that in places +// where the tsconfig is not set to moduleResolution: bundler so we export it here also. +export { matchers } from './matchers'; diff --git a/public/test/matchers/index.ts b/packages/grafana-test-utils/src/matchers/index.ts similarity index 86% rename from public/test/matchers/index.ts rename to packages/grafana-test-utils/src/matchers/index.ts index b62de8d6974..11a1891accd 100644 --- a/public/test/matchers/index.ts +++ b/packages/grafana-test-utils/src/matchers/index.ts @@ -1,4 +1,4 @@ -import { Observable } from 'rxjs'; +import type { Observable } from 'rxjs'; import { toEmitValues } from './toEmitValues'; import { toEmitValuesWith } from './toEmitValuesWith'; diff --git a/public/test/matchers/toEmitValues.test.ts b/packages/grafana-test-utils/src/matchers/toEmitValues.test.ts similarity index 100% rename from public/test/matchers/toEmitValues.test.ts rename to packages/grafana-test-utils/src/matchers/toEmitValues.test.ts diff --git a/public/test/matchers/toEmitValues.ts b/packages/grafana-test-utils/src/matchers/toEmitValues.ts similarity index 100% rename from public/test/matchers/toEmitValues.ts rename to packages/grafana-test-utils/src/matchers/toEmitValues.ts diff --git a/public/test/matchers/toEmitValuesWith.test.ts b/packages/grafana-test-utils/src/matchers/toEmitValuesWith.test.ts similarity index 100% rename from public/test/matchers/toEmitValuesWith.test.ts rename to packages/grafana-test-utils/src/matchers/toEmitValuesWith.test.ts diff --git a/public/test/matchers/toEmitValuesWith.ts b/packages/grafana-test-utils/src/matchers/toEmitValuesWith.ts similarity index 100% rename from public/test/matchers/toEmitValuesWith.ts rename to packages/grafana-test-utils/src/matchers/toEmitValuesWith.ts diff --git a/packages/grafana-test-utils/src/matchers/types.ts b/packages/grafana-test-utils/src/matchers/types.ts new file mode 100644 index 00000000000..c7c420e9355 --- /dev/null +++ b/packages/grafana-test-utils/src/matchers/types.ts @@ -0,0 +1,28 @@ +import { Observable } from 'rxjs'; + +export const OBSERVABLE_TEST_TIMEOUT_IN_MS = 1000; + +export interface ObservableMatchers extends jest.ExpectExtendMap { + toEmitValues(received: Observable, expected: T[]): Promise; + toEmitValuesWith( + received: Observable, + expectations: (received: T[]) => void + ): Promise; +} + +type ObservableType = T extends Observable ? V : never; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toEmitValues>(expected: E[]): Promise; + /** + * Collect all the values emitted by the observables (also errors) and pass them to the expectations functions after + * the observable ended (or emitted error). If Observable does not complete within OBSERVABLE_TEST_TIMEOUT_IN_MS the + * test fails. + */ + toEmitValuesWith>(expectations: (received: E[]) => void): Promise; + } + } +} diff --git a/public/test/matchers/utils.ts b/packages/grafana-test-utils/src/matchers/utils.ts similarity index 100% rename from public/test/matchers/utils.ts rename to packages/grafana-test-utils/src/matchers/utils.ts diff --git a/public/app/plugins/datasource/azuremonitor/jest-setup.js b/public/app/plugins/datasource/azuremonitor/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/azuremonitor/jest-setup.js +++ b/public/app/plugins/datasource/azuremonitor/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/cloud-monitoring/jest-setup.js b/public/app/plugins/datasource/cloud-monitoring/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/cloud-monitoring/jest-setup.js +++ b/public/app/plugins/datasource/cloud-monitoring/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/jest-setup.js b/public/app/plugins/datasource/grafana-postgresql-datasource/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/jest-setup.js +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/jest-setup.js b/public/app/plugins/datasource/grafana-pyroscope-datasource/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/jest-setup.js +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/jest-setup.js b/public/app/plugins/datasource/grafana-testdata-datasource/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/jest-setup.js +++ b/public/app/plugins/datasource/grafana-testdata-datasource/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/jaeger/jest-setup.js b/public/app/plugins/datasource/jaeger/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/jaeger/jest-setup.js +++ b/public/app/plugins/datasource/jaeger/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/mysql/jest-setup.js b/public/app/plugins/datasource/mysql/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/mysql/jest-setup.js +++ b/public/app/plugins/datasource/mysql/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/parca/jest-setup.js b/public/app/plugins/datasource/parca/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/parca/jest-setup.js +++ b/public/app/plugins/datasource/parca/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/tempo/jest-setup.js b/public/app/plugins/datasource/tempo/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/tempo/jest-setup.js +++ b/public/app/plugins/datasource/tempo/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/zipkin/jest-setup.js b/public/app/plugins/datasource/zipkin/jest-setup.js index 60f6932753d..c85bf9d3a57 100644 --- a/public/app/plugins/datasource/zipkin/jest-setup.js +++ b/public/app/plugins/datasource/zipkin/jest-setup.js @@ -1 +1 @@ -import '@grafana/plugin-configs/jest/jest-setup.js'; +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/test/matchers/types.ts b/public/test/matchers/types.ts deleted file mode 100644 index 2c8cd92ff3b..00000000000 --- a/public/test/matchers/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Observable } from 'rxjs'; - -export const OBSERVABLE_TEST_TIMEOUT_IN_MS = 1000; - -export interface ObservableMatchers extends jest.ExpectExtendMap { - toEmitValues(received: Observable, expected: T[]): Promise; - toEmitValuesWith( - received: Observable, - expectations: (received: T[]) => void - ): Promise; -} diff --git a/public/test/setupTests.ts b/public/test/setupTests.ts index 3f053609138..d6c97a0d1c8 100644 --- a/public/test/setupTests.ts +++ b/public/test/setupTests.ts @@ -7,9 +7,9 @@ import i18next from 'i18next'; import failOnConsole from 'jest-fail-on-console'; import { initReactI18next } from 'react-i18next'; -import getEnvConfig from '../../scripts/webpack/env-util'; +import { matchers } from '@grafana/test-utils'; -import { matchers } from './matchers'; +import getEnvConfig from '../../scripts/webpack/env-util'; const config = getEnvConfig() as Record; diff --git a/yarn.lock b/yarn.lock index dab33c9e9bc..7d72c331da7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3714,7 +3714,11 @@ __metadata: resolution: "@grafana/test-utils@workspace:packages/grafana-test-utils" dependencies: "@grafana/tsconfig": "npm:^2.0.0" + "@swc/core": "npm:1.10.12" + "@swc/jest": "npm:^0.2.26" + "@types/jest": "npm:29.5.14" "@types/node": "npm:22.16.5" + jest: "npm:29.7.0" msw: "npm:2.10.4" typescript: "npm:5.8.3" languageName: unknown From 3ff4c6a2d2cc6a3c936b88aad1373ced7c309ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Bedi?= Date: Wed, 30 Jul 2025 14:47:25 +0200 Subject: [PATCH 042/126] Postgres: Switch the datasource plugin from lib/pq to pgx (#108443) * Postgres: Switch the datasource plugin from lib/pq to pgx * Fix lint --- .betterer.results | 10 +- .../src/types/featureToggles.gen.ts | 4 + .../configuration/ConnectionLimits.tsx | 78 +- .../configuration/MaxLifetimeField.tsx | 47 + .../configuration/MaxOpenConnectionsField.tsx | 50 + packages/grafana-sql/src/index.ts | 2 + .../backendplugin/coreplugin/registry.go | 2 +- pkg/server/wire_gen.go | 4 +- 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 | 13 + .../plugintest/plugins_test.go | 2 +- .../pgxtlsmanager.go | 151 ++ .../pgxtlsmanager_test.go | 245 +++ .../grafana-postgresql-datasource/postgres.go | 240 ++- .../postgres_pgx_snapshot_test.go | 195 +++ .../postgres_pgx_test.go | 1382 +++++++++++++++++ .../postgres_test.go | 2 +- .../grafana-postgresql-datasource/proxy.go | 8 + .../sqleng/handler_checkhealth.go | 15 +- .../sqleng/sql_engine.go | 3 + .../sqleng/sql_engine_pgx.go | 554 +++++++ .../table/types_datetime_pgx.golden.jsonc | 214 +++ .../testdata/table/types_datetime_pgx.sql | 44 + .../configuration/ConfigurationEditor.tsx | 50 +- 26 files changed, 3179 insertions(+), 147 deletions(-) create mode 100644 packages/grafana-sql/src/components/configuration/MaxLifetimeField.tsx create mode 100644 packages/grafana-sql/src/components/configuration/MaxOpenConnectionsField.tsx create mode 100644 pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager_test.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_snapshot_test.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_pgx.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.golden.jsonc create mode 100644 pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.sql diff --git a/.betterer.results b/.betterer.results index 95894921943..37fb0e90b15 100644 --- a/.betterer.results +++ b/.betterer.results @@ -576,9 +576,13 @@ exports[`better eslint`] = { ], "packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "3"] + [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"] + ], + "packages/grafana-sql/src/components/configuration/MaxLifetimeField.tsx:5381": [ + [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] + ], + "packages/grafana-sql/src/components/configuration/MaxOpenConnectionsField.tsx:5381": [ + [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], "packages/grafana-sql/src/components/configuration/TLSSecretsConfig.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 835ff2230f2..90565ef3eb0 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -960,6 +960,10 @@ export interface FeatureToggles { */ metricsFromProfiles?: boolean; /** + * Enables using PGX instead of libpq for PostgreSQL datasource + */ + postgresDSUsePGX?: boolean; + /** * Enables creating alerts from Tempo data source */ tempoAlerting?: boolean; diff --git a/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx b/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx index 039425e608c..487ef88e61d 100644 --- a/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx +++ b/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx @@ -6,6 +6,8 @@ import { Field, Icon, InlineLabel, Label, Stack, Switch, Tooltip } from '@grafan import { SQLConnectionLimits, SQLOptions } from '../../types'; +import { MaxLifetimeField } from './MaxLifetimeField'; +import { MaxOpenConnectionsField } from './MaxOpenConnectionsField'; import { NumberInput } from './NumberInput'; interface Props { @@ -87,40 +89,11 @@ export const ConnectionLimits = (props: Props) - - - - Max open - - - - The maximum number of open connections to the database. If Max idle connections is greater - than 0 and the Max open connections is less than Max idle connections, then - Max idle connections will be reduced to match the Max open connections limit. If set - to 0, there is no limit on the number of open connections. - - - } - > - - - - - } - > - { - onMaxConnectionsChanged(value); - }} - width={labelWidth} - /> - + (props: Props) )} - - - - Max lifetime - - - - The maximum amount of time in seconds a connection may be reused. If set to 0, connections are - reused forever. - - - } - > - - - - - } - > - { - onJSONDataNumberChanged('connMaxLifetime')(value); - }} - width={labelWidth} - /> - + ); }; diff --git a/packages/grafana-sql/src/components/configuration/MaxLifetimeField.tsx b/packages/grafana-sql/src/components/configuration/MaxLifetimeField.tsx new file mode 100644 index 00000000000..a3317785716 --- /dev/null +++ b/packages/grafana-sql/src/components/configuration/MaxLifetimeField.tsx @@ -0,0 +1,47 @@ +import { Trans } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; +import { Field, Icon, Label, Stack, Tooltip } from '@grafana/ui'; + +import { SQLOptions } from '../../types'; + +import { NumberInput } from './NumberInput'; + +interface Props { + labelWidth: number; + onMaxLifetimeChanged: (number?: number) => void; + jsonData: SQLOptions; +} +export function MaxLifetimeField({ labelWidth, onMaxLifetimeChanged, jsonData }: Props) { + return ( + + + + Max lifetime + + + + The maximum amount of time in seconds a connection may be reused. If set to 0, connections are + reused forever. + + + } + > + + + + + } + > + + + ); +} diff --git a/packages/grafana-sql/src/components/configuration/MaxOpenConnectionsField.tsx b/packages/grafana-sql/src/components/configuration/MaxOpenConnectionsField.tsx new file mode 100644 index 00000000000..377f05f0fe0 --- /dev/null +++ b/packages/grafana-sql/src/components/configuration/MaxOpenConnectionsField.tsx @@ -0,0 +1,50 @@ +import { Trans } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; +import { Field, Icon, Label, Stack, Tooltip } from '@grafana/ui'; + +import { SQLOptions } from '../../types'; + +import { NumberInput } from './NumberInput'; + +interface Props { + labelWidth: number; + onMaxConnectionsChanged: (number?: number) => void; + jsonData: SQLOptions; +} + +export function MaxOpenConnectionsField({ labelWidth, onMaxConnectionsChanged, jsonData }: Props) { + return ( + + + + Max open + + + + The maximum number of open connections to the database. If Max idle connections is greater + than 0 and the Max open connections is less than Max idle connections, then + Max idle connections will be reduced to match the Max open connections limit. If set + to 0, there is no limit on the number of open connections. + + + } + > + + + + + } + > + + + ); +} diff --git a/packages/grafana-sql/src/index.ts b/packages/grafana-sql/src/index.ts index 0de11d1f5d6..0c24d89f46a 100644 --- a/packages/grafana-sql/src/index.ts +++ b/packages/grafana-sql/src/index.ts @@ -14,6 +14,8 @@ export { COMMON_FNS, MACRO_FUNCTIONS } from './constants'; export { SqlDatasource } from './datasource/SqlDatasource'; export { formatSQL } from './utils/formatSQL'; export { ConnectionLimits } from './components/configuration/ConnectionLimits'; +export { MaxLifetimeField } from './components/configuration/MaxLifetimeField'; +export { MaxOpenConnectionsField } from './components/configuration/MaxOpenConnectionsField'; export { Divider } from './components/configuration/Divider'; export { TLSSecretsConfig } from './components/configuration/TLSSecretsConfig'; export { useMigrateDatabaseFields } from './components/configuration/useMigrateDatabaseFields'; diff --git a/pkg/plugins/backendplugin/coreplugin/registry.go b/pkg/plugins/backendplugin/coreplugin/registry.go index 555c1b23ec3..de6772801af 100644 --- a/pkg/plugins/backendplugin/coreplugin/registry.go +++ b/pkg/plugins/backendplugin/coreplugin/registry.go @@ -237,7 +237,7 @@ func NewPlugin(pluginID string, cfg *setting.Cfg, httpClientProvider *httpclient case Tempo: svc = tempo.ProvideService(httpClientProvider) case PostgreSQL: - svc = postgres.ProvideService(cfg) + svc = postgres.ProvideService(cfg, features) case MySQL: svc = mysql.ProvideService() case MSSQL: diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 5d13b55fafe..b398b85d5a9 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -380,7 +380,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser prometheusService := prometheus.ProvideService(httpclientProvider) tempoService := tempo.ProvideService(httpclientProvider) testdatasourceService := testdatasource.ProvideService() - postgresService := postgres.ProvideService(cfg) + postgresService := postgres.ProvideService(cfg, featureToggles) mysqlService := mysql.ProvideService() mssqlService := mssql.ProvideService(cfg) entityEventsService := store.ProvideEntityEventsService(cfg, sqlStore, featureToggles) @@ -939,7 +939,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { prometheusService := prometheus.ProvideService(httpclientProvider) tempoService := tempo.ProvideService(httpclientProvider) testdatasourceService := testdatasource.ProvideService() - postgresService := postgres.ProvideService(cfg) + postgresService := postgres.ProvideService(cfg, featureToggles) mysqlService := mysql.ProvideService() mssqlService := mssql.ProvideService(cfg) entityEventsService := store.ProvideEntityEventsService(cfg, sqlStore, featureToggles) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 94caceac460..0ac3c4d9ce2 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1655,6 +1655,12 @@ var ( Owner: grafanaObservabilityTracesAndProfilingSquad, FrontendOnly: true, }, + { + Name: "postgresDSUsePGX", + Description: "Enables using PGX instead of libpq for PostgreSQL datasource", + Stage: FeatureStageExperimental, + Owner: grafanaOSSBigTent, + }, { Name: "tempoAlerting", Description: "Enables creating alerts from Tempo data source", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 24de39bdf99..7b5c27e5123 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -215,6 +215,7 @@ localizationForPlugins,experimental,@grafana/plugins-platform-backend,false,fals unifiedNavbars,GA,@grafana/plugins-platform-backend,false,false,true logsPanelControls,preview,@grafana/observability-logs,false,false,true metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true +postgresDSUsePGX,experimental,@grafana/oss-big-tent,false,false,false tempoAlerting,experimental,@grafana/observability-traces-and-profiling,false,false,true pluginsAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false multiTenantFrontend,experimental,@grafana/grafana-frontend-platform,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index a5ae09b01b3..b1cc2fb7f13 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -871,6 +871,10 @@ const ( // Enables creating metrics from profiles and storing them as recording rules FlagMetricsFromProfiles = "metricsFromProfiles" + // FlagPostgresDSUsePGX + // Enables using PGX instead of libpq for PostgreSQL datasource + FlagPostgresDSUsePGX = "postgresDSUsePGX" + // FlagTempoAlerting // Enables creating alerts from Tempo data source FlagTempoAlerting = "tempoAlerting" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 6d6ecd4880c..048f5512581 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2413,6 +2413,19 @@ "expression": "false" } }, + { + "metadata": { + "name": "postgresDSUsePGX", + "resourceVersion": "1753174666056", + "creationTimestamp": "2025-05-26T06:54:18Z", + "deletionTimestamp": "2025-06-03T12:45:07Z" + }, + "spec": { + "description": "Enables using PGX instead of libpq for PostgreSQL datasource", + "stage": "experimental", + "codeowner": "@grafana/oss-big-tent" + } + }, { "metadata": { "name": "preferLibraryPanelTitle", diff --git a/pkg/services/pluginsintegration/plugintest/plugins_test.go b/pkg/services/pluginsintegration/plugintest/plugins_test.go index 1ba9fb78fea..d22dd7ab7d5 100644 --- a/pkg/services/pluginsintegration/plugintest/plugins_test.go +++ b/pkg/services/pluginsintegration/plugintest/plugins_test.go @@ -163,7 +163,7 @@ func TestIntegrationPluginManager(t *testing.T) { pr := prometheus.ProvideService(hcp) tmpo := tempo.ProvideService(hcp) td := testdatasource.ProvideService() - pg := postgres.ProvideService(cfg) + pg := postgres.ProvideService(cfg, features) my := mysql.ProvideService() ms := mssql.ProvideService(cfg) db := db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: cfg}) diff --git a/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager.go b/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager.go new file mode 100644 index 00000000000..89b43263aec --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager.go @@ -0,0 +1,151 @@ +package postgres + +import ( + "fmt" + "os" + + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" +) + +var validateCertFuncPgx = validateCertFilePathsPgx + +type pgxTlsManager struct { + logger log.Logger +} + +func newPgxTlsManager(logger log.Logger) *pgxTlsManager { + return &pgxTlsManager{ + logger: logger, + } +} + +// getTLSSettings retrieves TLS settings and handles certificate file creation if needed. +func (m *pgxTlsManager) getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) { + tlsConfig := tlsSettings{ + Mode: dsInfo.JsonData.Mode, + } + + if tlsConfig.Mode == "disable" { + m.logger.Debug("Postgres TLS/SSL is disabled") + return tlsConfig, nil + } + + tlsConfig.ConfigurationMethod = dsInfo.JsonData.ConfigurationMethod + tlsConfig.RootCertFile = dsInfo.JsonData.RootCertFile + tlsConfig.CertFile = dsInfo.JsonData.CertFile + tlsConfig.CertKeyFile = dsInfo.JsonData.CertKeyFile + + if tlsConfig.ConfigurationMethod == "file-content" { + if err := m.createCertFiles(dsInfo, &tlsConfig); err != nil { + return tlsConfig, fmt.Errorf("failed to create TLS certificate files: %w", err) + } + } else { + if err := validateCertFuncPgx(tlsConfig.RootCertFile, tlsConfig.CertFile, tlsConfig.CertKeyFile); err != nil { + return tlsConfig, fmt.Errorf("invalid TLS certificate file paths: %w", err) + } + } + + return tlsConfig, nil +} + +// createCertFiles writes certificate files to temporary locations. +func (m *pgxTlsManager) createCertFiles(dsInfo sqleng.DataSourceInfo, tlsConfig *tlsSettings) error { + m.logger.Debug("Writing TLS certificate files to temporary locations") + + var err error + if tlsConfig.RootCertFile, err = m.writeCertFile("root-*.crt", dsInfo.DecryptedSecureJSONData["tlsCACert"]); err != nil { + return err + } + if tlsConfig.CertFile, err = m.writeCertFile("client-*.crt", dsInfo.DecryptedSecureJSONData["tlsClientCert"]); err != nil { + return err + } + if tlsConfig.CertKeyFile, err = m.writeCertFile("client-*.key", dsInfo.DecryptedSecureJSONData["tlsClientKey"]); err != nil { + return err + } + + return nil +} + +// writeCertFile writes a single certificate file to a temporary location. +func (m *pgxTlsManager) writeCertFile(pattern, content string) (string, error) { + if content == "" { + return "", nil + } + + m.logger.Debug("Writing certificate file", "pattern", pattern) + file, err := os.CreateTemp("", pattern) + if err != nil { + return "", fmt.Errorf("failed to create temporary file: %w", err) + } + defer func() { + if err := file.Close(); err != nil { + m.logger.Error("Failed to close file", "error", err) + } + }() + + if _, err := file.WriteString(content); err != nil { + return "", fmt.Errorf("failed to write to temporary file: %w", err) + } + + return file.Name(), nil +} + +// cleanupCertFiles removes temporary certificate files. +func (m *pgxTlsManager) cleanupCertFiles(tlsConfig tlsSettings) { + // Only clean up if the configuration method is "file-content" + if tlsConfig.ConfigurationMethod != "file-content" { + m.logger.Debug("Skipping cleanup of TLS certificate files") + return + } + m.logger.Debug("Cleaning up TLS certificate files") + + files := []struct { + path string + name string + }{ + {tlsConfig.RootCertFile, "root certificate"}, + {tlsConfig.CertFile, "client certificate"}, + {tlsConfig.CertKeyFile, "client key"}, + } + + for _, file := range files { + if file.path == "" { + continue + } + if err := os.Remove(file.path); err != nil { + m.logger.Error("Failed to remove file", "type", file.name, "path", file.path, "error", err) + } else { + m.logger.Debug("Successfully removed file", "type", file.name, "path", file.path) + } + } +} + +// validateCertFilePaths validates the existence of configured certificate file paths. +func validateCertFilePathsPgx(rootCert, clientCert, clientKey string) error { + for _, path := range []string{rootCert, clientCert, clientKey} { + if path == "" { + continue + } + exists, err := fileExistsPgx(path) + if err != nil { + return fmt.Errorf("error checking file existence: %w", err) + } + if !exists { + return sqleng.ErrCertFileNotExist + } + } + return nil +} + +// fileExists checks if a file exists at the given path. +func fileExistsPgx(path string) (bool, error) { + _, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager_test.go b/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager_test.go new file mode 100644 index 00000000000..4717469348f --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager_test.go @@ -0,0 +1,245 @@ +package postgres + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test getTLSSettings. +func TestPgxGetTLSSettings(t *testing.T) { + mockValidateCertFilePathsPgx() + t.Cleanup(resetValidateCertFilePathsPgx) + + updatedTime := time.Now() + + testCases := []struct { + desc string + expErr string + jsonData sqleng.JsonData + secureJSONData map[string]string + uid string + tlsSettings tlsSettings + updated time.Time + }{ + { + desc: "Custom TLS authentication disabled", + updated: updatedTime, + jsonData: sqleng.JsonData{ + Mode: "disable", + RootCertFile: "i/am/coding/ca.crt", + CertFile: "i/am/coding/client.crt", + CertKeyFile: "i/am/coding/client.key", + ConfigurationMethod: "file-path", + }, + tlsSettings: tlsSettings{Mode: "disable"}, + }, + { + desc: "Custom TLS authentication with file path", + updated: updatedTime.Add(time.Minute), + jsonData: sqleng.JsonData{ + Mode: "verify-full", + ConfigurationMethod: "file-path", + RootCertFile: "i/am/coding/ca.crt", + CertFile: "i/am/coding/client.crt", + CertKeyFile: "i/am/coding/client.key", + }, + tlsSettings: tlsSettings{ + Mode: "verify-full", + ConfigurationMethod: "file-path", + RootCertFile: "i/am/coding/ca.crt", + CertFile: "i/am/coding/client.crt", + CertKeyFile: "i/am/coding/client.key", + }, + }, + } + for _, tt := range testCases { + t.Run(tt.desc, func(t *testing.T) { + var settings tlsSettings + var err error + mng := pgxTlsManager{ + logger: backend.NewLoggerWith("logger", "tsdb.postgres"), + } + + ds := sqleng.DataSourceInfo{ + JsonData: tt.jsonData, + DecryptedSecureJSONData: tt.secureJSONData, + UID: tt.uid, + Updated: tt.updated, + } + + settings, err = mng.getTLSSettings(ds) + + if tt.expErr == "" { + require.NoError(t, err, tt.desc) + assert.Equal(t, tt.tlsSettings, settings) + } else { + require.Error(t, err, tt.desc) + assert.True(t, strings.HasPrefix(err.Error(), tt.expErr), + fmt.Sprintf("%s: %q doesn't start with %q", tt.desc, err, tt.expErr)) + } + }) + } +} + +func mockValidateCertFilePathsPgx() { + validateCertFuncPgx = func(rootCert, clientCert, clientKey string) error { + return nil + } +} + +func resetValidateCertFilePathsPgx() { + validateCertFuncPgx = validateCertFilePathsPgx +} + +func TestTLSManager_GetTLSSettings(t *testing.T) { + logger := log.New() + tlsManager := newPgxTlsManager(logger) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "file-content", + }, + DecryptedSecureJSONData: map[string]string{ + "tlsCACert": "root-cert-content", + "tlsClientCert": "client-cert-content", + "tlsClientKey": "client-key-content", + }, + } + + tlsConfig, err := tlsManager.getTLSSettings(dsInfo) + require.NoError(t, err) + assert.Equal(t, "require", tlsConfig.Mode) + assert.NotEmpty(t, tlsConfig.RootCertFile) + assert.NotEmpty(t, tlsConfig.CertFile) + assert.NotEmpty(t, tlsConfig.CertKeyFile) + + // Cleanup temporary files + tlsManager.cleanupCertFiles(tlsConfig) + assert.NoFileExists(t, tlsConfig.RootCertFile) + assert.NoFileExists(t, tlsConfig.CertFile) + assert.NoFileExists(t, tlsConfig.CertKeyFile) +} + +func TestTLSManager_CleanupCertFiles_FilePath(t *testing.T) { + logger := log.New() + tlsManager := newPgxTlsManager(logger) + + // Create temporary files for testing + rootCertFile, err := tlsManager.writeCertFile("root-*.crt", "root-cert-content") + require.NoError(t, err) + clientCertFile, err := tlsManager.writeCertFile("client-*.crt", "client-cert-content") + require.NoError(t, err) + clientKeyFile, err := tlsManager.writeCertFile("client-*.key", "client-key-content") + require.NoError(t, err) + + // Simulate a configuration where the method is "file-path" + tlsConfig := tlsSettings{ + ConfigurationMethod: "file-path", + RootCertFile: rootCertFile, + CertFile: clientCertFile, + CertKeyFile: clientKeyFile, + } + + // Call cleanupCertFiles + tlsManager.cleanupCertFiles(tlsConfig) + + // Verify the files are NOT deleted + assert.FileExists(t, rootCertFile, "Root certificate file should not be deleted") + assert.FileExists(t, clientCertFile, "Client certificate file should not be deleted") + assert.FileExists(t, clientKeyFile, "Client key file should not be deleted") + + // Cleanup the files manually + err = os.Remove(rootCertFile) + require.NoError(t, err) + err = os.Remove(clientCertFile) + require.NoError(t, err) + err = os.Remove(clientKeyFile) + require.NoError(t, err) +} + +func TestTLSManager_CreateCertFiles(t *testing.T) { + logger := log.New() + tlsManager := newPgxTlsManager(logger) + + dsInfo := sqleng.DataSourceInfo{ + DecryptedSecureJSONData: map[string]string{ + "tlsCACert": "root-cert-content", + "tlsClientCert": "client-cert-content", + "tlsClientKey": "client-key-content", + }, + } + + tlsConfig := tlsSettings{ + ConfigurationMethod: "file-content", + } + err := tlsManager.createCertFiles(dsInfo, &tlsConfig) + require.NoError(t, err) + + assert.FileExists(t, tlsConfig.RootCertFile) + assert.FileExists(t, tlsConfig.CertFile) + assert.FileExists(t, tlsConfig.CertKeyFile) + + // Cleanup temporary files + tlsManager.cleanupCertFiles(tlsConfig) + assert.NoFileExists(t, tlsConfig.RootCertFile) + assert.NoFileExists(t, tlsConfig.CertFile) + assert.NoFileExists(t, tlsConfig.CertKeyFile) +} + +func TestTLSManager_WriteCertFile(t *testing.T) { + logger := log.New() + tlsManager := newPgxTlsManager(logger) + + // Test writing a valid certificate file + filePath, err := tlsManager.writeCertFile("test-*.crt", "test-cert-content") + require.NoError(t, err) + assert.FileExists(t, filePath) + + content, err := os.ReadFile(filepath.Clean(filePath)) + require.NoError(t, err) + assert.Equal(t, "test-cert-content", string(content)) + + // Cleanup the file + err = os.Remove(filePath) + require.NoError(t, err) + assert.NoFileExists(t, filePath) +} + +func TestTLSManager_CleanupCertFiles(t *testing.T) { + logger := log.New() + tlsManager := newPgxTlsManager(logger) + + // Create temporary files for testing + rootCertFile, err := tlsManager.writeCertFile("root-*.crt", "root-cert-content") + require.NoError(t, err) + clientCertFile, err := tlsManager.writeCertFile("client-*.crt", "client-cert-content") + require.NoError(t, err) + clientKeyFile, err := tlsManager.writeCertFile("client-*.key", "client-key-content") + require.NoError(t, err) + + tlsConfig := tlsSettings{ + ConfigurationMethod: "file-content", + RootCertFile: rootCertFile, + CertFile: clientCertFile, + CertKeyFile: clientKeyFile, + } + + // Cleanup the files + tlsManager.cleanupCertFiles(tlsConfig) + + // Verify the files are deleted + assert.NoFileExists(t, rootCertFile) + assert.NoFileExists(t, clientCertFile) + assert.NoFileExists(t, clientKeyFile) +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres.go b/pkg/tsdb/grafana-postgresql-datasource/postgres.go index fde06c08172..a2ef32bbd76 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres.go @@ -15,27 +15,33 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "github.com/jackc/pgx/v5/pgxpool" "github.com/lib/pq" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" ) -func ProvideService(cfg *setting.Cfg) *Service { +func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles) *Service { logger := backend.NewLoggerWith("logger", "tsdb.postgres") s := &Service{ - tlsManager: newTLSManager(logger, cfg.DataPath), - logger: logger, + tlsManager: newTLSManager(logger, cfg.DataPath), + pgxTlsManager: newPgxTlsManager(logger), + logger: logger, + features: features, } s.im = datasource.NewInstanceManager(s.newInstanceSettings()) return s } type Service struct { - tlsManager tlsSettingsProvider - im instancemgmt.InstanceManager - logger log.Logger + tlsManager tlsSettingsProvider + pgxTlsManager *pgxTlsManager + im instancemgmt.InstanceManager + logger log.Logger + features featuremgmt.FeatureToggles } func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext) (*sqleng.DataSourceHandler, error) { @@ -52,6 +58,11 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) if err != nil { return nil, err } + + if s.features.IsEnabled(ctx, featuremgmt.FlagPostgresDSUsePGX) { + return dsInfo.QueryDataPGX(ctx, req) + } + return dsInfo.QueryData(ctx, req) } @@ -104,6 +115,62 @@ func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit in return db, handler, nil } +func newPostgresPGX(ctx context.Context, userFacingDefaultError string, rowLimit int64, dsInfo sqleng.DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*pgxpool.Pool, *sqleng.DataSourceHandler, error) { + pgxConf, err := pgxpool.ParseConfig(cnnstr) + if err != nil { + logger.Error("postgres config creation failed", "error", err) + return nil, nil, fmt.Errorf("postgres config creation failed") + } + + proxyClient, err := settings.ProxyClient(ctx) + if err != nil { + logger.Error("postgres proxy creation failed", "error", err) + return nil, nil, fmt.Errorf("postgres proxy creation failed") + } + + if proxyClient.SecureSocksProxyEnabled() { + dialer, err := proxyClient.NewSecureSocksProxyContextDialer() + if err != nil { + logger.Error("postgres proxy creation failed", "error", err) + return nil, nil, fmt.Errorf("postgres proxy creation failed") + } + + pgxConf.ConnConfig.DialFunc = newPgxDialFunc(dialer) + } + + // by default pgx resolves hostnames to ip addresses. we must avoid this. + // (certain socks-proxy related functionality relies on the hostname being preserved) + pgxConf.ConnConfig.LookupFunc = func(_ context.Context, host string) ([]string, error) { + return []string{host}, nil + } + + config := sqleng.DataPluginConfiguration{ + DSInfo: dsInfo, + MetricColumnTypes: []string{"unknown", "text", "varchar", "char", "bpchar"}, + RowLimit: rowLimit, + } + + queryResultTransformer := postgresQueryResultTransformer{} + pgxConf.MaxConnLifetime = time.Duration(config.DSInfo.JsonData.ConnMaxLifetime) * time.Second + pgxConf.MaxConns = int32(config.DSInfo.JsonData.MaxOpenConns) + + p, err := pgxpool.NewWithConfig(ctx, pgxConf) + if err != nil { + logger.Error("Failed connecting to Postgres", "err", err) + return nil, nil, err + } + + handler, err := sqleng.NewQueryDataHandlerPGX(userFacingDefaultError, p, config, &queryResultTransformer, newPostgresMacroEngine(dsInfo.JsonData.Timescaledb), + logger) + if err != nil { + logger.Error("Failed connecting to Postgres", "err", err) + return nil, nil, err + } + + logger.Debug("Successfully connected to Postgres") + return p, handler, nil +} + func (s *Service) newInstanceSettings() datasource.InstanceFactoryFunc { logger := s.logger return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { @@ -143,21 +210,45 @@ func (s *Service) newInstanceSettings() datasource.InstanceFactoryFunc { DecryptedSecureJSONData: settings.DecryptedSecureJSONData, } - cnnstr, err := s.generateConnectionString(dsInfo) - if err != nil { - return nil, err - } + isPGX := s.features.IsEnabled(ctx, featuremgmt.FlagPostgresDSUsePGX) userFacingDefaultError, err := cfg.UserFacingDefaultError() if err != nil { return nil, err } - _, handler, err := newPostgres(ctx, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, cnnstr, logger, settings) + var handler instancemgmt.Instance + if isPGX { + pgxTlsSettings, err := s.pgxTlsManager.getTLSSettings(dsInfo) + if err != nil { + return "", err + } - if err != nil { - logger.Error("Failed connecting to Postgres", "err", err) - return nil, err + // Ensure cleanupCertFiles is called after the connection is opened + defer s.pgxTlsManager.cleanupCertFiles(pgxTlsSettings) + cnnstr, err := s.generateConnectionString(dsInfo, pgxTlsSettings, isPGX) + if err != nil { + return "", err + } + _, handler, err = newPostgresPGX(ctx, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, cnnstr, logger, settings) + if err != nil { + logger.Error("Failed connecting to Postgres", "err", err) + return nil, err + } + } else { + tlsSettings, err := s.tlsManager.getTLSSettings(dsInfo) + if err != nil { + return "", err + } + cnnstr, err := s.generateConnectionString(dsInfo, tlsSettings, isPGX) + if err != nil { + return nil, err + } + _, handler, err = newPostgres(ctx, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, cnnstr, logger, settings) + if err != nil { + logger.Error("Failed connecting to Postgres", "err", err) + return nil, err + } } logger.Debug("Successfully connected to Postgres") @@ -170,65 +261,100 @@ func escape(input string) string { return strings.ReplaceAll(strings.ReplaceAll(input, `\`, `\\`), "'", `\'`) } -func (s *Service) generateConnectionString(dsInfo sqleng.DataSourceInfo) (string, error) { - logger := s.logger - var host string - var port int +type connectionParams struct { + host string + port int + user string + password string + database string +} + +func parseConnectionParams(dsInfo sqleng.DataSourceInfo, logger log.Logger) (connectionParams, error) { + var params connectionParams + var err error + if strings.HasPrefix(dsInfo.URL, "/") { - host = dsInfo.URL + params.host = dsInfo.URL logger.Debug("Generating connection string with Unix socket specifier", "address", dsInfo.URL) } else { - index := strings.LastIndex(dsInfo.URL, ":") - v6Index := strings.Index(dsInfo.URL, "]") - sp := strings.SplitN(dsInfo.URL, ":", 2) - host = sp[0] - if v6Index == -1 { - if len(sp) > 1 { - var err error - port, err = strconv.Atoi(sp[1]) - if err != nil { - logger.Debug("Error parsing the IPv4 address", "address", dsInfo.URL) - return "", sqleng.ErrParsingPostgresURL - } - logger.Debug("Generating IPv4 connection string with network host/port pair", "host", host, "port", port, "address", dsInfo.URL) - } else { - logger.Debug("Generating IPv4 connection string with network host", "host", host, "address", dsInfo.URL) - } - } else { - if index == v6Index+1 { - host = dsInfo.URL[1 : index-1] - var err error - port, err = strconv.Atoi(dsInfo.URL[index+1:]) - if err != nil { - logger.Debug("Error parsing the IPv6 address", "address", dsInfo.URL) - return "", sqleng.ErrParsingPostgresURL - } - logger.Debug("Generating IPv6 connection string with network host/port pair", "host", host, "port", port, "address", dsInfo.URL) - } else { - host = dsInfo.URL[1 : len(dsInfo.URL)-1] - logger.Debug("Generating IPv6 connection string with network host", "host", host, "address", dsInfo.URL) - } + params.host, params.port, err = parseNetworkAddress(dsInfo.URL, logger) + if err != nil { + return connectionParams{}, err } } - connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s'", - escape(dsInfo.User), escape(dsInfo.DecryptedSecureJSONData["password"]), escape(host), escape(dsInfo.Database)) - if port > 0 { - connStr += fmt.Sprintf(" port=%d", port) + params.user = dsInfo.User + params.password = dsInfo.DecryptedSecureJSONData["password"] + params.database = dsInfo.Database + + return params, nil +} + +func parseNetworkAddress(url string, logger log.Logger) (string, int, error) { + index := strings.LastIndex(url, ":") + v6Index := strings.Index(url, "]") + sp := strings.SplitN(url, ":", 2) + host := sp[0] + port := 0 + + if v6Index == -1 { + if len(sp) > 1 { + var err error + port, err = strconv.Atoi(sp[1]) + if err != nil { + logger.Debug("Error parsing the IPv4 address", "address", url) + return "", 0, sqleng.ErrParsingPostgresURL + } + logger.Debug("Generating IPv4 connection string with network host/port pair", "host", host, "port", port, "address", url) + } else { + logger.Debug("Generating IPv4 connection string with network host", "host", host, "address", url) + } + } else { + if index == v6Index+1 { + host = url[1 : index-1] + var err error + port, err = strconv.Atoi(url[index+1:]) + if err != nil { + logger.Debug("Error parsing the IPv6 address", "address", url) + return "", 0, sqleng.ErrParsingPostgresURL + } + logger.Debug("Generating IPv6 connection string with network host/port pair", "host", host, "port", port, "address", url) + } else { + host = url[1 : len(url)-1] + logger.Debug("Generating IPv6 connection string with network host", "host", host, "address", url) + } } - tlsSettings, err := s.tlsManager.getTLSSettings(dsInfo) + return host, port, nil +} + +func buildBaseConnectionString(params connectionParams) string { + connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s'", + escape(params.user), escape(params.password), escape(params.host), escape(params.database)) + if params.port > 0 { + connStr += fmt.Sprintf(" port=%d", params.port) + } + return connStr +} + +func (s *Service) generateConnectionString(dsInfo sqleng.DataSourceInfo, tlsSettings tlsSettings, isPGX bool) (string, error) { + logger := s.logger + + params, err := parseConnectionParams(dsInfo, logger) if err != nil { return "", err } + connStr := buildBaseConnectionString(params) + connStr += fmt.Sprintf(" sslmode='%s'", escape(tlsSettings.Mode)) // there is an issue with the lib/pq module, the `verify-ca` tls mode // does not work correctly. ( see https://github.com/lib/pq/issues/1106 ) // to workaround the problem, if the `verify-ca` mode is chosen, // we disable sslsni. - if tlsSettings.Mode == "verify-ca" { + if tlsSettings.Mode == "verify-ca" && !isPGX { + logger.Debug("Disabling sslsni for verify-ca mode") connStr += " sslsni=0" } @@ -262,7 +388,7 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque if err != nil { return sqleng.ErrToHealthCheckResult(err) } - return dsHandler.CheckHealth(ctx, req) + return dsHandler.CheckHealth(ctx, req, s.features) } func (t *postgresQueryResultTransformer) GetConverterList() []sqlutil.StringConverter { diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_snapshot_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_snapshot_test.go new file mode 100644 index 00000000000..5ff81b615a0 --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_snapshot_test.go @@ -0,0 +1,195 @@ +package postgres + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" +) + +// These tests require a real postgres database: +// - make devenv sources=postgres_tests +// - either set the env variable GRAFANA_TEST_DB = postgres +// - or set `forceRun := true` below +// +// The tests require a PostgreSQL db named grafanadstest and a user/password grafanatest/grafanatest! +// Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a +// preconfigured Postgres server suitable for running these tests. +func TestIntegrationPostgresPGXSnapshots(t *testing.T) { + // the logic in this function is copied from postgres_tests.go + shouldRunTest := func() bool { + if testing.Short() { + return false + } + + testDbName, present := os.LookupEnv("GRAFANA_TEST_DB") + + if present && testDbName == "postgres" { + return true + } + + return false + } + + if !shouldRunTest() { + t.Skip() + } + + getCnnStr := func() string { + host := os.Getenv("POSTGRES_HOST") + if host == "" { + host = "localhost" + } + port := os.Getenv("POSTGRES_PORT") + if port == "" { + port = "5432" + } + + return fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", + host, port) + } + + sqlQueryCommentRe := regexp.MustCompile(`^-- (.+)\n`) + + readSqlFile := func(path string) (string, string) { + // the file-path is not coming from the outside, + // it is hardcoded in this file. + //nolint:gosec + sqlBytes, err := os.ReadFile(path) + require.NoError(t, err) + + sql := string(sqlBytes) + + // first line of the file contains the sql query to run, commented out + match := sqlQueryCommentRe.FindStringSubmatch(sql) + require.Len(t, match, 2) + + rawSQL := strings.TrimSpace(match[1]) + + return rawSQL, sql + } + + makeQuery := func(rawSQL string, format string) backend.QueryDataRequest { + queryData := map[string]string{ + "rawSql": rawSQL, + "format": format, + } + + queryBytes, err := json.Marshal(queryData) + require.NoError(t, err) + + return backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: queryBytes, + RefID: "A", + TimeRange: backend.TimeRange{ + From: time.Date(2023, 12, 24, 14, 15, 22, 123456, time.UTC), + To: time.Date(2023, 12, 24, 14, 45, 13, 876543, time.UTC), + }, + }, + }, + } + } + + tt := []struct { + name string + format string + }{ + {format: "time_series", name: "simple"}, + {format: "time_series", name: "no_rows_long"}, + {format: "time_series", name: "no_rows_wide"}, + {format: "time_series", name: "7x_compat_metric_label"}, + {format: "time_series", name: "convert_to_float64"}, + {format: "time_series", name: "convert_to_float64_not"}, + {format: "time_series", name: "fill_null"}, + {format: "time_series", name: "fill_previous"}, + {format: "time_series", name: "fill_value"}, + {format: "time_series", name: "fill_value_wide"}, + {format: "table", name: "simple"}, + {format: "table", name: "multi_stat1"}, + {format: "table", name: "multi_stat2"}, + {format: "table", name: "no_rows"}, + {format: "table", name: "types_numeric"}, + {format: "table", name: "types_char"}, + {format: "table", name: "types_datetime_pgx"}, + {format: "table", name: "types_other"}, + {format: "table", name: "timestamp_convert_bigint"}, + {format: "table", name: "timestamp_convert_integer"}, + {format: "table", name: "timestamp_convert_real"}, + {format: "table", name: "timestamp_convert_double"}, + {format: "table", name: "time_group_compat_case1"}, + {format: "table", name: "time_group_compat_case2"}, + } + + for _, test := range tt { + require.True(t, test.format == "table" || test.format == "time_series") + t.Run(test.name, func(t *testing.T) { + origInterpolate := sqleng.Interpolate + t.Cleanup(func() { + sqleng.Interpolate = origInterpolate + }) + + sqleng.Interpolate = func(query backend.DataQuery, timeRange backend.TimeRange, timeInterval string, sql string) string { + return sql + } + + jsonData := sqleng.JsonData{ + MaxOpenConns: 10, + MaxIdleConns: 2, + ConnMaxLifetime: 14400, + Timescaledb: false, + Mode: "disable", + ConfigurationMethod: "file-path", + } + + dsInfo := sqleng.DataSourceInfo{ + JsonData: jsonData, + DecryptedSecureJSONData: map[string]string{}, + } + + logger := log.New() + + cnnstr := getCnnStr() + + p, handler, err := newPostgresPGX(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + + t.Cleanup((func() { + _, err := p.Exec(context.Background(), "DROP TABLE tbl") + require.NoError(t, err) + p.Close() + })) + + require.NoError(t, err) + + sqlFilePath := filepath.Join("testdata", test.format, test.name+".sql") + goldenFileName := filepath.Join(test.format, test.name+".golden") + + rawSQL, sql := readSqlFile(sqlFilePath) + + _, err = p.Exec(context.Background(), sql) + require.NoError(t, err) + + query := makeQuery(rawSQL, test.format) + + result, err := handler.QueryDataPGX(context.Background(), &query) + require.Len(t, result.Responses, 1) + response, found := result.Responses["A"] + require.True(t, found) + require.NoError(t, err) + experimental.CheckGoldenJSONResponse(t, "testdata", goldenFileName, &response, updateGoldenFiles) + }) + } +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go new file mode 100644 index 00000000000..3e7263d1de2 --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go @@ -0,0 +1,1382 @@ +package postgres + +import ( + "context" + "fmt" + "math/rand" + "strings" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" + + _ "github.com/lib/pq" +) + +// Test generateConnectionString. +func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + testCases := []struct { + desc string + host string + user string + password string + database string + tlsSettings tlsSettings + expConnStr string + expErr string + uid string + }{ + { + desc: "Unix socket host", + host: "/var/run/postgresql", + user: "user", + password: "password", + database: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' password='password' host='/var/run/postgresql' dbname='database' sslmode='verify-full'", + }, + { + desc: "TCP host", + host: "host", + user: "user", + password: "password", + database: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full'", + }, + { + desc: "verify-ca automatically adds disable-sni", + host: "host:1234", + user: "user", + password: "password", + database: "database", + tlsSettings: tlsSettings{Mode: "verify-ca"}, + expConnStr: "user='user' password='password' host='host' dbname='database' port=1234 sslmode='verify-ca' sslsni=0", + }, + { + desc: "TCP/port host", + host: "host:1234", + user: "user", + password: "password", + database: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' password='password' host='host' dbname='database' port=1234 sslmode='verify-full'", + }, + { + desc: "Ipv6 host", + host: "[::1]", + user: "user", + password: "password", + database: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' password='password' host='::1' dbname='database' sslmode='verify-full'", + }, + { + desc: "Ipv6/port host", + host: "[::1]:1234", + user: "user", + password: "password", + database: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' password='password' host='::1' dbname='database' port=1234 sslmode='verify-full'", + }, + { + desc: "Invalid port", + host: "host:invalid", + user: "user", + database: "database", + tlsSettings: tlsSettings{}, + expErr: "error parsing postgres url", + }, + { + desc: "Password with single quote and backslash", + host: "host", + user: "user", + password: `p'\assword`, + database: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: `user='user' password='p\'\\assword' host='host' dbname='database' sslmode='verify-full'`, + }, + { + desc: "User/DB with single quote and backslash", + host: "host", + user: `u'\ser`, + password: `password`, + database: `d'\atabase`, + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: `user='u\'\\ser' password='password' host='host' dbname='d\'\\atabase' sslmode='verify-full'`, + }, + { + desc: "Custom TLS mode disabled", + host: "host", + user: "user", + password: "password", + database: "database", + tlsSettings: tlsSettings{Mode: "disable"}, + expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='disable'", + }, + { + desc: "Custom TLS mode verify-full with certificate files", + host: "host", + user: "user", + password: "password", + database: "database", + tlsSettings: tlsSettings{ + Mode: "verify-full", + RootCertFile: "i/am/coding/ca.crt", + CertFile: "i/am/coding/client.crt", + CertKeyFile: "i/am/coding/client.key", + }, + expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full' " + + "sslrootcert='i/am/coding/ca.crt' sslcert='i/am/coding/client.crt' sslkey='i/am/coding/client.key'", + }, + } + for _, tt := range testCases { + t.Run(tt.desc, func(t *testing.T) { + svc := Service{ + logger: backend.NewLoggerWith("logger", "tsdb.postgres"), + } + + ds := sqleng.DataSourceInfo{ + URL: tt.host, + User: tt.user, + DecryptedSecureJSONData: map[string]string{"password": tt.password}, + Database: tt.database, + UID: tt.uid, + } + + connStr, err := svc.generateConnectionString(ds, tt.tlsSettings, false) + + if tt.expErr == "" { + require.NoError(t, err, tt.desc) + assert.Equal(t, tt.expConnStr, connStr) + } else { + require.Error(t, err, tt.desc) + assert.True(t, strings.HasPrefix(err.Error(), tt.expErr), + fmt.Sprintf("%s: %q doesn't start with %q", tt.desc, err, tt.expErr)) + } + }) + } +} + +// To run this test, set runPostgresTests=true +// Or from the commandline: GRAFANA_TEST_DB=postgres go test -tags=integration -v ./pkg/tsdb/grafana-postgresql-datasource +// The tests require a PostgreSQL db named grafanadstest and a user/password grafanatest/grafanatest! +// Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a +// preconfigured Postgres server suitable for running these tests. +// There is also a datasource and dashboard provisioned by devenv scripts that you can +// use to verify that the generated data are visualized as expected, see +// devenv/README.md for setup instructions. +func TestIntegrationPostgresPGX(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + // change to true to run the PostgreSQL tests + const runPostgresTests = false + + if !isTestDbPostgres() && !runPostgresTests { + t.Skip() + } + + origInterpolate := sqleng.Interpolate + t.Cleanup(func() { + sqleng.Interpolate = origInterpolate + }) + sqleng.Interpolate = func(query backend.DataQuery, timeRange backend.TimeRange, timeInterval string, sql string) string { + return sql + } + + jsonData := sqleng.JsonData{ + MaxOpenConns: 10, + MaxIdleConns: 2, + ConnMaxLifetime: 14400, + Timescaledb: false, + Mode: "disable", + ConfigurationMethod: "file-path", + } + + dsInfo := sqleng.DataSourceInfo{ + JsonData: jsonData, + DecryptedSecureJSONData: map[string]string{}, + } + + logger := backend.NewLoggerWith("logger", "postgres.test") + + cnnstr := postgresTestDBConnString() + + p, exe, err := newPostgresPGX(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + + require.NoError(t, err) + + fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) + + t.Run("Given a table with different native data types", func(t *testing.T) { + sql := ` + DROP TABLE IF EXISTS postgres_types; + CREATE TABLE postgres_types( + c00_smallint smallint, + c01_integer integer, + c02_bigint bigint, + + c03_real real, + c04_double double precision, + c05_decimal decimal(10,2), + c06_numeric numeric(10,2), + + c07_char char(10), + c08_varchar varchar(10), + c09_text text, + + c10_timestamp timestamp without time zone, + c11_timestamptz timestamp with time zone, + c12_date date, + c13_time time without time zone, + c14_timetz time with time zone, + time date, + c15_interval interval, + c16_smallint smallint + ); + ` + _, err := p.Exec(context.Background(), sql) + require.NoError(t, err) + + sql = ` + INSERT INTO postgres_types VALUES( + 1,2,3, + 4.5,6.7,1.1,1.2, + 'char10','varchar10','text', + + now(),now(),now(),now(),now(),now(),'15m'::interval, + null + ); + ` + _, err = p.Exec(context.Background(), sql) + require.NoError(t, err) + + t.Run("When doing a table query should map Postgres column types to Go types", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT * FROM postgres_types", + "format": "table" + }`), + RefID: "A", + }, + }, + } + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Len(t, frames, 1) + require.Len(t, frames[0].Fields, 18) + + require.Equal(t, int16(1), *frames[0].Fields[0].At(0).(*int16)) + require.Equal(t, int32(2), *frames[0].Fields[1].At(0).(*int32)) + require.Equal(t, int64(3), *frames[0].Fields[2].At(0).(*int64)) + + require.Equal(t, float64(4.5), *frames[0].Fields[3].At(0).(*float64)) + require.Equal(t, float64(6.7), *frames[0].Fields[4].At(0).(*float64)) + require.Equal(t, float64(1.1), *frames[0].Fields[5].At(0).(*float64)) + require.Equal(t, float64(1.2), *frames[0].Fields[6].At(0).(*float64)) + + require.Equal(t, "char10 ", *frames[0].Fields[7].At(0).(*string)) + require.Equal(t, "varchar10", *frames[0].Fields[8].At(0).(*string)) + require.Equal(t, "text", *frames[0].Fields[9].At(0).(*string)) + + _, ok := frames[0].Fields[10].At(0).(*time.Time) + require.True(t, ok) + _, ok = frames[0].Fields[11].At(0).(*time.Time) + require.True(t, ok) + _, ok = frames[0].Fields[12].At(0).(*time.Time) + require.True(t, ok) + _, ok = frames[0].Fields[13].At(0).(*string) + require.True(t, ok) + _, ok = frames[0].Fields[14].At(0).(*string) + require.True(t, ok) + _, ok = frames[0].Fields[15].At(0).(*time.Time) + require.True(t, ok) + require.Equal(t, "00:15:00", *frames[0].Fields[16].At(0).(*string)) + require.Nil(t, frames[0].Fields[17].At(0)) + }) + }) + + t.Run("Given a table with metrics that lacks data for some series ", func(t *testing.T) { + sql := ` + DROP TABLE IF EXISTS metric; + CREATE TABLE metric ( + time timestamp, + value integer + ) + ` + + _, err := p.Exec(context.Background(), sql) + require.NoError(t, err) + + type metric struct { + Time time.Time + Value int64 + } + + series := []*metric{} + firstRange := genTimeRangeByInterval(fromStart, 10*time.Minute, 10*time.Second) + secondRange := genTimeRangeByInterval(fromStart.Add(20*time.Minute), 10*time.Minute, 10*time.Second) + + for _, t := range firstRange { + series = append(series, &metric{ + Time: t, + Value: 15, + }) + } + + for _, t := range secondRange { + series = append(series, &metric{ + Time: t, + Value: 20, + }) + } + + for _, m := range series { + _, err := p.Exec(context.Background(), `INSERT INTO metric ("time", value) VALUES ($1, $2)`, m.Time.UTC(), m.Value) + require.NoError(t, err) + } + + t.Run("When doing a metric query using timeGroup", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Len(t, frames, 1) + require.Equal(t, 4, frames[0].Fields[0].Len()) + + // without fill this should result in 4 buckets + + dt := fromStart + + for i := 0; i < 2; i++ { + aValue := *frames[0].Fields[1].At(i).(*float64) + aTime := *frames[0].Fields[0].At(i).(*time.Time) + require.Equal(t, float64(15), aValue) + require.Equal(t, dt, aTime) + dt = dt.Add(5 * time.Minute) + } + + // adjust for 10 minute gap between first and second set of points + dt = dt.Add(10 * time.Minute) + for i := 2; i < 4; i++ { + aValue := *frames[0].Fields[1].At(i).(*float64) + aTime := *frames[0].Fields[0].At(i).(*time.Time) + require.Equal(t, float64(20), aValue) + require.Equal(t, dt, aTime) + dt = dt.Add(5 * time.Minute) + } + }) + + t.Run("When doing a metric query using timeGroup and $__interval", func(t *testing.T) { + mockInterpolate := sqleng.Interpolate + sqleng.Interpolate = origInterpolate + t.Cleanup(func() { + sqleng.Interpolate = mockInterpolate + }) + + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT $__timeGroup(time, $__interval) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series" + }`), + RefID: "A", + Interval: time.Second * 60, + TimeRange: backend.TimeRange{ + From: fromStart, + To: fromStart.Add(30 * time.Minute), + }, + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + frames := queryResult.Frames + + require.NoError(t, queryResult.Error) + require.Equal(t, + "SELECT floor(extract(epoch from time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + frames[0].Meta.ExecutedQueryString) + }) + + t.Run("When doing a metric query using timeGroup with NULL fill enabled", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series" + }`), + RefID: "A", + TimeRange: backend.TimeRange{ + From: fromStart, + To: fromStart.Add(34 * time.Minute), + }, + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 7, frames[0].Fields[0].Len()) + + dt := fromStart + + for i := 0; i < 2; i++ { + aValue := *frames[0].Fields[1].At(i).(*float64) + aTime := *frames[0].Fields[0].At(i).(*time.Time) + require.Equal(t, float64(15), aValue) + require.True(t, aTime.Equal(dt)) + dt = dt.Add(5 * time.Minute) + } + + // check for NULL values inserted by fill + require.Nil(t, frames[0].Fields[1].At(2)) + require.Nil(t, frames[0].Fields[1].At(3)) + + // adjust for 10 minute gap between first and second set of points + dt = dt.Add(10 * time.Minute) + for i := 4; i < 6; i++ { + aValue := *frames[0].Fields[1].At(i).(*float64) + aTime := *frames[0].Fields[0].At(i).(*time.Time) + require.Equal(t, float64(20), aValue) + require.True(t, aTime.Equal(dt)) + dt = dt.Add(5 * time.Minute) + } + + // check for NULL values inserted by fill + require.Nil(t, frames[0].Fields[1].At(6)) + }) + + t.Run("When doing a metric query using timeGroup with value fill enabled", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT $__timeGroup(time, '5m', 1.5) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series" + }`), + RefID: "A", + TimeRange: backend.TimeRange{ + From: fromStart, + To: fromStart.Add(34 * time.Minute), + }, + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 1.5, *frames[0].Fields[1].At(3).(*float64)) + }) + }) + + t.Run("Given a table with one data point", func(t *testing.T) { + type metric struct { + Time time.Time + Value int64 + } + + startTime := time.Now().UTC().Add(-time.Minute * 5) + series := []*metric{ + { + Time: startTime, + Value: 33, + }, + } + + for _, m := range series { + _, err := p.Exec(context.Background(), `INSERT INTO metric ("time", value) VALUES ($1, $2)`, m.Time.UTC(), m.Value) + require.NoError(t, err) + } + + t.Run("querying with time group with default value", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "WITH data AS (SELECT now()-'3m'::interval AS ts, 42 AS n) SELECT $__timeGroup(ts, '1m', 0), n FROM data", + "format": "time_series" + }`), + RefID: "A", + TimeRange: backend.TimeRange{ + From: startTime, + To: startTime.Add(5 * time.Minute), + }, + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, "Time", frames[0].Fields[0].Name) + require.Equal(t, "n", frames[0].Fields[1].Name) + require.Equal(t, float64(0), *frames[0].Fields[1].At(0).(*float64)) + require.Equal(t, float64(0), *frames[0].Fields[1].At(1).(*float64)) + require.Equal(t, float64(42), *frames[0].Fields[1].At(2).(*float64)) + require.Equal(t, float64(0), *frames[0].Fields[1].At(3).(*float64)) + require.Equal(t, float64(0), *frames[0].Fields[1].At(4).(*float64)) + require.Equal(t, float64(0), *frames[0].Fields[1].At(5).(*float64)) + }) + }) + + t.Run("When doing a metric query using timeGroup with previous fill enabled", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT $__timeGroup(time, '5m', previous), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series" + }`), + RefID: "A", + TimeRange: backend.TimeRange{ + From: fromStart, + To: fromStart.Add(34 * time.Minute), + }, + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, float64(15.0), *frames[0].Fields[1].At(2).(*float64)) + require.Equal(t, float64(15.0), *frames[0].Fields[1].At(3).(*float64)) + require.Equal(t, float64(20.0), *frames[0].Fields[1].At(6).(*float64)) + }) + + t.Run("Given a table with metrics having multiple values and measurements", func(t *testing.T) { + type metric_values struct { + Time time.Time + TimeInt64 int64 + TimeInt64Nullable *int64 + TimeFloat64 float64 + TimeFloat64Nullable *float64 + TimeInt32 int32 + TimeInt32Nullable *int32 + TimeFloat32 float32 + TimeFloat32Nullable *float32 + Measurement string + ValueOne int64 + ValueTwo int64 + } + + _, err := p.Exec(context.Background(), "DROP TABLE IF EXISTS metric_values") + require.NoError(t, err) + + _, err = p.Exec(context.Background(), `CREATE TABLE metric_values ( + "time" TIMESTAMP NULL, + "timeInt64" BIGINT NOT NULL, "timeInt64Nullable" BIGINT NULL, + "timeFloat64" DOUBLE PRECISION NOT NULL, "timeFloat64Nullable" DOUBLE PRECISION NULL, + "timeInt32" INTEGER NOT NULL, "timeInt32Nullable" INTEGER NULL, + "timeFloat32" DOUBLE PRECISION NOT NULL, "timeFloat32Nullable" DOUBLE PRECISION NULL, + measurement VARCHAR(255) NULL, + "valueOne" INTEGER NULL, "valueTwo" INTEGER NULL + )`) + require.NoError(t, err) + + rng := rand.New(rand.NewSource(time.Now().Unix())) + rnd := func(min, max int64) int64 { + return rng.Int63n(max-min) + min + } + + var tInitial time.Time + + series := []*metric_values{} + for i, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) { + if i == 0 { + tInitial = t + } + tSeconds := t.Unix() + tSecondsInt32 := int32(tSeconds) + tSecondsFloat32 := float32(tSeconds) + tMilliseconds := tSeconds * 1e3 + tMillisecondsFloat := float64(tMilliseconds) + first := metric_values{ + Time: t, + TimeInt64: tMilliseconds, + TimeInt64Nullable: &(tMilliseconds), + TimeFloat64: tMillisecondsFloat, + TimeFloat64Nullable: &tMillisecondsFloat, + TimeInt32: tSecondsInt32, + TimeInt32Nullable: &tSecondsInt32, + TimeFloat32: tSecondsFloat32, + TimeFloat32Nullable: &tSecondsFloat32, + Measurement: "Metric A", + ValueOne: rnd(0, 100), + ValueTwo: rnd(0, 100), + } + second := first + second.Measurement = "Metric B" + second.ValueOne = rnd(0, 100) + second.ValueTwo = rnd(0, 100) + + series = append(series, &first) + series = append(series, &second) + } + + // _, err = session.InsertMulti(series) + for _, m := range series { + _, err := p.Exec(context.Background(), `INSERT INTO "metric_values" ( + time, + "timeInt64", "timeInt64Nullable", + "timeFloat64", "timeFloat64Nullable", + "timeInt32", "timeInt32Nullable", + "timeFloat32", "timeFloat32Nullable", + measurement, "valueOne", "valueTwo" + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, + m.Time, + m.TimeInt64, m.TimeInt64Nullable, + m.TimeFloat64, m.TimeFloat64Nullable, + m.TimeInt32, m.TimeInt32Nullable, + m.TimeFloat32, m.TimeFloat32Nullable, + m.Measurement, m.ValueOne, m.ValueTwo, + ) + require.NoError(t, err) + } + + t.Run( + "When doing a metric query using epoch (int64) as time column and value column (int64) should return metric with time in time.Time", + func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT \"timeInt64\" as time, \"timeInt64\" FROM metric_values ORDER BY time LIMIT 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Len(t, frames, 1) + require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) + }) + + t.Run("When doing a metric query using epoch (int64 nullable) as time column and value column (int64 nullable,) should return metric with time in time.Time", + func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT \"timeInt64Nullable\" as time, \"timeInt64Nullable\" FROM metric_values ORDER BY time LIMIT 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Len(t, frames, 1) + require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) + }) + + t.Run("When doing a metric query using epoch (float64) as time column and value column (float64), should return metric with time in time.Time", + func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT \"timeFloat64\" as time, \"timeFloat64\" FROM metric_values ORDER BY time LIMIT 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Len(t, frames, 1) + require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) + }) + + t.Run("When doing a metric query using epoch (float64 nullable) as time column and value column (float64 nullable), should return metric with time in time.Time", + func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT \"timeFloat64Nullable\" as time, \"timeFloat64Nullable\" FROM metric_values ORDER BY time LIMIT 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) + }) + + t.Run("When doing a metric query using epoch (int32) as time column and value column (int32), should return metric with time in time.Time", + func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT \"timeInt32\" as time, \"timeInt32\" FROM metric_values ORDER BY time LIMIT 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) + }) + + t.Run("When doing a metric query using epoch (int32 nullable) as time column and value column (int32 nullable), should return metric with time in time.Time", + func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT \"timeInt32Nullable\" as time, \"timeInt32Nullable\" FROM metric_values ORDER BY time LIMIT 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) + }) + + t.Run("When doing a metric query using epoch (float32) as time column and value column (float32), should return metric with time in time.Time", + func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT \"timeFloat32\" as time, \"timeFloat32\" FROM metric_values ORDER BY time LIMIT 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + aTime := time.Unix(0, int64(float64(float32(tInitial.Unix()))*1e3)*int64(time.Millisecond)) + require.True(t, aTime.Equal(*frames[0].Fields[0].At(0).(*time.Time))) + }) + + t.Run("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable), should return metric with time in time.Time", + func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT \"timeFloat32Nullable\" as time, \"timeFloat32Nullable\" FROM metric_values ORDER BY time LIMIT 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + aTime := time.Unix(0, int64(float64(float32(tInitial.Unix()))*1e3)*int64(time.Millisecond)) + require.True(t, aTime.Equal(*frames[0].Fields[0].At(0).(*time.Time))) + }) + + t.Run("When doing a metric query grouping by time and select metric column should return correct series", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values ORDER BY 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 3, len(frames[0].Fields)) + require.Equal(t, "Metric A - value one", frames[0].Fields[1].Name) + require.Equal(t, "Metric B - value one", frames[0].Fields[2].Name) + }) + + t.Run("When doing a metric query with metric column and multiple value columns", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, \"valueOne\", \"valueTwo\" FROM metric_values ORDER BY 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.NoError(t, err) + require.Equal(t, 1, len(frames)) + require.Equal(t, 5, len(frames[0].Fields)) + require.Equal(t, "valueOne", frames[0].Fields[1].Name) + require.Equal(t, data.Labels{"metric": "Metric A"}, frames[0].Fields[1].Labels) + require.Equal(t, "valueOne", frames[0].Fields[2].Name) + require.Equal(t, data.Labels{"metric": "Metric B"}, frames[0].Fields[2].Labels) + require.Equal(t, "valueTwo", frames[0].Fields[3].Name) + require.Equal(t, data.Labels{"metric": "Metric A"}, frames[0].Fields[3].Labels) + require.Equal(t, "valueTwo", frames[0].Fields[4].Name) + require.Equal(t, data.Labels{"metric": "Metric B"}, frames[0].Fields[4].Labels) + }) + + t.Run("When doing a metric query grouping by time should return correct series", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values ORDER BY 1", + "format": "time_series" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 3, len(frames[0].Fields)) + require.Equal(t, "valueOne", frames[0].Fields[1].Name) + require.Equal(t, "valueTwo", frames[0].Fields[2].Name) + }) + + t.Run("When doing a query with timeFrom,timeTo,unixEpochFrom,unixEpochTo macros", func(t *testing.T) { + fakeInterpolate := sqleng.Interpolate + t.Cleanup(func() { + sqleng.Interpolate = fakeInterpolate + }) + sqleng.Interpolate = origInterpolate + + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT time FROM metric_values WHERE time > $__timeFrom() OR time < $__timeFrom() OR 1 < $__unixEpochFrom() OR $__unixEpochTo() > 1 ORDER BY 1", + "format": "time_series" + }`), + RefID: "A", + TimeRange: backend.TimeRange{ + From: fromStart.Add(-5 * time.Minute), + To: fromStart, + }, + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + frames := queryResult.Frames + require.Len(t, frames, 1) + require.Equal(t, + "SELECT time FROM metric_values WHERE time > '2018-03-15T12:55:00Z' OR time < '2018-03-15T12:55:00Z' OR 1 < 1521118500 OR 1521118800 > 1 ORDER BY 1", + frames[0].Meta.ExecutedQueryString) + }) + }) + + t.Run("Given a table with event data", func(t *testing.T) { + type event struct { + TimeSec int64 + Description string + Tags string + } + + _, err := p.Exec(context.Background(), "DROP TABLE IF EXISTS event") + require.NoError(t, err) + _, err = p.Exec(context.Background(), `CREATE TABLE event (time_sec BIGINT NULL, description VARCHAR(255) NULL, tags VARCHAR(255) NULL)`) + require.NoError(t, err) + + events := []*event{} + for _, t := range genTimeRangeByInterval(fromStart.Add(-20*time.Minute), time.Hour, 25*time.Minute) { + events = append(events, &event{ + TimeSec: t.Unix(), + Description: "Someone deployed something", + Tags: "deploy", + }) + events = append(events, &event{ + TimeSec: t.Add(5 * time.Minute).Unix(), + Description: "New support ticket registered", + Tags: "ticket", + }) + } + + for _, e := range events { + _, err := p.Exec(context.Background(), "INSERT INTO event (time_sec, description, tags) VALUES ($1, $2, $3)", e.TimeSec, e.Description, e.Tags) + require.NoError(t, err) + } + + t.Run("When doing an annotation query of deploy events should return expected result", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT \"time_sec\" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC", + "format": "table" + }`), + RefID: "Deploys", + TimeRange: backend.TimeRange{ + From: fromStart.Add(-20 * time.Minute), + To: fromStart.Add(40 * time.Minute), + }, + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + + queryResult := resp.Responses["Deploys"] + + frames := queryResult.Frames + require.Len(t, frames, 1) + require.Len(t, frames[0].Fields, 3) + }) + + t.Run("When doing an annotation query of ticket events should return expected result", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT \"time_sec\" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC", + "format": "table" + }`), + RefID: "Tickets", + TimeRange: backend.TimeRange{ + From: fromStart.Add(-20 * time.Minute), + To: fromStart.Add(40 * time.Minute), + }, + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + + queryResult := resp.Responses["Tickets"] + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 3, len(frames[0].Fields)) + }) + + t.Run("When doing an annotation query with a time column in datetime format", func(t *testing.T) { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + dtFormat := "2006-01-02 15:04:05.999999999" + + queryjson := fmt.Sprintf("{\"rawSql\": \"SELECT CAST('%s' AS TIMESTAMP) as time, 'message' as text, 'tag1,tag2' as tags\", \"format\": \"table\" }", dt.Format(dtFormat)) + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(queryjson), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 3, len(frames[0].Fields)) + + // Should be in time.Time + require.Equal(t, dt.Unix(), (*frames[0].Fields[0].At(0).(*time.Time)).Unix()) + }) + + t.Run("When doing an annotation query with a time column in epoch second format should return time.Time", func(t *testing.T) { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + queryjson := fmt.Sprintf("{\"rawSql\": \"SELECT %d as time, 'message' as text, 'tag1,tag2' as tags\", \"format\": \"table\"}", dt.Unix()) + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(queryjson), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 3, len(frames[0].Fields)) + + // Should be in time.Time + require.Equal(t, dt.Unix(), (*frames[0].Fields[0].At(0).(*time.Time)).Unix()) + }) + + t.Run("When doing an annotation query with a time column in epoch second format (t *testing.Tint) should return time.Time", func(t *testing.T) { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + queryjson := fmt.Sprintf("{\"rawSql\": \"SELECT cast(%d as bigint) as time, 'message' as text, 'tag1,tag2' as tags\", \"format\": \"table\"}", dt.Unix()) + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(queryjson), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 3, len(frames[0].Fields)) + + // Should be in time.Time + require.Equal(t, dt.Unix(), (*frames[0].Fields[0].At(0).(*time.Time)).Unix()) + }) + + t.Run("When doing an annotation query with a time column in epoch millisecond format should return time.Time", func(t *testing.T) { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + queryjson := fmt.Sprintf("{\"rawSql\":\"SELECT %d as time, 'message' as text, 'tag1,tag2' as tags\", \"format\": \"table\"}", dt.Unix()*1000) + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(queryjson), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 3, len(frames[0].Fields)) + + // Should be in time.Time + require.Equal(t, dt.Unix(), (*frames[0].Fields[0].At(0).(*time.Time)).Unix()) + }) + + t.Run("When doing an annotation query with a time column holding a bigint null value should return nil", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT cast(null as bigint) as time, 'message' as text, 'tag1,tag2' as tags", + "format": "table" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 3, len(frames[0].Fields)) + + // Should be in time.Time + require.Nil(t, frames[0].Fields[0].At(0)) + }) + + t.Run("When doing an annotation query with a time column holding a timestamp null value should return nil", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT cast(null as timestamp) as time, 'message' as text, 'tag1,tag2' as tags", + "format": "table" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 3, len(frames[0].Fields)) + + // Should be in time.Time + assert.Nil(t, frames[0].Fields[0].At(0)) + }) + + t.Run("When doing an annotation query with a time and timeend column should return two fields of type time", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT 1631053772276 as time, 1631054012276 as timeend, '' as text, '' as tags", + "format": "table" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Equal(t, 1, len(frames)) + require.Equal(t, 4, len(frames[0].Fields)) + + require.Equal(t, data.FieldTypeNullableTime, frames[0].Fields[0].Type()) + require.Equal(t, data.FieldTypeNullableTime, frames[0].Fields[1].Type()) + }) + + t.Run("When row limit set to 1", func(t *testing.T) { + jsonData := sqleng.JsonData{ + MaxOpenConns: 10, + MaxIdleConns: 2, + ConnMaxLifetime: 14400, + Timescaledb: false, + Mode: "disable", + ConfigurationMethod: "file-path", + } + + dsInfo := sqleng.DataSourceInfo{ + JsonData: jsonData, + DecryptedSecureJSONData: map[string]string{}, + } + _, handler, err := newPostgresPGX(context.Background(), "error", 1, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + + require.NoError(t, err) + + t.Run("When doing a table query that returns 2 rows should limit the result to 1 row", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT 1 as value UNION ALL select 2 as value", + "format": "table" + }`), + RefID: "A", + TimeRange: backend.TimeRange{ + From: time.Now(), + To: time.Now(), + }, + }, + }, + } + + resp, err := handler.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + frames := queryResult.Frames + require.NoError(t, err) + require.Equal(t, 1, len(frames)) + require.Equal(t, 1, len(frames[0].Fields)) + require.Equal(t, 1, frames[0].Rows()) + require.Len(t, frames[0].Meta.Notices, 1) + require.Equal(t, data.NoticeSeverityWarning, frames[0].Meta.Notices[0].Severity) + }) + + t.Run("When doing a time series query that returns 2 rows should limit the result to 1 row", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT 1 as time, 1 as value UNION ALL select 2 as time, 2 as value", + "format": "time_series" + }`), + RefID: "A", + TimeRange: backend.TimeRange{ + From: time.Now(), + To: time.Now(), + }, + }, + }, + } + + resp, err := handler.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + frames := queryResult.Frames + require.NoError(t, err) + require.Equal(t, 1, len(frames)) + require.Equal(t, 2, len(frames[0].Fields)) + require.Equal(t, 1, frames[0].Rows()) + require.Len(t, frames[0].Meta.Notices, 1) + require.Equal(t, data.NoticeSeverityWarning, frames[0].Meta.Notices[0].Severity) + }) + }) + }) + + t.Run("Given an empty table", func(t *testing.T) { + _, err := p.Exec(context.Background(), "DROP TABLE IF EXISTS empty_obj") + require.NoError(t, err) + _, err = p.Exec(context.Background(), "CREATE TABLE empty_obj (empty_key VARCHAR(255) NULL, empty_val BIGINT NULL)") + require.NoError(t, err) + + t.Run("When no rows are returned, should return an empty frame", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT empty_key, empty_val FROM empty_obj", + "format": "table" + }`), + RefID: "A", + TimeRange: backend.TimeRange{ + From: time.Now(), + To: time.Now().Add(1 * time.Minute), + }, + }, + }, + } + + resp, err := exe.QueryDataPGX(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + + frames := queryResult.Frames + require.Len(t, frames, 1) + require.Equal(t, 0, frames[0].Rows()) + require.NotNil(t, frames[0].Fields) + require.Empty(t, frames[0].Fields) + }) + }) +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go index 47eee7b1a6b..2197270cb98 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go @@ -159,7 +159,7 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { UID: tt.uid, } - connStr, err := svc.generateConnectionString(ds) + connStr, err := svc.generateConnectionString(ds, tt.tlsSettings, false) if tt.expErr == "" { require.NoError(t, err, tt.desc) diff --git a/pkg/tsdb/grafana-postgresql-datasource/proxy.go b/pkg/tsdb/grafana-postgresql-datasource/proxy.go index d06d8b68152..dca66df38e9 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/proxy.go +++ b/pkg/tsdb/grafana-postgresql-datasource/proxy.go @@ -33,3 +33,11 @@ func (p *postgresProxyDialer) DialTimeout(network, address string, timeout time. return p.d.(proxy.ContextDialer).DialContext(ctx, network, address) } + +type PgxDialFunc = func(ctx context.Context, network string, address string) (net.Conn, error) + +func newPgxDialFunc(dialer proxy.Dialer) PgxDialFunc { + return func(ctx context.Context, network string, addr string) (net.Conn, error) { + return dialer.Dial(network, addr) + } +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go index 25b4ee0fdf8..99d36aaaa85 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go +++ b/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go @@ -10,11 +10,17 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/lib/pq" ) -func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - err := e.Ping() +func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest, features featuremgmt.FeatureToggles) (*backend.CheckHealthResult, error) { + var err error + if features.IsEnabled(ctx, featuremgmt.FlagPostgresDSUsePGX) { + err = e.PingPGX(ctx) + } else { + err = e.Ping() + } if err != nil { logCheckHealthError(ctx, e.dsInfo, err) if strings.EqualFold(req.PluginContext.User.Role, "Admin") { @@ -63,6 +69,7 @@ func ErrToHealthCheckResult(err error) (*backend.CheckHealthResult, error) { res.Message += fmt.Sprintf(". Error message: %s", errMessage) } } + if errors.Is(err, pq.ErrSSLNotSupported) { res.Message = "SSL error: Failed to connect to the server" } @@ -125,10 +132,10 @@ func logCheckHealthError(ctx context.Context, dsInfo DataSourceInfo, err error) "config_tls_client_cert_length": len(dsInfo.DecryptedSecureJSONData["tlsClientCert"]), "config_tls_client_key_length": len(dsInfo.DecryptedSecureJSONData["tlsClientKey"]), } - configSummaryJson, marshalError := json.Marshal(configSummary) + configSummaryJSON, marshalError := json.Marshal(configSummary) if marshalError != nil { logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error") return } - logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error_detailed", "details", string(configSummaryJson)) + logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error_detailed", "details", string(configSummaryJSON)) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go index 7712fe7c3d5..8f354497689 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go +++ b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" + "github.com/jackc/pgx/v5/pgxpool" ) // MetaKeyExecutedQueryString is the key where the executed query should get stored @@ -88,6 +89,7 @@ type DataSourceHandler struct { dsInfo DataSourceInfo rowLimit int64 userError string + pool *pgxpool.Pool } type QueryJson struct { @@ -489,6 +491,7 @@ type dataQueryModel struct { Interval time.Duration columnNames []string columnTypes []*sql.ColumnType + columnTypesPGX []string timeIndex int timeEndIndex int metricIndex int diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_pgx.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_pgx.go new file mode 100644 index 00000000000..df182a3119f --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_pgx.go @@ -0,0 +1,554 @@ +package sqleng + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "runtime/debug" + "strings" + "sync" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" +) + +func NewQueryDataHandlerPGX(userFacingDefaultError string, p *pgxpool.Pool, config DataPluginConfiguration, queryResultTransformer SqlQueryResultTransformer, + macroEngine SQLMacroEngine, log log.Logger) (*DataSourceHandler, error) { + queryDataHandler := DataSourceHandler{ + queryResultTransformer: queryResultTransformer, + macroEngine: macroEngine, + timeColumnNames: []string{"time"}, + log: log, + dsInfo: config.DSInfo, + rowLimit: config.RowLimit, + userError: userFacingDefaultError, + } + + if len(config.TimeColumnNames) > 0 { + queryDataHandler.timeColumnNames = config.TimeColumnNames + } + + if len(config.MetricColumnTypes) > 0 { + queryDataHandler.metricColumnTypes = config.MetricColumnTypes + } + + queryDataHandler.pool = p + return &queryDataHandler, nil +} + +func (e *DataSourceHandler) DisposePGX() { + e.log.Debug("Disposing DB...") + + if e.pool != nil { + e.pool.Close() + } + + e.log.Debug("DB disposed") +} + +func (e *DataSourceHandler) PingPGX(ctx context.Context) error { + return e.pool.Ping(ctx) +} + +func (e *DataSourceHandler) QueryDataPGX(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + result := backend.NewQueryDataResponse() + ch := make(chan DBDataResponse, len(req.Queries)) + var wg sync.WaitGroup + // Execute each query in a goroutine and wait for them to finish afterwards + for _, query := range req.Queries { + queryjson := QueryJson{ + Fill: false, + Format: "time_series", + } + err := json.Unmarshal(query.JSON, &queryjson) + if err != nil { + return nil, fmt.Errorf("error unmarshal query json: %w", err) + } + + // the fill-params are only stored inside this function, during query-interpolation. we do not support + // sending them in "from the outside" + if queryjson.Fill || queryjson.FillInterval != 0.0 || queryjson.FillMode != "" || queryjson.FillValue != 0.0 { + return nil, fmt.Errorf("query fill-parameters not supported") + } + + if queryjson.RawSql == "" { + continue + } + + wg.Add(1) + go e.executeQueryPGX(ctx, query, &wg, ch, queryjson) + } + + wg.Wait() + + // Read results from channels + close(ch) + result.Responses = make(map[string]backend.DataResponse) + for queryResult := range ch { + result.Responses[queryResult.refID] = queryResult.dataResponse + } + + return result, nil +} + +func (e *DataSourceHandler) handleQueryError(frameErr string, err error, query string, source backend.ErrorSource, ch chan DBDataResponse, queryResult DBDataResponse) { + var emptyFrame data.Frame + emptyFrame.SetMeta(&data.FrameMeta{ExecutedQueryString: query}) + if backend.IsDownstreamError(err) { + source = backend.ErrorSourceDownstream + } + queryResult.dataResponse.Error = fmt.Errorf("%s: %w", frameErr, err) + queryResult.dataResponse.ErrorSource = source + queryResult.dataResponse.Frames = data.Frames{&emptyFrame} + ch <- queryResult +} + +func (e *DataSourceHandler) handlePanic(logger log.Logger, queryResult *DBDataResponse, ch chan DBDataResponse) { + if r := recover(); r != nil { + logger.Error("ExecuteQuery panic", "error", r, "stack", string(debug.Stack())) + if theErr, ok := r.(error); ok { + queryResult.dataResponse.Error = theErr + queryResult.dataResponse.ErrorSource = backend.ErrorSourcePlugin + } else if theErrString, ok := r.(string); ok { + queryResult.dataResponse.Error = errors.New(theErrString) + queryResult.dataResponse.ErrorSource = backend.ErrorSourcePlugin + } else { + queryResult.dataResponse.Error = fmt.Errorf("unexpected error - %s", e.userError) + queryResult.dataResponse.ErrorSource = backend.ErrorSourceDownstream + } + ch <- *queryResult + } +} + +func (e *DataSourceHandler) execQuery(ctx context.Context, query string, logger log.Logger) ([]*pgconn.Result, error) { + c, err := e.pool.Acquire(ctx) + if err != nil { + return nil, fmt.Errorf("failed to acquire connection: %w", err) + } + defer c.Release() + + mrr := c.Conn().PgConn().Exec(ctx, query) + defer func() { + if err := mrr.Close(); err != nil { + logger.Warn("Failed to close multi-result reader", "error", err) + } + }() + return mrr.ReadAll() +} + +func (e *DataSourceHandler) executeQueryPGX(queryContext context.Context, query backend.DataQuery, wg *sync.WaitGroup, + ch chan DBDataResponse, queryJSON QueryJson) { + defer wg.Done() + queryResult := DBDataResponse{ + dataResponse: backend.DataResponse{}, + refID: query.RefID, + } + + logger := e.log.FromContext(queryContext) + defer e.handlePanic(logger, &queryResult, ch) + + if queryJSON.RawSql == "" { + panic("Query model property rawSql should not be empty at this point") + } + + // global substitutions + interpolatedQuery := Interpolate(query, query.TimeRange, e.dsInfo.JsonData.TimeInterval, queryJSON.RawSql) + + // data source specific substitutions + interpolatedQuery, err := e.macroEngine.Interpolate(&query, query.TimeRange, interpolatedQuery) + if err != nil { + e.handleQueryError("interpolation failed", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourcePlugin, ch, queryResult) + return + } + + results, err := e.execQuery(queryContext, interpolatedQuery, logger) + if err != nil { + e.handleQueryError("db query error", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourcePlugin, ch, queryResult) + return + } + + qm, err := e.newProcessCfgPGX(queryContext, query, results, interpolatedQuery) + if err != nil { + e.handleQueryError("failed to get configurations", err, interpolatedQuery, backend.ErrorSourcePlugin, ch, queryResult) + return + } + + frame, err := convertResultsToFrame(results, e.rowLimit) + if err != nil { + e.handleQueryError("convert frame from rows error", err, interpolatedQuery, backend.ErrorSourcePlugin, ch, queryResult) + return + } + + e.processFrame(frame, qm, queryResult, ch, logger) +} + +func (e *DataSourceHandler) processFrame(frame *data.Frame, qm *dataQueryModel, queryResult DBDataResponse, ch chan DBDataResponse, logger log.Logger) { + if frame.Meta == nil { + frame.Meta = &data.FrameMeta{} + } + frame.Meta.ExecutedQueryString = qm.InterpolatedQuery + + // If no rows were returned, clear any previously set `Fields` with a single empty `data.Field` slice. + // Then assign `queryResult.dataResponse.Frames` the current single frame with that single empty Field. + // This assures 1) our visualization doesn't display unwanted empty fields, and also that 2) + // additionally-needed frame data stays intact and is correctly passed to our visulization. + if frame.Rows() == 0 { + frame.Fields = []*data.Field{} + queryResult.dataResponse.Frames = data.Frames{frame} + ch <- queryResult + return + } + + if err := convertSQLTimeColumnsToEpochMS(frame, qm); err != nil { + e.handleQueryError("converting time columns failed", err, qm.InterpolatedQuery, backend.ErrorSourcePlugin, ch, queryResult) + return + } + + if qm.Format == dataQueryFormatSeries { + // time series has to have time column + if qm.timeIndex == -1 { + e.handleQueryError("db has no time column", errors.New("time column is missing; make sure your data includes a time column for time series format or switch to a table format that doesn't require it"), qm.InterpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) + return + } + + // Make sure to name the time field 'Time' to be backward compatible with Grafana pre-v8. + frame.Fields[qm.timeIndex].Name = data.TimeSeriesTimeFieldName + + for i := range qm.columnNames { + if i == qm.timeIndex || i == qm.metricIndex { + continue + } + + if t := frame.Fields[i].Type(); t == data.FieldTypeString || t == data.FieldTypeNullableString { + continue + } + + var err error + if frame, err = convertSQLValueColumnToFloat(frame, i); err != nil { + e.handleQueryError("convert value to float failed", err, qm.InterpolatedQuery, backend.ErrorSourcePlugin, ch, queryResult) + return + } + } + + tsSchema := frame.TimeSeriesSchema() + if tsSchema.Type == data.TimeSeriesTypeLong { + var err error + originalData := frame + frame, err = data.LongToWide(frame, qm.FillMissing) + if err != nil { + e.handleQueryError("failed to convert long to wide series when converting from dataframe", err, qm.InterpolatedQuery, backend.ErrorSourcePlugin, ch, queryResult) + return + } + + // Before 8x, a special metric column was used to name time series. The LongToWide transforms that into a metric label on the value field. + // But that makes series name have both the value column name AND the metric name. So here we are removing the metric label here and moving it to the + // field name to get the same naming for the series as pre v8 + if len(originalData.Fields) == 3 { + for _, field := range frame.Fields { + if len(field.Labels) == 1 { // 7x only supported one label + name, ok := field.Labels["metric"] + if ok { + field.Name = name + field.Labels = nil + } + } + } + } + } + if qm.FillMissing != nil { + // we align the start-time + startUnixTime := qm.TimeRange.From.Unix() / int64(qm.Interval.Seconds()) * int64(qm.Interval.Seconds()) + alignedTimeRange := backend.TimeRange{ + From: time.Unix(startUnixTime, 0), + To: qm.TimeRange.To, + } + + var err error + frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) + if err != nil { + logger.Error("Failed to resample dataframe", "err", err) + frame.AppendNotices(data.Notice{Text: "Failed to resample dataframe", Severity: data.NoticeSeverityWarning}) + return + } + } + } + + queryResult.dataResponse.Frames = data.Frames{frame} + ch <- queryResult +} + +func (e *DataSourceHandler) newProcessCfgPGX(queryContext context.Context, query backend.DataQuery, + results []*pgconn.Result, interpolatedQuery string) (*dataQueryModel, error) { + columnNames := []string{} + columnTypesPGX := []string{} + + // The results will contain column information in the metadata + for _, result := range results { + // Get column names from the result metadata + for _, field := range result.FieldDescriptions { + columnNames = append(columnNames, field.Name) + pqtype, ok := pgtype.NewMap().TypeForOID(field.DataTypeOID) + if !ok { + // Handle special cases for field types + switch field.DataTypeOID { + case pgtype.TimetzOID: + columnTypesPGX = append(columnTypesPGX, "timetz") + case 790: + columnTypesPGX = append(columnTypesPGX, "money") + default: + return nil, fmt.Errorf("unknown data type oid: %d", field.DataTypeOID) + } + } else { + columnTypesPGX = append(columnTypesPGX, pqtype.Name) + } + } + } + + qm := &dataQueryModel{ + columnTypesPGX: columnTypesPGX, + columnNames: columnNames, + timeIndex: -1, + timeEndIndex: -1, + metricIndex: -1, + metricPrefix: false, + queryContext: queryContext, + } + + queryJSON := QueryJson{} + err := json.Unmarshal(query.JSON, &queryJSON) + if err != nil { + return nil, err + } + + if queryJSON.Fill { + qm.FillMissing = &data.FillMissing{} + qm.Interval = time.Duration(queryJSON.FillInterval * float64(time.Second)) + switch strings.ToLower(queryJSON.FillMode) { + case "null": + qm.FillMissing.Mode = data.FillModeNull + case "previous": + qm.FillMissing.Mode = data.FillModePrevious + case "value": + qm.FillMissing.Mode = data.FillModeValue + qm.FillMissing.Value = queryJSON.FillValue + default: + } + } + + qm.TimeRange.From = query.TimeRange.From.UTC() + qm.TimeRange.To = query.TimeRange.To.UTC() + + switch queryJSON.Format { + case "time_series": + qm.Format = dataQueryFormatSeries + case "table": + qm.Format = dataQueryFormatTable + default: + panic(fmt.Sprintf("Unrecognized query model format: %q", queryJSON.Format)) + } + + for i, col := range qm.columnNames { + for _, tc := range e.timeColumnNames { + if col == tc { + qm.timeIndex = i + break + } + } + + if qm.Format == dataQueryFormatTable && strings.EqualFold(col, "timeend") { + qm.timeEndIndex = i + continue + } + + switch col { + case "metric": + qm.metricIndex = i + default: + if qm.metricIndex == -1 { + columnType := qm.columnTypesPGX[i] + for _, mct := range e.metricColumnTypes { + if columnType == mct { + qm.metricIndex = i + continue + } + } + } + } + } + qm.InterpolatedQuery = interpolatedQuery + return qm, nil +} + +func convertResultsToFrame(results []*pgconn.Result, rowLimit int64) (*data.Frame, error) { + frame := data.Frame{} + m := pgtype.NewMap() + + for _, result := range results { + // Skip non-select statements + if !result.CommandTag.Select() { + continue + } + fields := make(data.Fields, len(result.FieldDescriptions)) + + fieldTypes, err := getFieldTypesFromDescriptions(result.FieldDescriptions, m) + if err != nil { + return nil, err + } + + for i, v := range result.FieldDescriptions { + fields[i] = data.NewFieldFromFieldType(fieldTypes[i], 0) + fields[i].Name = v.Name + } + // Create a new frame + frame = *data.NewFrame("", fields...) + } + + // Add rows to the frame + for _, result := range results { + // Skip non-select statements + if !result.CommandTag.Select() { + continue + } + fieldDescriptions := result.FieldDescriptions + for rowIdx := range result.Rows { + if rowIdx == int(rowLimit) { + frame.AppendNotices(data.Notice{ + Severity: data.NoticeSeverityWarning, + Text: fmt.Sprintf("Results have been limited to %v because the SQL row limit was reached", rowLimit), + }) + break + } + row := make([]interface{}, len(fieldDescriptions)) + for colIdx, fd := range fieldDescriptions { + rawValue := result.Rows[rowIdx][colIdx] + dataTypeOID := fd.DataTypeOID + format := fd.Format + + if rawValue == nil { + row[colIdx] = nil + continue + } + + // Convert based on type + switch fd.DataTypeOID { + case pgtype.Int2OID: + var d *int16 + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + row[colIdx] = d + case pgtype.Int4OID: + var d *int32 + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + row[colIdx] = d + case pgtype.Int8OID: + var d *int64 + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + row[colIdx] = d + case pgtype.NumericOID, pgtype.Float8OID, pgtype.Float4OID: + var d *float64 + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + row[colIdx] = d + case pgtype.BoolOID: + var d *bool + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + row[colIdx] = d + case pgtype.ByteaOID: + d, err := pgtype.ByteaCodec.DecodeValue(pgtype.ByteaCodec{}, m, dataTypeOID, format, rawValue) + if err != nil { + return nil, err + } + str := string(d.([]byte)) + row[colIdx] = &str + case pgtype.TimestampOID, pgtype.TimestamptzOID, pgtype.DateOID: + var d *time.Time + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + row[colIdx] = d + case pgtype.TimeOID, pgtype.TimetzOID: + var d *string + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + row[colIdx] = d + default: + var d *string + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + row[colIdx] = d + } + } + frame.AppendRow(row...) + } + } + + return &frame, nil +} + +func getFieldTypesFromDescriptions(fieldDescriptions []pgconn.FieldDescription, m *pgtype.Map) ([]data.FieldType, error) { + fieldTypes := make([]data.FieldType, len(fieldDescriptions)) + for i, v := range fieldDescriptions { + typeName, ok := m.TypeForOID(v.DataTypeOID) + if !ok { + // Handle special cases for field types + if v.DataTypeOID == pgtype.TimetzOID || v.DataTypeOID == 790 { + fieldTypes[i] = data.FieldTypeNullableString + } else { + return nil, fmt.Errorf("unknown data type oid: %d", v.DataTypeOID) + } + } else { + switch typeName.Name { + case "int2": + fieldTypes[i] = data.FieldTypeNullableInt16 + case "int4": + fieldTypes[i] = data.FieldTypeNullableInt32 + case "int8": + fieldTypes[i] = data.FieldTypeNullableInt64 + case "float4", "float8", "numeric": + fieldTypes[i] = data.FieldTypeNullableFloat64 + case "bool": + fieldTypes[i] = data.FieldTypeNullableBool + case "timestamptz", "timestamp", "date": + fieldTypes[i] = data.FieldTypeNullableTime + case "json", "jsonb": + fieldTypes[i] = data.FieldTypeNullableJSON + default: + fieldTypes[i] = data.FieldTypeNullableString + } + } + } + return fieldTypes, nil +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.golden.jsonc b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.golden.jsonc new file mode 100644 index 00000000000..5754a588b10 --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.golden.jsonc @@ -0,0 +1,214 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "executedQueryString": "SELECT * FROM tbl" +// } +// Name: +// Dimensions: 12 Fields by 2 Rows +// +--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-------------------------------+-------------------------------+-----------------+-----------------+--------------------+--------------------+-----------------+-----------------+ +// | Name: ts | Name: tsnn | Name: tsz | Name: tsznn | Name: d | Name: dnn | Name: t | Name: tnn | Name: tz | Name: tznn | Name: i | Name: inn | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*string | Type: []*string | Type: []*string | Type: []*string | Type: []*string | Type: []*string | +// +--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-------------------------------+-------------------------------+-----------------+-----------------+--------------------+--------------------+-----------------+-----------------+ +// | 2023-11-15 05:06:07.123456 +0000 UTC | 2023-11-15 05:06:08.123456 +0000 UTC | 2021-07-22 11:22:33.654321 +0000 +0000 | 2021-07-22 11:22:34.654321 +0000 +0000 | 2023-12-20 00:00:00 +0000 UTC | 2023-12-21 00:00:00 +0000 UTC | 12:34:56.234567 | 12:34:57.234567 | 23:12:36.765432+01 | 23:12:37.765432+01 | 00:00:00.987654 | 00:00:00.887654 | +// | null | 2023-11-15 05:06:09.123456 +0000 UTC | null | 2021-07-22 11:22:35.654321 +0000 +0000 | null | 2023-12-22 00:00:00 +0000 UTC | null | 12:34:58.234567 | null | 23:12:38.765432+01 | null | 00:00:00.787654 | +// +--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-------------------------------+-------------------------------+-----------------+-----------------+--------------------+--------------------+-----------------+-----------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "meta": { + "typeVersion": [ + 0, + 0 + ], + "executedQueryString": "SELECT * FROM tbl" + }, + "fields": [ + { + "name": "ts", + "type": "time", + "typeInfo": { + "frame": "time.Time", + "nullable": true + } + }, + { + "name": "tsnn", + "type": "time", + "typeInfo": { + "frame": "time.Time", + "nullable": true + } + }, + { + "name": "tsz", + "type": "time", + "typeInfo": { + "frame": "time.Time", + "nullable": true + } + }, + { + "name": "tsznn", + "type": "time", + "typeInfo": { + "frame": "time.Time", + "nullable": true + } + }, + { + "name": "d", + "type": "time", + "typeInfo": { + "frame": "time.Time", + "nullable": true + } + }, + { + "name": "dnn", + "type": "time", + "typeInfo": { + "frame": "time.Time", + "nullable": true + } + }, + { + "name": "t", + "type": "string", + "typeInfo": { + "frame": "string", + "nullable": true + } + }, + { + "name": "tnn", + "type": "string", + "typeInfo": { + "frame": "string", + "nullable": true + } + }, + { + "name": "tz", + "type": "string", + "typeInfo": { + "frame": "string", + "nullable": true + } + }, + { + "name": "tznn", + "type": "string", + "typeInfo": { + "frame": "string", + "nullable": true + } + }, + { + "name": "i", + "type": "string", + "typeInfo": { + "frame": "string", + "nullable": true + } + }, + { + "name": "inn", + "type": "string", + "typeInfo": { + "frame": "string", + "nullable": true + } + } + ] + }, + "data": { + "values": [ + [ + 1700024767123, + null + ], + [ + 1700024768123, + 1700024769123 + ], + [ + 1626952953654, + null + ], + [ + 1626952954654, + 1626952955654 + ], + [ + 1703030400000, + null + ], + [ + 1703116800000, + 1703203200000 + ], + [ + "12:34:56.234567", + null + ], + [ + "12:34:57.234567", + "12:34:58.234567" + ], + [ + "23:12:36.765432+01", + null + ], + [ + "23:12:37.765432+01", + "23:12:38.765432+01" + ], + [ + "00:00:00.987654", + null + ], + [ + "00:00:00.887654", + "00:00:00.787654" + ] + ], + "nanos": [ + [ + 456000, + 0 + ], + [ + 456000, + 456000 + ], + [ + 321000, + 0 + ], + [ + 321000, + 321000 + ], + null, + null, + null, + null, + null, + null, + null, + null + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.sql b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.sql new file mode 100644 index 00000000000..d81b3ca235b --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.sql @@ -0,0 +1,44 @@ +-- SELECT * FROM tbl +-- test all date/time-based postgres data types +CREATE TEMPORARY TABLE tbl ( + ts timestamp, + tsnn timestamp NOT NULL, + tsz timestamp with time zone, + tsznn timestamp with time zone NOT NULL, + d date, + dnn date NOT NULL, + t time, + tnn time NOT NULL, + tz time with time zone, + tznn time with time zone NOT NULL, + i interval, + inn interval NOT NULL +); + +INSERT INTO tbl (ts, tsnn, tsz, tsznn, d, dnn, t, tnn, tz, tznn, i, inn) VALUES ( +'2023-11-15 05:06:07.123456', +'2023-11-15 05:06:08.123456', +'2021-07-22 13:22:33.654321 Europe/Berlin', +'2021-07-22 13:22:34.654321 Europe/Berlin', +'2023-12-20', +'2023-12-21', +'12:34:56.234567', +'12:34:57.234567', +'23:12:36.765432+1', +'23:12:37.765432+1', +'987654 microsecond', +'887654 microsecond' +), ( +NULL, +'2023-11-15 05:06:09.123456', +NULL, +'2021-07-22 13:22:35.654321 Europe/Berlin', +NULL, +'2023-12-22', +NULL, +'12:34:58.234567', +NULL, +'23:12:38.765432+1', +NULL, +'787654 microsecond' +); \ No newline at end of file diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx index 389110e9517..273c60df162 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx @@ -10,7 +10,14 @@ import { } from '@grafana/data'; import { ConfigSection, ConfigSubSection, DataSourceDescription, EditorStack } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; -import { ConnectionLimits, Divider, TLSSecretsConfig, useMigrateDatabaseFields } from '@grafana/sql'; +import { + ConnectionLimits, + Divider, + MaxLifetimeField, + MaxOpenConnectionsField, + TLSSecretsConfig, + useMigrateDatabaseFields, +} from '@grafana/sql'; import { Input, Select, @@ -76,6 +83,14 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps

    { + updateDatasourcePluginJsonDataOption(props, 'maxOpenConns', number); + }; + + const onMaxLifetimeChanged = (number?: number) => { + updateDatasourcePluginJsonDataOption(props, 'connMaxLifetime', number); + }; + const onTimeScaleDBChanged = (event: SyntheticEvent) => { updateDatasourcePluginJsonDataOption(props, 'timescaledb', event.currentTarget.checked); }; @@ -153,11 +168,7 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps

    - - - - @@ -184,6 +195,7 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps

    + {options.jsonData.sslmode !== PostgresTLSModes.disable ? ( ) : null} - {jsonData.sslmode !== PostgresTLSModes.disable ? ( - <> + + + {jsonData.sslmode !== PostgresTLSModes.disable ? ( + <> + + {jsonData.tlsConfigurationMethod === PostgresTLSMethods.fileContent ? ( )} - - ) : null} - + + + ) : null} @@ -396,8 +412,18 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps

    - - + {config.featureToggles.postgresDSUsePGX ? ( + + + + + ) : ( + + )} {config.secureSocksDSProxyEnabled && ( )} From 599fe5a40015850e239382aad57e51691286871e Mon Sep 17 00:00:00 2001 From: Jason Marshall Date: Wed, 30 Jul 2025 05:49:17 -0700 Subject: [PATCH 043/126] Remove deprecated links tags in favor of depends_on (#106680) These prevent grafana from working with podman. --- .../blocks/auth/authentik/docker-compose.yaml | 10 ++-------- .../blocks/auth/jwt_proxy/docker-compose.yaml | 4 +--- .../docker/blocks/auth/oauth/docker-compose.yaml | 2 -- .../docker/blocks/collectd/docker-compose.yaml | 2 +- .../docker/blocks/influxdb/docker-compose.yaml | 2 -- .../docker/blocks/influxdb1/docker-compose.yaml | 4 ++-- devenv/docker/blocks/tempo/docker-compose.yaml | 16 ++++++++-------- devenv/docker/blocks/traefik/docker-compose.yml | 2 +- 8 files changed, 15 insertions(+), 27 deletions(-) diff --git a/devenv/docker/blocks/auth/authentik/docker-compose.yaml b/devenv/docker/blocks/auth/authentik/docker-compose.yaml index 5457ed51460..21b70ad7376 100644 --- a/devenv/docker/blocks/auth/authentik/docker-compose.yaml +++ b/devenv/docker/blocks/auth/authentik/docker-compose.yaml @@ -35,8 +35,8 @@ AUTHENTIK_HOST: http://authentik:9000 AUTHENTIK_INSECURE: "true" AUTHENTIK_TOKEN: 77vDKwFWqCAsD9ykbH6vsGuMHtloM6urfeglrR9KRhWFcABtwmGGiQWHQStw - links: - - "authentik:authentik" + depends_on: + - "authentik" authentik: image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.6} @@ -61,9 +61,6 @@ - ./certs:/certs - ./custom-templates:/templates - ./geoip:/geoip - links: - - "authentikdb:authentikdb" - - "authentikredis:authentikredis" authentik-worker: image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.6} @@ -86,6 +83,3 @@ - ./certs:/certs - ./custom-templates:/templates - ./geoip:/geoip - links: - - "authentikdb:authentikdb" - - "authentikredis:authentikredis" diff --git a/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml b/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml index 552664f1c97..de6fedd097f 100644 --- a/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml +++ b/devenv/docker/blocks/auth/jwt_proxy/docker-compose.yaml @@ -25,8 +25,6 @@ - 8087:8080 depends_on: - oauthkeycloakdb - links: - - "oauthkeycloakdb:oauthkeycloakdb" restart: unless-stopped oauthproxy: @@ -54,4 +52,4 @@ - "env.grafana.local:host-gateway" ports: - 8088:8088 - restart: unless-stopped \ No newline at end of file + restart: unless-stopped diff --git a/devenv/docker/blocks/auth/oauth/docker-compose.yaml b/devenv/docker/blocks/auth/oauth/docker-compose.yaml index ba88c5d9e8f..a9ac08fd57e 100644 --- a/devenv/docker/blocks/auth/oauth/docker-compose.yaml +++ b/devenv/docker/blocks/auth/oauth/docker-compose.yaml @@ -25,6 +25,4 @@ - 8087:8080 depends_on: - oauthkeycloakdb - links: - - "oauthkeycloakdb:oauthkeycloakdb" restart: unless-stopped diff --git a/devenv/docker/blocks/collectd/docker-compose.yaml b/devenv/docker/blocks/collectd/docker-compose.yaml index c5e189b58d8..8a263eb6c54 100644 --- a/devenv/docker/blocks/collectd/docker-compose.yaml +++ b/devenv/docker/blocks/collectd/docker-compose.yaml @@ -7,5 +7,5 @@ GRAPHITE_PREFIX: collectd. REPORT_BY_CPU: 'false' COLLECT_INTERVAL: 10 - links: + depends_on: - graphite diff --git a/devenv/docker/blocks/influxdb/docker-compose.yaml b/devenv/docker/blocks/influxdb/docker-compose.yaml index 244bb472107..0a50a257e7f 100644 --- a/devenv/docker/blocks/influxdb/docker-compose.yaml +++ b/devenv/docker/blocks/influxdb/docker-compose.yaml @@ -17,8 +17,6 @@ telegraf: image: telegraf:latest - links: - - influxdb depends_on: - influxdb volumes: diff --git a/devenv/docker/blocks/influxdb1/docker-compose.yaml b/devenv/docker/blocks/influxdb1/docker-compose.yaml index 100c3b4c80a..66743f2a1a5 100644 --- a/devenv/docker/blocks/influxdb1/docker-compose.yaml +++ b/devenv/docker/blocks/influxdb1/docker-compose.yaml @@ -14,7 +14,7 @@ fake-influxdb1-data: image: grafana/fake-data-gen - links: + depends_on: - influxdb1 environment: FD_SERVER: influxdb1 @@ -23,7 +23,7 @@ telegraf-influxdb1: image: telegraf:latest - links: + depends_on: - influxdb1 volumes: - ./docker/blocks/influxdb1/telegraf.conf:/etc/telegraf/telegraf.conf:ro diff --git a/devenv/docker/blocks/tempo/docker-compose.yaml b/devenv/docker/blocks/tempo/docker-compose.yaml index bdac352a523..89ef5c43ae3 100644 --- a/devenv/docker/blocks/tempo/docker-compose.yaml +++ b/devenv/docker/blocks/tempo/docker-compose.yaml @@ -11,7 +11,7 @@ JAEGER_SAMPLER_PARAM: 1 labels: namespace: tns - logging: + logging: driver: loki options: loki-url: 'http://localhost:3100/api/prom/push' @@ -27,7 +27,7 @@ command: - '-log.level=debug' - 'http://db' - links: + depends_on: - db ports: - 0.0.0.0:8001:80 @@ -38,7 +38,7 @@ JAEGER_SAMPLER_PARAM: 1 labels: namespace: tns - logging: + logging: driver: loki options: loki-url: 'http://localhost:3100/api/prom/push' @@ -54,7 +54,7 @@ command: - '-log.level=debug' - 'http://app' - links: + depends_on: - app ports: - 0.0.0.0:8002:80 @@ -65,7 +65,7 @@ JAEGER_SAMPLER_PARAM: 1 labels: namespace: tns - logging: + logging: driver: loki options: loki-url: 'http://localhost:3100/api/prom/push' @@ -93,7 +93,7 @@ - --enable-feature=native-histograms volumes: - ./docker/blocks/tempo/prometheus.yaml:/etc/prometheus.yaml - links: + depends_on: - app - db - loadgen @@ -101,7 +101,7 @@ - "9090:9090" labels: namespace: monitoring - logging: + logging: driver: loki options: loki-url: 'http://localhost:3100/api/prom/push' @@ -117,7 +117,7 @@ - "3100:3100" labels: namespace: monitoring - logging: + logging: driver: loki options: loki-url: 'http://localhost:3100/api/prom/push' diff --git a/devenv/docker/blocks/traefik/docker-compose.yml b/devenv/docker/blocks/traefik/docker-compose.yml index e62805a37f5..b4908e36a2b 100644 --- a/devenv/docker/blocks/traefik/docker-compose.yml +++ b/devenv/docker/blocks/traefik/docker-compose.yml @@ -9,7 +9,7 @@ services: ports: - '80:80' - '8080:8080' - links: + depends_on: - grafana-subpath grafana-subpath: From f92752c2f00e2bc17ed1b2fe30f7bb9de256e0ed Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 30 Jul 2025 13:59:32 +0100 Subject: [PATCH 044/126] Chore: Harden panel-layouts test by wrapping synchronous checks in `.toPass()` (#108849) harden panel layouts test by wrapping synchronous checks in .toPass() --- .../dashboards-panel-layouts.spec.ts | 87 ++++++++++++------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts index 2cdb847a03f..d91e5ba59f2 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts @@ -69,11 +69,14 @@ test.describe( // Get initial positions - standard width should have panels on different rows const firstPanelTop = await getPanelTop(dashboardPage, selectors); const lastPanel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).last(); - const lastPanelBox = await lastPanel.boundingBox(); - const lastPanelTop = lastPanelBox?.y || 0; - // Verify standard layout has panels on different rows - expect(lastPanelTop).toBeGreaterThan(firstPanelTop); + expect(async () => { + const lastPanelBox = await lastPanel.boundingBox(); + const lastPanelTop = lastPanelBox?.y || 0; + + // Verify standard layout has panels on different rows + expect(lastPanelTop).toBeGreaterThan(firstPanelTop); + }).toPass(); // Change to narrow min column width await dashboardPage @@ -82,11 +85,13 @@ test.describe( await page.getByRole('option', { name: 'Narrow' }).click(); // Verify narrow layout has all panels on same row - const firstPanelTopNarrow = await getPanelTop(dashboardPage, selectors); - const lastPanelBoxNarrow = await lastPanel.boundingBox(); - const lastPanelTopNarrow = lastPanelBoxNarrow?.y || 0; + expect(async () => { + const firstPanelTopNarrow = await getPanelTop(dashboardPage, selectors); + const lastPanelBoxNarrow = await lastPanel.boundingBox(); + const lastPanelTopNarrow = lastPanelBoxNarrow?.y || 0; - expect(lastPanelTopNarrow).toBe(firstPanelTopNarrow); + expect(lastPanelTopNarrow).toBe(firstPanelTopNarrow); + }).toPass(); await saveDashboard(dashboardPage, selectors); await page.reload(); @@ -99,11 +104,13 @@ test.describe( ) ).toHaveValue('Narrow'); - const firstPanelTopReload = await getPanelTop(dashboardPage, selectors); - const lastPanelBoxReload = await lastPanel.boundingBox(); - const lastPanelTopReload = lastPanelBoxReload?.y || 0; + expect(async () => { + const firstPanelTopReload = await getPanelTop(dashboardPage, selectors); + const lastPanelBoxReload = await lastPanel.boundingBox(); + const lastPanelTopReload = lastPanelBoxReload?.y || 0; - expect(lastPanelTopReload).toBe(firstPanelTopReload); + expect(lastPanelTopReload).toBe(firstPanelTopReload); + }).toPass(); }); test('can change to custom min column width in auto grid layout', async ({ dashboardPage, selectors, page }) => { @@ -220,22 +227,28 @@ test.describe( .click(); await page.getByRole('option', { name: 'Short' }).click(); - const shortHeight = await getPanelHeight(dashboardPage, selectors); - expect(shortHeight).toBeLessThan(regularRowHeight); + await expect(async () => { + const shortHeight = await getPanelHeight(dashboardPage, selectors); + expect(shortHeight).toBeLessThan(regularRowHeight); + }).toPass(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) .click(); await page.getByRole('option', { name: 'Tall' }).click(); - const tallHeight = await getPanelHeight(dashboardPage, selectors); - expect(tallHeight).toBeGreaterThan(regularRowHeight); + await expect(async () => { + const tallHeight = await getPanelHeight(dashboardPage, selectors); + expect(tallHeight).toBeGreaterThan(regularRowHeight); + }).toPass(); await saveDashboard(dashboardPage, selectors); await page.reload(); - const tallHeightAfterReload = await getPanelHeight(dashboardPage, selectors); - expect(tallHeightAfterReload).toBeGreaterThan(regularRowHeight); + await expect(async () => { + const tallHeightAfterReload = await getPanelHeight(dashboardPage, selectors); + expect(tallHeightAfterReload).toBeGreaterThan(regularRowHeight); + }).toPass(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); @@ -243,8 +256,10 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) ).toHaveValue('Tall'); - const tallHeightAfterEdit = await getPanelHeight(dashboardPage, selectors); - expect(tallHeightAfterEdit).toBeGreaterThan(regularRowHeight); + await expect(async () => { + const tallHeightAfterEdit = await getPanelHeight(dashboardPage, selectors); + expect(tallHeightAfterEdit).toBeGreaterThan(regularRowHeight); + }).toPass(); }); test('can change to custom row height in auto grid layout', async ({ dashboardPage, selectors, page }) => { @@ -276,15 +291,19 @@ test.describe( .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight) .blur(); - const customHeight = await getPanelHeight(dashboardPage, selectors); - expect(customHeight).toBeCloseTo(800, 5); // Allow some tolerance for rendering differences - expect(customHeight).toBeGreaterThan(regularRowHeight); + await expect(async () => { + const customHeight = await getPanelHeight(dashboardPage, selectors); + expect(customHeight).toBeCloseTo(800, 5); // Allow some tolerance for rendering differences + expect(customHeight).toBeGreaterThan(regularRowHeight); + }).toPass(); await saveDashboard(dashboardPage, selectors); await page.reload(); - const customHeightAfterReload = await getPanelHeight(dashboardPage, selectors); - expect(customHeightAfterReload).toBeCloseTo(800, 5); + await expect(async () => { + const customHeightAfterReload = await getPanelHeight(dashboardPage, selectors); + expect(customHeightAfterReload).toBeCloseTo(800, 5); + }).toPass(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); @@ -329,14 +348,18 @@ test.describe( .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen) .click({ force: true }); - const fillScreenHeight = await getPanelHeight(dashboardPage, selectors); - expect(fillScreenHeight).toBeGreaterThan(initialHeight); + await expect(async () => { + const fillScreenHeight = await getPanelHeight(dashboardPage, selectors); + expect(fillScreenHeight).toBeGreaterThan(initialHeight); + }).toPass(); await saveDashboard(dashboardPage, selectors); await page.reload(); - const fillScreenHeightAfterReload = await getPanelHeight(dashboardPage, selectors); - expect(fillScreenHeightAfterReload).toBeGreaterThan(initialHeight); + await expect(async () => { + const fillScreenHeightAfterReload = await getPanelHeight(dashboardPage, selectors); + expect(fillScreenHeightAfterReload).toBeGreaterThan(initialHeight); + }).toPass(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); @@ -344,8 +367,10 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen) ).toBeChecked(); - const fillScreenHeightAfterEdit = await getPanelHeight(dashboardPage, selectors); - expect(fillScreenHeightAfterEdit).toBeGreaterThan(initialHeight); + await expect(async () => { + const fillScreenHeightAfterEdit = await getPanelHeight(dashboardPage, selectors); + expect(fillScreenHeightAfterEdit).toBeGreaterThan(initialHeight); + }).toPass(); }); } ); From 20b08ccaab817bf8724a4c3643d88760511c3221 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 30 Jul 2025 15:01:27 +0200 Subject: [PATCH 045/126] Add `v2beta1` api version: Consolidate schema breaking changes (#108172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "Revert: Future-proofing query and data source model in Dashboard Sche… (#107985)" This reverts commit 13a89d4ae3f1f4ccdb98edc9b42f3c35522d2dfa. * Revert "Revert "Schema V2: Simplify annotations v1<->v2 conversions" (#107984)" This reverts commit 2b8c5bea1a6e5a54d761ce3e7bc7f5a1c529f60e. * make gen apps * e2e update * Use v2alpha2 by default (#108177) * Use v2alpha2 by default * Apply only DS changes to alpha2 * Use v2alpha2 by default except to query * Create a v2 index in @grafana/schema * Update path and apply lint * Update tests * Update imports to v2 status * Fix failing openapi test * Schemav2 breaking changes: conversion implementation (#108224) * provision v2alpha1 dashboard * Run conversions for DS refactor * Run snapshot testing on conversions * Normalize output name * Update snapshots to include all panel and variable cases * fix lint * fix lint * fix test and go lint * more go lint --------- Co-authored-by: Ivan Ortega Co-authored-by: Haris Rozajac * Schema v2: Introduce group/datasource convention to GroupBy and AdHoc variable (#108237) * Schema v2: Introduce group/datasource convention to GroupBy and AdHoc variables * add conversion * App Installer: Authorizer support (#108419) * Chore: use `satisfies` and remove a load of `any`s (#108397) use satisfies and remove a load of anys * improve logging and fail unified-storage migration with more than 0 errors (#108471) improve logging and fail unified-storage migration with more than 0 errors * fix conversion test * Secrets: Create more granular fixed roles for SecureValues (#108382) * Provisioning: Fix bug in job progress recording (#108440) Fix bug in job progress recording * Provisioning: Fix ImportAllPanelsFromLocalRepository test (#108441) * Provisioning: Skip flaky test * Fix flaky provisioning test * Fix lint --------- Co-authored-by: Roberto Jimenez Sanchez * BulkDeleteProvisionedResource: Move progress bar into a second step (#108417) * Move progress bar into a second step --------- Co-authored-by: Alex Khomenko * [Dashboard Schema Codegen] Move dashboard CUE codegen block back up into kind body (#108476) [Dashboard Schema Codegen] Move dashboard CUE codegen block back up into kind body to make sure new versions have the same settings. --------- Co-authored-by: Haris Rozajac Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com> Co-authored-by: Ashley Harrison Co-authored-by: Will Assis <35489495+gassiss@users.noreply.github.com> Co-authored-by: Matheus Macabu Co-authored-by: Roberto Jiménez Sánchez Co-authored-by: Roberto Jimenez Sanchez Co-authored-by: Yunwen Zheng Co-authored-by: Alex Khomenko Co-authored-by: Austin Pond Co-authored-by: Ivan Ortega * Dashboard Schema V2: Refactor VizConfigKind to follow DataQueryKind convention (#108148) * Dashboards API: Register v2alpha2 API * Prepare conversion functions * Fix test * Refactor VizConfigKind to follow DataQueryKind convention * fix tests * use new dataquerykind convention alpha 2 * add conversion * fix tests * fix tests * fix another test * Fix merge --------- Co-authored-by: Dominik Prokop * fix k8s codegen * Update e2e-playwright/dashboards/TestV2Dashboard.json * Update e2e/dashboards/TestV2Dashboard.json * revert app generation for non-related apps * try again * another try * also revert folder and secret app generation * v2alpha1 provisioned dashboard * Fix kind * Fix conversion snapshots * Update API discovery registry * Rename to v2beta1 * Rename migrations * Update apps/dashboard/pkg/apis/dashboard/v2beta1/doc.go Co-authored-by: Stephanie Hingtgen * Ensure conditional rendering and other non changed properties --------- Co-authored-by: Ivan Ortega Co-authored-by: Haris Rozajac Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com> Co-authored-by: Ashley Harrison Co-authored-by: Will Assis <35489495+gassiss@users.noreply.github.com> Co-authored-by: Matheus Macabu Co-authored-by: Roberto Jiménez Sánchez Co-authored-by: Roberto Jimenez Sanchez Co-authored-by: Yunwen Zheng Co-authored-by: Alex Khomenko Co-authored-by: Austin Pond Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Co-authored-by: Stephanie Hingtgen --- apps/dashboard/Makefile | 12 +- apps/dashboard/kinds/dashboard.cue | 6 +- .../{v2alpha2 => v2beta1}/dashboard_spec.cue | 45 +- .../v2alpha2/zz_generated.conversion.go | 175 --- ...enerated.openapi_violation_exceptions.list | 67 - .../{v2alpha2 => v2beta1}/constants.go | 4 +- .../dashboard_codec_gen.go | 2 +- .../dashboard_metadata_gen.go | 2 +- .../dashboard_object_gen.go | 2 +- .../dashboard_schema_gen.go | 4 +- .../{v2alpha2 => v2beta1}/dashboard_spec.cue | 47 +- .../dashboard_spec_gen.go | 163 ++- .../dashboard_status_gen.go | 2 +- .../dashboard/{v2alpha2 => v2beta1}/doc.go | 2 +- .../{v2alpha2 => v2beta1}/register.go | 4 +- .../dashboard/{v2alpha2 => v2beta1}/types.go | 2 +- .../{v2alpha2 => v2beta1}/validation.go | 2 +- .../v2beta1/zz_generated.conversion.go | 175 +++ .../zz_generated.deepcopy.go | 2 +- .../zz_generated.defaults.go | 2 +- .../zz_generated.openapi.go | 1110 +++++++++-------- ...enerated.openapi_violation_exceptions.list | 67 + apps/dashboard/pkg/apis/dashboard_manifest.go | 6 +- .../pkg/migration/conversion/conversion.go | 28 +- .../migration/conversion/conversion_test.go | 155 ++- .../testdata/input/v2alpha1.complete.json | 462 +++++++ .../input/v2alpha1.ds-data-query.json | 1001 +++++++++++++++ .../input/v2alpha1.groupby-adhoc-vars.json | 73 ++ .../testdata/input/v2alpha1.viz-config.json | 202 +++ .../output/v2alpha1.complete.v0alpha1.json | 20 + .../output/v2alpha1.complete.v1beta1.json | 20 + .../output/v2alpha1.complete.v2beta1.json | 511 ++++++++ .../v2alpha1.ds-data-query.v0alpha1.json | 14 + .../v2alpha1.ds-data-query.v1beta1.json | 14 + .../v2alpha1.ds-data-query.v2beta1.json | 1094 ++++++++++++++++ .../v2alpha1.groupby-adhoc-vars.v0alpha1.json | 14 + .../v2alpha1.groupby-adhoc-vars.v1beta1.json | 14 + .../v2alpha1.groupby-adhoc-vars.v2beta1.json | 100 ++ .../output/v2alpha1.viz-config.v0alpha1.json | 14 + .../output/v2alpha1.viz-config.v1beta1.json | 14 + .../output/v2alpha1.viz-config.v2beta1.json | 260 ++-- apps/dashboard/pkg/migration/conversion/v0.go | 10 +- apps/dashboard/pkg/migration/conversion/v1.go | 10 +- apps/dashboard/pkg/migration/conversion/v2.go | 43 +- .../conversion/v2alpha1_to_v2beta1.go | 981 +++++++++++++++ .../sample/dashboard-v1-annotations.json | 257 ++++ .../sample/dashboard-v1beta1.json | 257 ++++ .../sample/dashboard-v2alpha1.json | 1025 +++++++++++++++ .../dashboard-duplicate-panel.spec.ts | 2 + .../dashboard-group-panels.spec.ts | 2 + .../dashboards-panel-layouts.spec.ts | 2 + .../dashboards/TestV2Dashboard.json | 69 +- e2e/dashboards/TestV2Dashboard.json | 69 +- e2e/utils/flows/scenes/importDashboard.ts | 4 + go.work.sum | 4 - .../grafana-schema/src/schema/dashboard/v2.ts | 3 + .../src/schema/dashboard/v2_examples.ts | 89 +- .../dashboard_object_gen.ts | 0 .../types.metadata.gen.ts | 0 .../{v2alpha2 => v2beta1}/types.spec.gen.ts | 56 +- .../{v2alpha2 => v2beta1}/types.status.gen.ts | 0 pkg/registry/apis/dashboard/large.go | 8 +- pkg/registry/apis/dashboard/large_test.go | 30 +- pkg/registry/apis/dashboard/mutate.go | 10 +- pkg/registry/apis/dashboard/mutation_test.go | 10 +- pkg/registry/apis/dashboard/register.go | 20 +- pkg/registry/apis/dashboard/register_test.go | 8 +- .../apis/dashboard/schema_validation.go | 8 +- .../provisioning/jobs/export/resources.go | 4 +- .../jobs/export/resources_test.go | 14 +- .../apis/provisioning/resources/client.go | 4 +- pkg/services/authz/zanzana/server/server.go | 6 +- pkg/tests/apis/dashboard/dashboards_test.go | 8 +- .../integration/api_validation_test.go | 8 +- .../dashboard-test-v2alpha2.yaml | 10 - .../dashboard-test-v2beta1.yaml | 10 + pkg/tests/apis/provisioning/helper_test.go | 10 +- .../apis/provisioning/provisioning_test.go | 8 +- .../Select/DashboardPicker.test.tsx | 4 +- .../api/browseDashboardsAPI.ts | 2 +- .../ConditionalRendering.tsx | 2 +- .../ConditionalRenderingData.tsx | 2 +- .../ConditionalRenderingGroup.tsx | 2 +- .../ConditionalRenderingTimeRangeSize.tsx | 2 +- .../ConditionalRenderingVariable.tsx | 2 +- .../conditional-rendering/types.ts | 2 +- .../DashboardScenePageStateManager.test.ts | 107 +- .../pages/DashboardScenePageStateManager.ts | 10 +- .../saving/DetectChangesWorker.ts | 2 +- .../saving/getDashboardChanges.ts | 2 +- .../dashboard-scene/saving/shared.tsx | 2 +- .../saving/useSaveDashboard.ts | 2 +- .../dashboard-scene/scene/DashboardScene.tsx | 4 +- .../scene/export/exporters.test.ts | 29 +- .../dashboard-scene/scene/export/exporters.ts | 12 +- .../dashboard-scene/scene/export/utils.ts | 2 +- .../AutoGridLayoutManager.tsx | 2 +- .../DefaultGridLayoutManager.tsx | 2 +- .../scene/layout-rows/RowItem.tsx | 2 +- .../scene/layout-rows/RowsLayoutManager.tsx | 2 +- .../scene/layout-tabs/TabItem.tsx | 2 +- .../scene/layout-tabs/TabsLayoutManager.tsx | 2 +- .../scene/layouts-shared/paste.ts | 2 +- .../scene/types/DashboardLayoutManager.ts | 2 +- .../DashboardSceneSerializer.test.ts | 291 ++++- .../serialization/DashboardSceneSerializer.ts | 10 +- ...sformSceneToSaveModelSchemaV2.test.ts.snap | 68 +- .../serialization/annotations.test.ts | 226 ++++ .../serialization/annotations.ts | 115 ++ .../buildNewDashboardSaveModel.ts | 15 +- .../AutoGridLayoutSerializer.ts | 2 +- .../DefaultGridLayoutSerializer.ts | 2 +- .../RowsLayoutSerializer.test.ts | 2 +- .../layoutSerializers/RowsLayoutSerializer.ts | 5 +- .../TabsLayoutSerializer.test.ts | 2 +- .../layoutSerializers/TabsLayoutSerializer.ts | 5 +- .../layoutSerializerRegistry.ts | 2 +- .../layoutSerializers/utils.test.ts | 64 +- .../serialization/layoutSerializers/utils.ts | 51 +- .../sceneVariablesSetToVariables.test.ts | 163 +-- .../sceneVariablesSetToVariables.ts | 46 +- .../transformSaveModelSchemaV2ToScene.test.ts | 102 +- .../transformSaveModelSchemaV2ToScene.ts | 74 +- .../transformSceneToSaveModelSchemaV2.test.ts | 87 +- .../transformSceneToSaveModelSchemaV2.ts | 119 +- .../serialization/transformToV1TypesUtils.ts | 2 +- .../transformToV2TypesUtils.test.ts | 2 +- .../serialization/transformToV2TypesUtils.ts | 2 +- .../settings/JsonModelEditView.tsx | 2 +- .../sharing/ExportButton/ResourceExport.tsx | 2 +- .../sharing/ShareExportTab.test.tsx | 10 +- .../sharing/ShareExportTab.tsx | 2 +- .../v2schema/ImportDashboardFormV2.tsx | 2 +- .../v2schema/ImportDashboardOverviewV2.tsx | 41 +- .../dashboard-scene/v2schema/test-helpers.ts | 12 +- .../v2schema/transformers.test.ts | 2 +- .../api/ResponseTransformers.test.ts | 87 +- .../dashboard/api/ResponseTransformers.ts | 103 +- .../dashboard/api/UnifiedDashboardAPI.test.ts | 16 +- .../dashboard/api/UnifiedDashboardAPI.ts | 12 +- .../features/dashboard/api/dashboard_api.ts | 2 +- public/app/features/dashboard/api/types.ts | 4 +- public/app/features/dashboard/api/utils.ts | 8 +- public/app/features/dashboard/api/v1.test.ts | 4 +- public/app/features/dashboard/api/v1.ts | 5 +- public/app/features/dashboard/api/v2.test.ts | 12 +- public/app/features/dashboard/api/v2.ts | 6 +- .../dashboard/services/DashboardLoaderSrv.ts | 2 +- .../app/features/dashboard/utils/tracking.ts | 2 +- .../manage-dashboards/state/actions.test.ts | 27 +- .../manage-dashboards/state/actions.ts | 59 +- .../utils/validation.test.ts | 4 +- 152 files changed, 9087 insertions(+), 1974 deletions(-) rename apps/dashboard/kinds/{v2alpha2 => v2beta1}/dashboard_spec.cue (97%) delete mode 100644 apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.conversion.go delete mode 100644 apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi_violation_exceptions.list rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/constants.go (91%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/dashboard_codec_gen.go (97%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/dashboard_metadata_gen.go (98%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/dashboard_object_gen.go (99%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/dashboard_schema_gen.go (89%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/dashboard_spec.cue (97%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/dashboard_spec_gen.go (95%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/dashboard_status_gen.go (98%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/doc.go (80%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/register.go (98%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/types.go (99%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/validation.go (99%) create mode 100644 apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.conversion.go rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/zz_generated.deepcopy.go (99%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/zz_generated.defaults.go (96%) rename apps/dashboard/pkg/apis/dashboard/{v2alpha2 => v2beta1}/zz_generated.openapi.go (73%) create mode 100644 apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi_violation_exceptions.list create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.complete.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.ds-data-query.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.groupby-adhoc-vars.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.viz-config.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v2beta1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json rename conf/provisioning/sample/dashboard-v2.json => apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json (54%) create mode 100644 apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go create mode 100644 conf/provisioning/sample/dashboard-v1-annotations.json create mode 100644 conf/provisioning/sample/dashboard-v1beta1.json create mode 100644 conf/provisioning/sample/dashboard-v2alpha1.json create mode 100644 packages/grafana-schema/src/schema/dashboard/v2.ts rename packages/grafana-schema/src/schema/dashboard/{v2alpha2 => v2beta1}/dashboard_object_gen.ts (100%) rename packages/grafana-schema/src/schema/dashboard/{v2alpha2 => v2beta1}/types.metadata.gen.ts (100%) rename packages/grafana-schema/src/schema/dashboard/{v2alpha2 => v2beta1}/types.spec.gen.ts (98%) rename packages/grafana-schema/src/schema/dashboard/{v2alpha2 => v2beta1}/types.status.gen.ts (100%) delete mode 100644 pkg/tests/apis/provisioning/exportunifiedtorepository/dashboard-test-v2alpha2.yaml create mode 100644 pkg/tests/apis/provisioning/exportunifiedtorepository/dashboard-test-v2beta1.yaml create mode 100644 public/app/features/dashboard-scene/serialization/annotations.test.ts create mode 100644 public/app/features/dashboard-scene/serialization/annotations.ts diff --git a/apps/dashboard/Makefile b/apps/dashboard/Makefile index c3905410aed..fa75513d964 100644 --- a/apps/dashboard/Makefile +++ b/apps/dashboard/Makefile @@ -44,9 +44,9 @@ post-generate-cleanup: ## Clean up the generated code @echo "" >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @cat ./kinds/v2alpha1/dashboard_spec.cue >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue - # Copy dashboard/v2alpha2 spec so we can use it for schema validation - @echo "// This file is managed by grafana-app-sdk - DO NOT EDIT MANUALLY" > ./pkg/apis/dashboard/v2alpha2/dashboard_spec.cue - @echo "// Source: apps/dashboard/kinds/v2alpha2/dashboard_spec.cue" >> ./pkg/apis/dashboard/v2alpha2/dashboard_spec.cue - @echo "// To sync changes, run: make generate in apps/dashboard" >> ./pkg/apis/dashboard/v2alpha2/dashboard_spec.cue - @echo "" >> ./pkg/apis/dashboard/v2alpha2/dashboard_spec.cue - @cat ./kinds/v2alpha2/dashboard_spec.cue >> ./pkg/apis/dashboard/v2alpha2/dashboard_spec.cue + # Copy dashboard/v2beta1 spec so we can use it for schema validation + @echo "// This file is managed by grafana-app-sdk - DO NOT EDIT MANUALLY" > ./pkg/apis/dashboard/v2beta1/dashboard_spec.cue + @echo "// Source: apps/dashboard/kinds/v2beta1/dashboard_spec.cue" >> ./pkg/apis/dashboard/v2beta1/dashboard_spec.cue + @echo "// To sync changes, run: make generate in apps/dashboard" >> ./pkg/apis/dashboard/v2beta1/dashboard_spec.cue + @echo "" >> ./pkg/apis/dashboard/v2beta1/dashboard_spec.cue + @cat ./kinds/v2beta1/dashboard_spec.cue >> ./pkg/apis/dashboard/v2beta1/dashboard_spec.cue diff --git a/apps/dashboard/kinds/dashboard.cue b/apps/dashboard/kinds/dashboard.cue index 7c68295d394..490f6173700 100644 --- a/apps/dashboard/kinds/dashboard.cue +++ b/apps/dashboard/kinds/dashboard.cue @@ -4,7 +4,7 @@ import ( v0 "github.com/grafana/grafana/sdkkinds/dashboard/v0alpha1" v1 "github.com/grafana/grafana/sdkkinds/dashboard/v1beta1" v2alpha1 "github.com/grafana/grafana/sdkkinds/dashboard/v2alpha1" - v2alpha2 "github.com/grafana/grafana/sdkkinds/dashboard/v2alpha2" + v2beta1 "github.com/grafana/grafana/sdkkinds/dashboard/v2beta1" ) // Status is the shared status of all dashboard versions. @@ -67,9 +67,9 @@ dashboard: { status: DashboardStatus } } - "v2alpha2": { + "v2beta1": { schema: { - spec: v2alpha2.DashboardSpec + spec: v2beta1.DashboardSpec status: DashboardStatus } } diff --git a/apps/dashboard/kinds/v2alpha2/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue similarity index 97% rename from apps/dashboard/kinds/v2alpha2/dashboard_spec.cue rename to apps/dashboard/kinds/v2beta1/dashboard_spec.cue index 4c1202caea7..cf71e6b2dc4 100644 --- a/apps/dashboard/kinds/v2alpha2/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue @@ -1,4 +1,4 @@ -package v2alpha2 +package v2beta1 DashboardSpec: { annotations: [...AnnotationQueryKind] | *[] @@ -111,14 +111,6 @@ DashboardLink: { keepTime: bool | *false } -DataSourceRef: { - // The plugin type-id - type?: string - - // Specific datasource instance - uid?: string -} - // A topic is attached to DataFrame metadata in query results. // This specifies where the data should be used. DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames="Series|Annotations|AlertStates") @@ -373,27 +365,27 @@ Kind: { // --- Kinds --- VizConfigSpec: { - pluginVersion: string options: [string]: _ fieldConfig: FieldConfigSource } VizConfigKind: { - // The kind of a VizConfigKind is the plugin ID - kind: string + kind: "VizConfig" + // The group is the plugin ID + group: string + version: string spec: VizConfigSpec } AnnotationQuerySpec: { - datasource?: DataSourceRef - query?: DataQueryKind + query: DataQueryKind enable: bool hide: bool iconColor: string name: string builtIn?: bool | *false filter?: AnnotationPanelFilter - legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties + legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling. } AnnotationQueryKind: { @@ -412,15 +404,19 @@ QueryOptionsSpec: { } DataQueryKind: { - // The kind of a DataQueryKind is the datasource type - kind: string + kind: "DataQuery" + group: string + version: string | *"v0" + // New type for datasource reference + // Not creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS. + datasource?: { + name?: string + } spec: [string]: _ } PanelQuerySpec: { query: DataQueryKind - datasource?: DataSourceRef - refId: string hidden: bool } @@ -723,7 +719,6 @@ QueryVariableSpec: { refresh: VariableRefresh skipUrlSync: bool | *false description?: string - datasource?: DataSourceRef query: DataQueryKind regex: string | *"" sort: VariableSort @@ -861,7 +856,6 @@ CustomVariableKind: { // GroupBy variable specification GroupByVariableSpec: { name: string | *"" - datasource?: DataSourceRef defaultValue?: VariableOption current: VariableOption | *{ text: "" @@ -878,13 +872,16 @@ GroupByVariableSpec: { // Group variable kind GroupByVariableKind: { kind: "GroupByVariable" + group: string + datasource?: { + name?: string + } spec: GroupByVariableSpec } // Adhoc variable specification AdhocVariableSpec: { name: string | *"" - datasource?: DataSourceRef baseFilters: [...AdHocFilterWithLabels] | *[] filters: [...AdHocFilterWithLabels] | *[] defaultKeys: [...MetricFindValue] | *[] @@ -920,6 +917,10 @@ AdHocFilterWithLabels: { // Adhoc variable kind AdhocVariableKind: { kind: "AdhocVariable" + group: string + datasource?: { + name?: string + } spec: AdhocVariableSpec } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.conversion.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.conversion.go deleted file mode 100644 index 3159ff2e0f7..00000000000 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.conversion.go +++ /dev/null @@ -1,175 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by conversion-gen. DO NOT EDIT. - -package v2alpha2 - -import ( - url "net/url" - unsafe "unsafe" - - dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" - conversion "k8s.io/apimachinery/pkg/conversion" - runtime "k8s.io/apimachinery/pkg/runtime" -) - -func init() { - localSchemeBuilder.Register(RegisterConversions) -} - -// RegisterConversions adds conversion functions to the given scheme. -// Public to allow building arbitrary schemes. -func RegisterConversions(s *runtime.Scheme) error { - if err := s.AddGeneratedConversionFunc((*AnnotationActions)(nil), (*dashboard.AnnotationActions)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(a.(*AnnotationActions), b.(*dashboard.AnnotationActions), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*dashboard.AnnotationActions)(nil), (*AnnotationActions)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(a.(*dashboard.AnnotationActions), b.(*AnnotationActions), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*AnnotationPermission)(nil), (*dashboard.AnnotationPermission)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v2alpha2_AnnotationPermission_To_dashboard_AnnotationPermission(a.(*AnnotationPermission), b.(*dashboard.AnnotationPermission), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*dashboard.AnnotationPermission)(nil), (*AnnotationPermission)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_dashboard_AnnotationPermission_To_v2alpha2_AnnotationPermission(a.(*dashboard.AnnotationPermission), b.(*AnnotationPermission), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*DashboardAccess)(nil), (*dashboard.DashboardAccess)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v2alpha2_DashboardAccess_To_dashboard_DashboardAccess(a.(*DashboardAccess), b.(*dashboard.DashboardAccess), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*dashboard.DashboardAccess)(nil), (*DashboardAccess)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_dashboard_DashboardAccess_To_v2alpha2_DashboardAccess(a.(*dashboard.DashboardAccess), b.(*DashboardAccess), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*url.Values)(nil), (*VersionsQueryOptions)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_url_Values_To_v2alpha2_VersionsQueryOptions(a.(*url.Values), b.(*VersionsQueryOptions), scope) - }); err != nil { - return err - } - return nil -} - -func autoConvert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(in *AnnotationActions, out *dashboard.AnnotationActions, s conversion.Scope) error { - out.CanAdd = in.CanAdd - out.CanEdit = in.CanEdit - out.CanDelete = in.CanDelete - return nil -} - -// Convert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions is an autogenerated conversion function. -func Convert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(in *AnnotationActions, out *dashboard.AnnotationActions, s conversion.Scope) error { - return autoConvert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(in, out, s) -} - -func autoConvert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(in *dashboard.AnnotationActions, out *AnnotationActions, s conversion.Scope) error { - out.CanAdd = in.CanAdd - out.CanEdit = in.CanEdit - out.CanDelete = in.CanDelete - return nil -} - -// Convert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions is an autogenerated conversion function. -func Convert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(in *dashboard.AnnotationActions, out *AnnotationActions, s conversion.Scope) error { - return autoConvert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(in, out, s) -} - -func autoConvert_v2alpha2_AnnotationPermission_To_dashboard_AnnotationPermission(in *AnnotationPermission, out *dashboard.AnnotationPermission, s conversion.Scope) error { - if err := Convert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(&in.Dashboard, &out.Dashboard, s); err != nil { - return err - } - if err := Convert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(&in.Organization, &out.Organization, s); err != nil { - return err - } - return nil -} - -// Convert_v2alpha2_AnnotationPermission_To_dashboard_AnnotationPermission is an autogenerated conversion function. -func Convert_v2alpha2_AnnotationPermission_To_dashboard_AnnotationPermission(in *AnnotationPermission, out *dashboard.AnnotationPermission, s conversion.Scope) error { - return autoConvert_v2alpha2_AnnotationPermission_To_dashboard_AnnotationPermission(in, out, s) -} - -func autoConvert_dashboard_AnnotationPermission_To_v2alpha2_AnnotationPermission(in *dashboard.AnnotationPermission, out *AnnotationPermission, s conversion.Scope) error { - if err := Convert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(&in.Dashboard, &out.Dashboard, s); err != nil { - return err - } - if err := Convert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(&in.Organization, &out.Organization, s); err != nil { - return err - } - return nil -} - -// Convert_dashboard_AnnotationPermission_To_v2alpha2_AnnotationPermission is an autogenerated conversion function. -func Convert_dashboard_AnnotationPermission_To_v2alpha2_AnnotationPermission(in *dashboard.AnnotationPermission, out *AnnotationPermission, s conversion.Scope) error { - return autoConvert_dashboard_AnnotationPermission_To_v2alpha2_AnnotationPermission(in, out, s) -} - -func autoConvert_v2alpha2_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error { - out.Slug = in.Slug - out.Url = in.Url - out.CanSave = in.CanSave - out.CanEdit = in.CanEdit - out.CanAdmin = in.CanAdmin - out.CanStar = in.CanStar - out.CanDelete = in.CanDelete - out.AnnotationsPermissions = (*dashboard.AnnotationPermission)(unsafe.Pointer(in.AnnotationsPermissions)) - return nil -} - -// Convert_v2alpha2_DashboardAccess_To_dashboard_DashboardAccess is an autogenerated conversion function. -func Convert_v2alpha2_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error { - return autoConvert_v2alpha2_DashboardAccess_To_dashboard_DashboardAccess(in, out, s) -} - -func autoConvert_dashboard_DashboardAccess_To_v2alpha2_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error { - out.Slug = in.Slug - out.Url = in.Url - out.CanSave = in.CanSave - out.CanEdit = in.CanEdit - out.CanAdmin = in.CanAdmin - out.CanStar = in.CanStar - out.CanDelete = in.CanDelete - out.AnnotationsPermissions = (*AnnotationPermission)(unsafe.Pointer(in.AnnotationsPermissions)) - return nil -} - -// Convert_dashboard_DashboardAccess_To_v2alpha2_DashboardAccess is an autogenerated conversion function. -func Convert_dashboard_DashboardAccess_To_v2alpha2_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error { - return autoConvert_dashboard_DashboardAccess_To_v2alpha2_DashboardAccess(in, out, s) -} - -func autoConvert_url_Values_To_v2alpha2_VersionsQueryOptions(in *url.Values, out *VersionsQueryOptions, s conversion.Scope) error { - // WARNING: Field TypeMeta does not have json tag, skipping. - - if values, ok := map[string][]string(*in)["path"]; ok && len(values) > 0 { - if err := runtime.Convert_Slice_string_To_string(&values, &out.Path, s); err != nil { - return err - } - } else { - out.Path = "" - } - if values, ok := map[string][]string(*in)["version"]; ok && len(values) > 0 { - if err := runtime.Convert_Slice_string_To_int64(&values, &out.Version, s); err != nil { - return err - } - } else { - out.Version = 0 - } - return nil -} - -// Convert_url_Values_To_v2alpha2_VersionsQueryOptions is an autogenerated conversion function. -func Convert_url_Values_To_v2alpha2_VersionsQueryOptions(in *url.Values, out *VersionsQueryOptions, s conversion.Scope) error { - return autoConvert_url_Values_To_v2alpha2_VersionsQueryOptions(in, out, s) -} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi_violation_exceptions.list deleted file mode 100644 index 1db67fbe438..00000000000 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi_violation_exceptions.list +++ /dev/null @@ -1,67 +0,0 @@ -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAdHocFilterWithLabels,ValueLabels -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAdHocFilterWithLabels,Values -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAdhocVariableSpec,BaseFilters -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAdhocVariableSpec,DefaultKeys -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAdhocVariableSpec,Filters -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAnnotationPanelFilter,Ids -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAutoGridLayoutSpec,Items -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardConditionalRenderingGroupSpec,Items -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardCustomVariableSpec,Options -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardDashboardLink,Tags -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardDatasourceVariableSpec,Options -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardFieldConfig,Links -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardFieldConfig,Mappings -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardFieldConfigSource,Overrides -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutSpec,Items -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGroupByVariableSpec,Options -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardIntervalVariableSpec,Options -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardMetadata,Finalizers -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardPanelSpec,Links -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryGroupSpec,Queries -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryGroupSpec,Transformations -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableSpec,Options -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableSpec,StaticOptions -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardRowsLayoutSpec,Rows -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardSpec,Annotations -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardSpec,Links -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardSpec,Tags -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardSpec,Variables -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardStringOrArrayOfString,ArrayOfString -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardTabsLayoutSpec,Tabs -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardThresholdsConfig,Steps -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardTimeSettingsSpec,AutoRefreshIntervals -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardTimeSettingsSpec,QuickRanges -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardV2alpha2FieldConfigSourceOverrides,Properties -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,LibraryPanelStatus,Warnings -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingDataKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingTimeRangeSizeKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,AutoGridLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,GridLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,RowsLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,TabsLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,AutoGridLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,GridLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,RowsLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,TabsLayoutKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardIntervalVariableSpec,AutoCount -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardIntervalVariableSpec,AutoMin -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardPanelKindOrLibraryPanelKind,LibraryPanelKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardPanelKindOrLibraryPanelKind,PanelKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,AdhocVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,ConstantVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,CustomVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,DatasourceVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,GroupByVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,IntervalVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,QueryVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,TextVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardStringOrArrayOfString,ArrayOfString -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardStringOrArrayOfString,String -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardStringOrFloat64,Float64 -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardStringOrFloat64,String -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,RangeMap -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,RegexMap -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,SpecialValueMap -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,ValueMap -API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardList,ListMeta diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/constants.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/constants.go similarity index 91% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/constants.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/constants.go index 3942a399896..d69cd10d958 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/constants.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/constants.go @@ -1,4 +1,4 @@ -package v2alpha2 +package v2beta1 import "k8s.io/apimachinery/pkg/runtime/schema" @@ -6,7 +6,7 @@ const ( // APIGroup is the API group used by all kinds in this package APIGroup = "dashboard.grafana.app" // APIVersion is the API version used by all kinds in this package - APIVersion = "v2alpha2" + APIVersion = "v2beta1" ) var ( diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_codec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_codec_gen.go similarity index 97% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_codec_gen.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_codec_gen.go index 9c679dcafd7..eccc24b91f7 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_codec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_codec_gen.go @@ -2,7 +2,7 @@ // Code generated by grafana-app-sdk. DO NOT EDIT. // -package v2alpha2 +package v2beta1 import ( "encoding/json" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_metadata_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_metadata_gen.go similarity index 98% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_metadata_gen.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_metadata_gen.go index 62e187af661..d260605bd51 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_metadata_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_metadata_gen.go @@ -1,6 +1,6 @@ // Code generated - EDITING IS FUTILE. DO NOT EDIT. -package v2alpha2 +package v2beta1 import ( time "time" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_object_gen.go similarity index 99% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_object_gen.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_object_gen.go index 641ba8ce808..5076b7d9b0c 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_object_gen.go @@ -2,7 +2,7 @@ // Code generated by grafana-app-sdk. DO NOT EDIT. // -package v2alpha2 +package v2beta1 import ( "fmt" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_schema_gen.go similarity index 89% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_schema_gen.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_schema_gen.go index da9ae612d71..30c2237ca31 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_schema_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_schema_gen.go @@ -2,7 +2,7 @@ // Code generated by grafana-app-sdk. DO NOT EDIT. // -package v2alpha2 +package v2beta1 import ( "github.com/grafana/grafana-app-sdk/resource" @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2alpha2", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"), + schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2beta1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"), resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope)) kindDashboard = resource.Kind{ Schema: schemaDashboard, diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue similarity index 97% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec.cue rename to apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue index 4fdce729b2c..a35eaa482f7 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue @@ -1,8 +1,8 @@ // This file is managed by grafana-app-sdk - DO NOT EDIT MANUALLY -// Source: apps/dashboard/kinds/v2alpha2/dashboard_spec.cue +// Source: apps/dashboard/kinds/v2beta1/dashboard_spec.cue // To sync changes, run: make generate in apps/dashboard -package v2alpha2 +package v2beta1 DashboardSpec: { annotations: [...AnnotationQueryKind] | *[] @@ -115,14 +115,6 @@ DashboardLink: { keepTime: bool | *false } -DataSourceRef: { - // The plugin type-id - type?: string - - // Specific datasource instance - uid?: string -} - // A topic is attached to DataFrame metadata in query results. // This specifies where the data should be used. DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames="Series|Annotations|AlertStates") @@ -377,27 +369,27 @@ Kind: { // --- Kinds --- VizConfigSpec: { - pluginVersion: string options: [string]: _ fieldConfig: FieldConfigSource } VizConfigKind: { - // The kind of a VizConfigKind is the plugin ID - kind: string + kind: "VizConfig" + // The group is the plugin ID + group: string + version: string spec: VizConfigSpec } AnnotationQuerySpec: { - datasource?: DataSourceRef - query?: DataQueryKind + query: DataQueryKind enable: bool hide: bool iconColor: string name: string builtIn?: bool | *false filter?: AnnotationPanelFilter - legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties + legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling. } AnnotationQueryKind: { @@ -416,15 +408,19 @@ QueryOptionsSpec: { } DataQueryKind: { - // The kind of a DataQueryKind is the datasource type - kind: string + kind: "DataQuery" + group: string + version: string | *"v0" + // New type for datasource reference + // Not creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS. + datasource?: { + name?: string + } spec: [string]: _ } PanelQuerySpec: { query: DataQueryKind - datasource?: DataSourceRef - refId: string hidden: bool } @@ -727,7 +723,6 @@ QueryVariableSpec: { refresh: VariableRefresh skipUrlSync: bool | *false description?: string - datasource?: DataSourceRef query: DataQueryKind regex: string | *"" sort: VariableSort @@ -865,7 +860,6 @@ CustomVariableKind: { // GroupBy variable specification GroupByVariableSpec: { name: string | *"" - datasource?: DataSourceRef defaultValue?: VariableOption current: VariableOption | *{ text: "" @@ -882,13 +876,16 @@ GroupByVariableSpec: { // Group variable kind GroupByVariableKind: { kind: "GroupByVariable" + group: string + datasource?: { + name?: string + } spec: GroupByVariableSpec } // Adhoc variable specification AdhocVariableSpec: { name: string | *"" - datasource?: DataSourceRef baseFilters: [...AdHocFilterWithLabels] | *[] filters: [...AdHocFilterWithLabels] | *[] defaultKeys: [...MetricFindValue] | *[] @@ -924,6 +921,10 @@ AdHocFilterWithLabels: { // Adhoc variable kind AdhocVariableKind: { kind: "AdhocVariable" + group: string + datasource?: { + name?: string + } spec: AdhocVariableSpec } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go similarity index 95% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec_gen.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go index 32b90053977..d66f582e42a 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go @@ -1,6 +1,6 @@ // Code generated - EDITING IS FUTILE. DO NOT EDIT. -package v2alpha2 +package v2beta1 import ( json "encoding/json" @@ -23,49 +23,42 @@ func NewDashboardAnnotationQueryKind() *DashboardAnnotationQueryKind { // +k8s:openapi-gen=true type DashboardAnnotationQuerySpec struct { - Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` - Query *DashboardDataQueryKind `json:"query,omitempty"` - Enable bool `json:"enable"` - Hide bool `json:"hide"` - IconColor string `json:"iconColor"` - Name string `json:"name"` - BuiltIn *bool `json:"builtIn,omitempty"` - Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"` - // Catch-all field for datasource-specific properties + Query DashboardDataQueryKind `json:"query"` + Enable bool `json:"enable"` + Hide bool `json:"hide"` + IconColor string `json:"iconColor"` + Name string `json:"name"` + BuiltIn *bool `json:"builtIn,omitempty"` + Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"` + // Catch-all field for datasource-specific properties. Should not be available in as code tooling. LegacyOptions map[string]interface{} `json:"legacyOptions,omitempty"` } // NewDashboardAnnotationQuerySpec creates a new DashboardAnnotationQuerySpec object. func NewDashboardAnnotationQuerySpec() *DashboardAnnotationQuerySpec { return &DashboardAnnotationQuerySpec{ + Query: *NewDashboardDataQueryKind(), BuiltIn: (func(input bool) *bool { return &input })(false), } } -// +k8s:openapi-gen=true -type DashboardDataSourceRef struct { - // The plugin type-id - Type *string `json:"type,omitempty"` - // Specific datasource instance - Uid *string `json:"uid,omitempty"` -} - -// NewDashboardDataSourceRef creates a new DashboardDataSourceRef object. -func NewDashboardDataSourceRef() *DashboardDataSourceRef { - return &DashboardDataSourceRef{} -} - // +k8s:openapi-gen=true type DashboardDataQueryKind struct { - // The kind of a DataQueryKind is the datasource type - Kind string `json:"kind"` - Spec map[string]interface{} `json:"spec"` + Kind string `json:"kind"` + Group string `json:"group"` + Version string `json:"version"` + // New type for datasource reference + // Not creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS. + Datasource *DashboardV2beta1DataQueryKindDatasource `json:"datasource,omitempty"` + Spec map[string]interface{} `json:"spec"` } // NewDashboardDataQueryKind creates a new DashboardDataQueryKind object. func NewDashboardDataQueryKind() *DashboardDataQueryKind { return &DashboardDataQueryKind{ - Spec: map[string]interface{}{}, + Kind: "DataQuery", + Version: "v0", + Spec: map[string]interface{}{}, } } @@ -199,10 +192,9 @@ func NewDashboardPanelQueryKind() *DashboardPanelQueryKind { // +k8s:openapi-gen=true type DashboardPanelQuerySpec struct { - Query DashboardDataQueryKind `json:"query"` - Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` - RefId string `json:"refId"` - Hidden bool `json:"hidden"` + Query DashboardDataQueryKind `json:"query"` + RefId string `json:"refId"` + Hidden bool `json:"hidden"` } // NewDashboardPanelQuerySpec creates a new DashboardPanelQuerySpec object. @@ -295,14 +287,17 @@ func NewDashboardQueryOptionsSpec() *DashboardQueryOptionsSpec { // +k8s:openapi-gen=true type DashboardVizConfigKind struct { - // The kind of a VizConfigKind is the plugin ID - Kind string `json:"kind"` - Spec DashboardVizConfigSpec `json:"spec"` + Kind string `json:"kind"` + // The group is the plugin ID + Group string `json:"group"` + Version string `json:"version"` + Spec DashboardVizConfigSpec `json:"spec"` } // NewDashboardVizConfigKind creates a new DashboardVizConfigKind object. func NewDashboardVizConfigKind() *DashboardVizConfigKind { return &DashboardVizConfigKind{ + Kind: "VizConfig", Spec: *NewDashboardVizConfigSpec(), } } @@ -310,9 +305,8 @@ func NewDashboardVizConfigKind() *DashboardVizConfigKind { // --- Kinds --- // +k8s:openapi-gen=true type DashboardVizConfigSpec struct { - PluginVersion string `json:"pluginVersion"` - Options map[string]interface{} `json:"options"` - FieldConfig DashboardFieldConfigSource `json:"fieldConfig"` + Options map[string]interface{} `json:"options"` + FieldConfig DashboardFieldConfigSource `json:"fieldConfig"` } // NewDashboardVizConfigSpec creates a new DashboardVizConfigSpec object. @@ -331,14 +325,14 @@ type DashboardFieldConfigSource struct { // Defaults are the options applied to all fields. Defaults DashboardFieldConfig `json:"defaults"` // Overrides are the options applied to specific fields overriding the defaults. - Overrides []DashboardV2alpha2FieldConfigSourceOverrides `json:"overrides"` + Overrides []DashboardV2beta1FieldConfigSourceOverrides `json:"overrides"` } // NewDashboardFieldConfigSource creates a new DashboardFieldConfigSource object. func NewDashboardFieldConfigSource() *DashboardFieldConfigSource { return &DashboardFieldConfigSource{ Defaults: *NewDashboardFieldConfig(), - Overrides: []DashboardV2alpha2FieldConfigSourceOverrides{}, + Overrides: []DashboardV2beta1FieldConfigSourceOverrides{}, } } @@ -468,14 +462,14 @@ func NewDashboardValueMappingResult() *DashboardValueMappingResult { type DashboardRangeMap struct { Type DashboardMappingType `json:"type"` // Range to match against and the result to apply when the value is within the range - Options DashboardV2alpha2RangeMapOptions `json:"options"` + Options DashboardV2beta1RangeMapOptions `json:"options"` } // NewDashboardRangeMap creates a new DashboardRangeMap object. func NewDashboardRangeMap() *DashboardRangeMap { return &DashboardRangeMap{ Type: DashboardMappingTypeRange, - Options: *NewDashboardV2alpha2RangeMapOptions(), + Options: *NewDashboardV2beta1RangeMapOptions(), } } @@ -485,14 +479,14 @@ func NewDashboardRangeMap() *DashboardRangeMap { type DashboardRegexMap struct { Type DashboardMappingType `json:"type"` // Regular expression to match against and the result to apply when the value matches the regex - Options DashboardV2alpha2RegexMapOptions `json:"options"` + Options DashboardV2beta1RegexMapOptions `json:"options"` } // NewDashboardRegexMap creates a new DashboardRegexMap object. func NewDashboardRegexMap() *DashboardRegexMap { return &DashboardRegexMap{ Type: DashboardMappingTypeRegex, - Options: *NewDashboardV2alpha2RegexMapOptions(), + Options: *NewDashboardV2beta1RegexMapOptions(), } } @@ -501,15 +495,15 @@ func NewDashboardRegexMap() *DashboardRegexMap { // For example, you can configure a special value mapping so that null values appear as N/A. // +k8s:openapi-gen=true type DashboardSpecialValueMap struct { - Type DashboardMappingType `json:"type"` - Options DashboardV2alpha2SpecialValueMapOptions `json:"options"` + Type DashboardMappingType `json:"type"` + Options DashboardV2beta1SpecialValueMapOptions `json:"options"` } // NewDashboardSpecialValueMap creates a new DashboardSpecialValueMap object. func NewDashboardSpecialValueMap() *DashboardSpecialValueMap { return &DashboardSpecialValueMap{ Type: DashboardMappingTypeSpecial, - Options: *NewDashboardV2alpha2SpecialValueMapOptions(), + Options: *NewDashboardV2beta1SpecialValueMapOptions(), } } @@ -1229,7 +1223,6 @@ type DashboardQueryVariableSpec struct { Refresh DashboardVariableRefresh `json:"refresh"` SkipUrlSync bool `json:"skipUrlSync"` Description *string `json:"description,omitempty"` - Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` Query DashboardDataQueryKind `json:"query"` Regex string `json:"regex"` Sort DashboardVariableSort `json:"sort"` @@ -1591,8 +1584,10 @@ func NewDashboardCustomVariableSpec() *DashboardCustomVariableSpec { // Group variable kind // +k8s:openapi-gen=true type DashboardGroupByVariableKind struct { - Kind string `json:"kind"` - Spec DashboardGroupByVariableSpec `json:"spec"` + Kind string `json:"kind"` + Group string `json:"group"` + Datasource *DashboardV2beta1GroupByVariableKindDatasource `json:"datasource,omitempty"` + Spec DashboardGroupByVariableSpec `json:"spec"` } // NewDashboardGroupByVariableKind creates a new DashboardGroupByVariableKind object. @@ -1607,7 +1602,6 @@ func NewDashboardGroupByVariableKind() *DashboardGroupByVariableKind { // +k8s:openapi-gen=true type DashboardGroupByVariableSpec struct { Name string `json:"name"` - Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` DefaultValue *DashboardVariableOption `json:"defaultValue,omitempty"` Current DashboardVariableOption `json:"current"` Options []DashboardVariableOption `json:"options"` @@ -1640,8 +1634,10 @@ func NewDashboardGroupByVariableSpec() *DashboardGroupByVariableSpec { // Adhoc variable kind // +k8s:openapi-gen=true type DashboardAdhocVariableKind struct { - Kind string `json:"kind"` - Spec DashboardAdhocVariableSpec `json:"spec"` + Kind string `json:"kind"` + Group string `json:"group"` + Datasource *DashboardV2beta1AdhocVariableKindDatasource `json:"datasource,omitempty"` + Spec DashboardAdhocVariableSpec `json:"spec"` } // NewDashboardAdhocVariableKind creates a new DashboardAdhocVariableKind object. @@ -1656,7 +1652,6 @@ func NewDashboardAdhocVariableKind() *DashboardAdhocVariableKind { // +k8s:openapi-gen=true type DashboardAdhocVariableSpec struct { Name string `json:"name"` - Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` BaseFilters []DashboardAdHocFilterWithLabels `json:"baseFilters"` Filters []DashboardAdHocFilterWithLabels `json:"filters"` DefaultKeys []DashboardMetricFindValue `json:"defaultKeys"` @@ -1771,21 +1766,31 @@ func NewDashboardSpec() *DashboardSpec { } // +k8s:openapi-gen=true -type DashboardV2alpha2FieldConfigSourceOverrides struct { +type DashboardV2beta1DataQueryKindDatasource struct { + Name *string `json:"name,omitempty"` +} + +// NewDashboardV2beta1DataQueryKindDatasource creates a new DashboardV2beta1DataQueryKindDatasource object. +func NewDashboardV2beta1DataQueryKindDatasource() *DashboardV2beta1DataQueryKindDatasource { + return &DashboardV2beta1DataQueryKindDatasource{} +} + +// +k8s:openapi-gen=true +type DashboardV2beta1FieldConfigSourceOverrides struct { Matcher DashboardMatcherConfig `json:"matcher"` Properties []DashboardDynamicConfigValue `json:"properties"` } -// NewDashboardV2alpha2FieldConfigSourceOverrides creates a new DashboardV2alpha2FieldConfigSourceOverrides object. -func NewDashboardV2alpha2FieldConfigSourceOverrides() *DashboardV2alpha2FieldConfigSourceOverrides { - return &DashboardV2alpha2FieldConfigSourceOverrides{ +// NewDashboardV2beta1FieldConfigSourceOverrides creates a new DashboardV2beta1FieldConfigSourceOverrides object. +func NewDashboardV2beta1FieldConfigSourceOverrides() *DashboardV2beta1FieldConfigSourceOverrides { + return &DashboardV2beta1FieldConfigSourceOverrides{ Matcher: *NewDashboardMatcherConfig(), Properties: []DashboardDynamicConfigValue{}, } } // +k8s:openapi-gen=true -type DashboardV2alpha2RangeMapOptions struct { +type DashboardV2beta1RangeMapOptions struct { // Min value of the range. It can be null which means -Infinity From *float64 `json:"from"` // Max value of the range. It can be null which means +Infinity @@ -1794,43 +1799,63 @@ type DashboardV2alpha2RangeMapOptions struct { Result DashboardValueMappingResult `json:"result"` } -// NewDashboardV2alpha2RangeMapOptions creates a new DashboardV2alpha2RangeMapOptions object. -func NewDashboardV2alpha2RangeMapOptions() *DashboardV2alpha2RangeMapOptions { - return &DashboardV2alpha2RangeMapOptions{ +// NewDashboardV2beta1RangeMapOptions creates a new DashboardV2beta1RangeMapOptions object. +func NewDashboardV2beta1RangeMapOptions() *DashboardV2beta1RangeMapOptions { + return &DashboardV2beta1RangeMapOptions{ Result: *NewDashboardValueMappingResult(), } } // +k8s:openapi-gen=true -type DashboardV2alpha2RegexMapOptions struct { +type DashboardV2beta1RegexMapOptions struct { // Regular expression to match against Pattern string `json:"pattern"` // Config to apply when the value matches the regex Result DashboardValueMappingResult `json:"result"` } -// NewDashboardV2alpha2RegexMapOptions creates a new DashboardV2alpha2RegexMapOptions object. -func NewDashboardV2alpha2RegexMapOptions() *DashboardV2alpha2RegexMapOptions { - return &DashboardV2alpha2RegexMapOptions{ +// NewDashboardV2beta1RegexMapOptions creates a new DashboardV2beta1RegexMapOptions object. +func NewDashboardV2beta1RegexMapOptions() *DashboardV2beta1RegexMapOptions { + return &DashboardV2beta1RegexMapOptions{ Result: *NewDashboardValueMappingResult(), } } // +k8s:openapi-gen=true -type DashboardV2alpha2SpecialValueMapOptions struct { +type DashboardV2beta1SpecialValueMapOptions struct { // Special value to match against Match DashboardSpecialValueMatch `json:"match"` // Config to apply when the value matches the special value Result DashboardValueMappingResult `json:"result"` } -// NewDashboardV2alpha2SpecialValueMapOptions creates a new DashboardV2alpha2SpecialValueMapOptions object. -func NewDashboardV2alpha2SpecialValueMapOptions() *DashboardV2alpha2SpecialValueMapOptions { - return &DashboardV2alpha2SpecialValueMapOptions{ +// NewDashboardV2beta1SpecialValueMapOptions creates a new DashboardV2beta1SpecialValueMapOptions object. +func NewDashboardV2beta1SpecialValueMapOptions() *DashboardV2beta1SpecialValueMapOptions { + return &DashboardV2beta1SpecialValueMapOptions{ Result: *NewDashboardValueMappingResult(), } } +// +k8s:openapi-gen=true +type DashboardV2beta1GroupByVariableKindDatasource struct { + Name *string `json:"name,omitempty"` +} + +// NewDashboardV2beta1GroupByVariableKindDatasource creates a new DashboardV2beta1GroupByVariableKindDatasource object. +func NewDashboardV2beta1GroupByVariableKindDatasource() *DashboardV2beta1GroupByVariableKindDatasource { + return &DashboardV2beta1GroupByVariableKindDatasource{} +} + +// +k8s:openapi-gen=true +type DashboardV2beta1AdhocVariableKindDatasource struct { + Name *string `json:"name,omitempty"` +} + +// NewDashboardV2beta1AdhocVariableKindDatasource creates a new DashboardV2beta1AdhocVariableKindDatasource object. +func NewDashboardV2beta1AdhocVariableKindDatasource() *DashboardV2beta1AdhocVariableKindDatasource { + return &DashboardV2beta1AdhocVariableKindDatasource{} +} + // +k8s:openapi-gen=true type DashboardRepeatOptionsDirection string diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_status_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_status_gen.go similarity index 98% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_status_gen.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_status_gen.go index 322f9f072ec..87ab45e9c54 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_status_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_status_gen.go @@ -1,6 +1,6 @@ // Code generated - EDITING IS FUTILE. DO NOT EDIT. -package v2alpha2 +package v2beta1 // ConversionStatus is the status of the conversion of the dashboard. // +k8s:openapi-gen=true diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/doc.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/doc.go similarity index 80% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/doc.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/doc.go index 95dba55d4cd..ec8aad18799 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/doc.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/doc.go @@ -7,4 +7,4 @@ // because grafana-app-sdk already provides deepcopy functions. // Kinds which are not generated by the SDK are explicitly opted in to deepcopy generation. -package v2alpha2 // import "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" +package v2beta1 // import "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/register.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/register.go similarity index 98% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/register.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/register.go index 0a68d910fb1..28f74501e6e 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/register.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/register.go @@ -1,4 +1,4 @@ -package v2alpha2 +package v2beta1 import ( "fmt" @@ -12,7 +12,7 @@ import ( const ( GROUP = "dashboard.grafana.app" - VERSION = "v2alpha2" + VERSION = "v2beta1" APIVERSION = GROUP + "/" + VERSION ) diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/types.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/types.go similarity index 99% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/types.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/types.go index b40a86fe94e..8ecda3cfd7d 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/types.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/types.go @@ -1,4 +1,4 @@ -package v2alpha2 +package v2beta1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/validation.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/validation.go similarity index 99% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/validation.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/validation.go index 9b01c53a3e1..3946c0d9b14 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/validation.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/validation.go @@ -1,4 +1,4 @@ -package v2alpha2 +package v2beta1 import ( _ "embed" diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.conversion.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.conversion.go new file mode 100644 index 00000000000..fce43178b8a --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.conversion.go @@ -0,0 +1,175 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by conversion-gen. DO NOT EDIT. + +package v2beta1 + +import ( + url "net/url" + unsafe "unsafe" + + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +func init() { + localSchemeBuilder.Register(RegisterConversions) +} + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*AnnotationActions)(nil), (*dashboard.AnnotationActions)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v2beta1_AnnotationActions_To_dashboard_AnnotationActions(a.(*AnnotationActions), b.(*dashboard.AnnotationActions), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*dashboard.AnnotationActions)(nil), (*AnnotationActions)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_dashboard_AnnotationActions_To_v2beta1_AnnotationActions(a.(*dashboard.AnnotationActions), b.(*AnnotationActions), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*AnnotationPermission)(nil), (*dashboard.AnnotationPermission)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v2beta1_AnnotationPermission_To_dashboard_AnnotationPermission(a.(*AnnotationPermission), b.(*dashboard.AnnotationPermission), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*dashboard.AnnotationPermission)(nil), (*AnnotationPermission)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_dashboard_AnnotationPermission_To_v2beta1_AnnotationPermission(a.(*dashboard.AnnotationPermission), b.(*AnnotationPermission), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*DashboardAccess)(nil), (*dashboard.DashboardAccess)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v2beta1_DashboardAccess_To_dashboard_DashboardAccess(a.(*DashboardAccess), b.(*dashboard.DashboardAccess), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*dashboard.DashboardAccess)(nil), (*DashboardAccess)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_dashboard_DashboardAccess_To_v2beta1_DashboardAccess(a.(*dashboard.DashboardAccess), b.(*DashboardAccess), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*url.Values)(nil), (*VersionsQueryOptions)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_url_Values_To_v2beta1_VersionsQueryOptions(a.(*url.Values), b.(*VersionsQueryOptions), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v2beta1_AnnotationActions_To_dashboard_AnnotationActions(in *AnnotationActions, out *dashboard.AnnotationActions, s conversion.Scope) error { + out.CanAdd = in.CanAdd + out.CanEdit = in.CanEdit + out.CanDelete = in.CanDelete + return nil +} + +// Convert_v2beta1_AnnotationActions_To_dashboard_AnnotationActions is an autogenerated conversion function. +func Convert_v2beta1_AnnotationActions_To_dashboard_AnnotationActions(in *AnnotationActions, out *dashboard.AnnotationActions, s conversion.Scope) error { + return autoConvert_v2beta1_AnnotationActions_To_dashboard_AnnotationActions(in, out, s) +} + +func autoConvert_dashboard_AnnotationActions_To_v2beta1_AnnotationActions(in *dashboard.AnnotationActions, out *AnnotationActions, s conversion.Scope) error { + out.CanAdd = in.CanAdd + out.CanEdit = in.CanEdit + out.CanDelete = in.CanDelete + return nil +} + +// Convert_dashboard_AnnotationActions_To_v2beta1_AnnotationActions is an autogenerated conversion function. +func Convert_dashboard_AnnotationActions_To_v2beta1_AnnotationActions(in *dashboard.AnnotationActions, out *AnnotationActions, s conversion.Scope) error { + return autoConvert_dashboard_AnnotationActions_To_v2beta1_AnnotationActions(in, out, s) +} + +func autoConvert_v2beta1_AnnotationPermission_To_dashboard_AnnotationPermission(in *AnnotationPermission, out *dashboard.AnnotationPermission, s conversion.Scope) error { + if err := Convert_v2beta1_AnnotationActions_To_dashboard_AnnotationActions(&in.Dashboard, &out.Dashboard, s); err != nil { + return err + } + if err := Convert_v2beta1_AnnotationActions_To_dashboard_AnnotationActions(&in.Organization, &out.Organization, s); err != nil { + return err + } + return nil +} + +// Convert_v2beta1_AnnotationPermission_To_dashboard_AnnotationPermission is an autogenerated conversion function. +func Convert_v2beta1_AnnotationPermission_To_dashboard_AnnotationPermission(in *AnnotationPermission, out *dashboard.AnnotationPermission, s conversion.Scope) error { + return autoConvert_v2beta1_AnnotationPermission_To_dashboard_AnnotationPermission(in, out, s) +} + +func autoConvert_dashboard_AnnotationPermission_To_v2beta1_AnnotationPermission(in *dashboard.AnnotationPermission, out *AnnotationPermission, s conversion.Scope) error { + if err := Convert_dashboard_AnnotationActions_To_v2beta1_AnnotationActions(&in.Dashboard, &out.Dashboard, s); err != nil { + return err + } + if err := Convert_dashboard_AnnotationActions_To_v2beta1_AnnotationActions(&in.Organization, &out.Organization, s); err != nil { + return err + } + return nil +} + +// Convert_dashboard_AnnotationPermission_To_v2beta1_AnnotationPermission is an autogenerated conversion function. +func Convert_dashboard_AnnotationPermission_To_v2beta1_AnnotationPermission(in *dashboard.AnnotationPermission, out *AnnotationPermission, s conversion.Scope) error { + return autoConvert_dashboard_AnnotationPermission_To_v2beta1_AnnotationPermission(in, out, s) +} + +func autoConvert_v2beta1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error { + out.Slug = in.Slug + out.Url = in.Url + out.CanSave = in.CanSave + out.CanEdit = in.CanEdit + out.CanAdmin = in.CanAdmin + out.CanStar = in.CanStar + out.CanDelete = in.CanDelete + out.AnnotationsPermissions = (*dashboard.AnnotationPermission)(unsafe.Pointer(in.AnnotationsPermissions)) + return nil +} + +// Convert_v2beta1_DashboardAccess_To_dashboard_DashboardAccess is an autogenerated conversion function. +func Convert_v2beta1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error { + return autoConvert_v2beta1_DashboardAccess_To_dashboard_DashboardAccess(in, out, s) +} + +func autoConvert_dashboard_DashboardAccess_To_v2beta1_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error { + out.Slug = in.Slug + out.Url = in.Url + out.CanSave = in.CanSave + out.CanEdit = in.CanEdit + out.CanAdmin = in.CanAdmin + out.CanStar = in.CanStar + out.CanDelete = in.CanDelete + out.AnnotationsPermissions = (*AnnotationPermission)(unsafe.Pointer(in.AnnotationsPermissions)) + return nil +} + +// Convert_dashboard_DashboardAccess_To_v2beta1_DashboardAccess is an autogenerated conversion function. +func Convert_dashboard_DashboardAccess_To_v2beta1_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error { + return autoConvert_dashboard_DashboardAccess_To_v2beta1_DashboardAccess(in, out, s) +} + +func autoConvert_url_Values_To_v2beta1_VersionsQueryOptions(in *url.Values, out *VersionsQueryOptions, s conversion.Scope) error { + // WARNING: Field TypeMeta does not have json tag, skipping. + + if values, ok := map[string][]string(*in)["path"]; ok && len(values) > 0 { + if err := runtime.Convert_Slice_string_To_string(&values, &out.Path, s); err != nil { + return err + } + } else { + out.Path = "" + } + if values, ok := map[string][]string(*in)["version"]; ok && len(values) > 0 { + if err := runtime.Convert_Slice_string_To_int64(&values, &out.Version, s); err != nil { + return err + } + } else { + out.Version = 0 + } + return nil +} + +// Convert_url_Values_To_v2beta1_VersionsQueryOptions is an autogenerated conversion function. +func Convert_url_Values_To_v2beta1_VersionsQueryOptions(in *url.Values, out *VersionsQueryOptions, s conversion.Scope) error { + return autoConvert_url_Values_To_v2beta1_VersionsQueryOptions(in, out, s) +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.deepcopy.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.deepcopy.go similarity index 99% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.deepcopy.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.deepcopy.go index 9a8a05f7f22..5216c101ff9 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.deepcopy.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ // Code generated by deepcopy-gen. DO NOT EDIT. -package v2alpha2 +package v2beta1 import ( v0alpha1 "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.defaults.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.defaults.go similarity index 96% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.defaults.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.defaults.go index efead5bdea7..a41e5b8d613 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.defaults.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.defaults.go @@ -5,7 +5,7 @@ // Code generated by defaulter-gen. DO NOT EDIT. -package v2alpha2 +package v2beta1 import ( runtime "k8s.io/apimachinery/pkg/runtime" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go similarity index 73% rename from apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi.go rename to apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go index 7ed5e3983d4..c7aa3cb8481 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go @@ -5,7 +5,7 @@ // Code generated by openapi-gen. DO NOT EDIT. -package v2alpha2 +package v2beta1 import ( common "k8s.io/kube-openapi/pkg/common" @@ -14,123 +14,125 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationActions": schema_pkg_apis_dashboard_v2alpha2_AnnotationActions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationPermission": schema_pkg_apis_dashboard_v2alpha2_AnnotationPermission(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.Dashboard": schema_pkg_apis_dashboard_v2alpha2_Dashboard(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAccess": schema_pkg_apis_dashboard_v2alpha2_DashboardAccess(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2alpha2_DashboardAdHocFilterWithLabels(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationPanelFilter(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQuerySpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridRepeatOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingDataKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingDataSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingTimeRangeSizeKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingTimeRangeSizeSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConversionStatus": schema_pkg_apis_dashboard_v2alpha2_DashboardConversionStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDashboardLink": schema_pkg_apis_dashboard_v2alpha2_DashboardDashboardLink(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataLink": schema_pkg_apis_dashboard_v2alpha2_DashboardDataLink(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind": schema_pkg_apis_dashboard_v2alpha2_DashboardDataQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef": schema_pkg_apis_dashboard_v2alpha2_DashboardDataSourceRef(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataTransformerConfig": schema_pkg_apis_dashboard_v2alpha2_DashboardDataTransformerConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDynamicConfigValue": schema_pkg_apis_dashboard_v2alpha2_DashboardDynamicConfigValue(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardElementReference": schema_pkg_apis_dashboard_v2alpha2_DashboardElementReference(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldColor": schema_pkg_apis_dashboard_v2alpha2_DashboardFieldColor(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfig": schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfigSource": schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfigSource(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardJSONCodec": schema_pkg_apis_dashboard_v2alpha2_DashboardJSONCodec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKindSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKindSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelRef": schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelRef(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardList": schema_pkg_apis_dashboard_v2alpha2_DashboardList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMatcherConfig": schema_pkg_apis_dashboard_v2alpha2_DashboardMatcherConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMetadata": schema_pkg_apis_dashboard_v2alpha2_DashboardMetadata(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMetricFindValue": schema_pkg_apis_dashboard_v2alpha2_DashboardMetricFindValue(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKind": schema_pkg_apis_dashboard_v2alpha2_DashboardPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKindOrLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha2_DashboardPanelKindOrLibraryPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQueryKind": schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQuerySpec": schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQuerySpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardPanelSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupKind": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryOptionsSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryOptionsSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRangeMap": schema_pkg_apis_dashboard_v2alpha2_DashboardRangeMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRegexMap": schema_pkg_apis_dashboard_v2alpha2_DashboardRegexMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRepeatOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowRepeatOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardRowRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowKind": schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpecialValueMap": schema_pkg_apis_dashboard_v2alpha2_DashboardSpecialValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStatus": schema_pkg_apis_dashboard_v2alpha2_DashboardStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrArrayOfString": schema_pkg_apis_dashboard_v2alpha2_DashboardStringOrArrayOfString(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrFloat64": schema_pkg_apis_dashboard_v2alpha2_DashboardStringOrFloat64(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabRepeatOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardTabRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabKind": schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThreshold": schema_pkg_apis_dashboard_v2alpha2_DashboardThreshold(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThresholdsConfig": schema_pkg_apis_dashboard_v2alpha2_DashboardThresholdsConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeRangeOption": schema_pkg_apis_dashboard_v2alpha2_DashboardTimeRangeOption(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeSettingsSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardTimeSettingsSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTransformationKind": schema_pkg_apis_dashboard_v2alpha2_DashboardTransformationKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2FieldConfigSourceOverrides": schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2FieldConfigSourceOverrides(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RangeMapOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RangeMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RegexMapOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RegexMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2SpecialValueMapOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2SpecialValueMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMap": schema_pkg_apis_dashboard_v2alpha2_DashboardValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap": schema_pkg_apis_dashboard_v2alpha2_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult": schema_pkg_apis_dashboard_v2alpha2_DashboardValueMappingResult(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption": schema_pkg_apis_dashboard_v2alpha2_DashboardVariableOption(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVersionInfo": schema_pkg_apis_dashboard_v2alpha2_DashboardVersionInfo(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVersionList": schema_pkg_apis_dashboard_v2alpha2_DashboardVersionList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigKind": schema_pkg_apis_dashboard_v2alpha2_DashboardVizConfigKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardVizConfigSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v2alpha2_DashboardWithAccessInfo(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanel": schema_pkg_apis_dashboard_v2alpha2_LibraryPanel(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelList": schema_pkg_apis_dashboard_v2alpha2_LibraryPanelList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelSpec": schema_pkg_apis_dashboard_v2alpha2_LibraryPanelSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelStatus": schema_pkg_apis_dashboard_v2alpha2_LibraryPanelStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.VersionsQueryOptions": schema_pkg_apis_dashboard_v2alpha2_VersionsQueryOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.AnnotationActions": schema_pkg_apis_dashboard_v2beta1_AnnotationActions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.AnnotationPermission": schema_pkg_apis_dashboard_v2beta1_AnnotationPermission(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.Dashboard": schema_pkg_apis_dashboard_v2beta1_Dashboard(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAccess": schema_pkg_apis_dashboard_v2beta1_DashboardAccess(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2beta1_DashboardAdHocFilterWithLabels(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationPanelFilter(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQuerySpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutItemKind": schema_pkg_apis_dashboard_v2beta1_DashboardAutoGridLayoutItemKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutItemSpec": schema_pkg_apis_dashboard_v2beta1_DashboardAutoGridLayoutItemSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutKind": schema_pkg_apis_dashboard_v2beta1_DashboardAutoGridLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutSpec": schema_pkg_apis_dashboard_v2beta1_DashboardAutoGridLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridRepeatOptions": schema_pkg_apis_dashboard_v2beta1_DashboardAutoGridRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingDataKind": schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingDataKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingDataSpec": schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingDataSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingGroupKind": schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingGroupKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingGroupSpec": schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingGroupSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingTimeRangeSizeKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingTimeRangeSizeSpec": schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingTimeRangeSizeSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardConstantVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardConstantVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConversionStatus": schema_pkg_apis_dashboard_v2beta1_DashboardConversionStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardCustomVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardCustomVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDashboardLink": schema_pkg_apis_dashboard_v2beta1_DashboardDashboardLink(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataLink": schema_pkg_apis_dashboard_v2beta1_DashboardDataLink(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataQueryKind": schema_pkg_apis_dashboard_v2beta1_DashboardDataQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataTransformerConfig": schema_pkg_apis_dashboard_v2beta1_DashboardDataTransformerConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDatasourceVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardDatasourceVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDatasourceVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardDatasourceVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDynamicConfigValue": schema_pkg_apis_dashboard_v2beta1_DashboardDynamicConfigValue(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardElementReference": schema_pkg_apis_dashboard_v2beta1_DashboardElementReference(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardFieldColor": schema_pkg_apis_dashboard_v2beta1_DashboardFieldColor(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardFieldConfig": schema_pkg_apis_dashboard_v2beta1_DashboardFieldConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardFieldConfigSource": schema_pkg_apis_dashboard_v2beta1_DashboardFieldConfigSource(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutItemKind": schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutItemKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutItemSpec": schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutItemSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKind": schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutSpec": schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGroupByVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardGroupByVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGroupByVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardGroupByVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardIntervalVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardIntervalVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardIntervalVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardIntervalVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardJSONCodec": schema_pkg_apis_dashboard_v2beta1_DashboardJSONCodec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardLibraryPanelKind": schema_pkg_apis_dashboard_v2beta1_DashboardLibraryPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardLibraryPanelKindSpec": schema_pkg_apis_dashboard_v2beta1_DashboardLibraryPanelKindSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardLibraryPanelRef": schema_pkg_apis_dashboard_v2beta1_DashboardLibraryPanelRef(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardList": schema_pkg_apis_dashboard_v2beta1_DashboardList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardMatcherConfig": schema_pkg_apis_dashboard_v2beta1_DashboardMatcherConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardMetadata": schema_pkg_apis_dashboard_v2beta1_DashboardMetadata(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardMetricFindValue": schema_pkg_apis_dashboard_v2beta1_DashboardMetricFindValue(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelKind": schema_pkg_apis_dashboard_v2beta1_DashboardPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelKindOrLibraryPanelKind": schema_pkg_apis_dashboard_v2beta1_DashboardPanelKindOrLibraryPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelQueryKind": schema_pkg_apis_dashboard_v2beta1_DashboardPanelQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelQuerySpec": schema_pkg_apis_dashboard_v2beta1_DashboardPanelQuerySpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelSpec": schema_pkg_apis_dashboard_v2beta1_DashboardPanelSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryGroupKind": schema_pkg_apis_dashboard_v2beta1_DashboardQueryGroupKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryGroupSpec": schema_pkg_apis_dashboard_v2beta1_DashboardQueryGroupSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryOptionsSpec": schema_pkg_apis_dashboard_v2beta1_DashboardQueryOptionsSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardQueryVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardQueryVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRangeMap": schema_pkg_apis_dashboard_v2beta1_DashboardRangeMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRegexMap": schema_pkg_apis_dashboard_v2beta1_DashboardRegexMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRepeatOptions": schema_pkg_apis_dashboard_v2beta1_DashboardRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowRepeatOptions": schema_pkg_apis_dashboard_v2beta1_DashboardRowRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutKind": schema_pkg_apis_dashboard_v2beta1_DashboardRowsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutRowKind": schema_pkg_apis_dashboard_v2beta1_DashboardRowsLayoutRowKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutRowSpec": schema_pkg_apis_dashboard_v2beta1_DashboardRowsLayoutRowSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutSpec": schema_pkg_apis_dashboard_v2beta1_DashboardRowsLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardSpec": schema_pkg_apis_dashboard_v2beta1_DashboardSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardSpecialValueMap": schema_pkg_apis_dashboard_v2beta1_DashboardSpecialValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStatus": schema_pkg_apis_dashboard_v2beta1_DashboardStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStringOrArrayOfString": schema_pkg_apis_dashboard_v2beta1_DashboardStringOrArrayOfString(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStringOrFloat64": schema_pkg_apis_dashboard_v2beta1_DashboardStringOrFloat64(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabRepeatOptions": schema_pkg_apis_dashboard_v2beta1_DashboardTabRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutKind": schema_pkg_apis_dashboard_v2beta1_DashboardTabsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutSpec": schema_pkg_apis_dashboard_v2beta1_DashboardTabsLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutTabKind": schema_pkg_apis_dashboard_v2beta1_DashboardTabsLayoutTabKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutTabSpec": schema_pkg_apis_dashboard_v2beta1_DashboardTabsLayoutTabSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTextVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardTextVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTextVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardTextVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardThreshold": schema_pkg_apis_dashboard_v2beta1_DashboardThreshold(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardThresholdsConfig": schema_pkg_apis_dashboard_v2beta1_DashboardThresholdsConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTimeRangeOption": schema_pkg_apis_dashboard_v2beta1_DashboardTimeRangeOption(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTimeSettingsSpec": schema_pkg_apis_dashboard_v2beta1_DashboardTimeSettingsSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTransformationKind": schema_pkg_apis_dashboard_v2beta1_DashboardTransformationKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1AdhocVariableKindDatasource": schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1AdhocVariableKindDatasource(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1DataQueryKindDatasource": schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1DataQueryKindDatasource(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1FieldConfigSourceOverrides": schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1FieldConfigSourceOverrides(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1GroupByVariableKindDatasource": schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1GroupByVariableKindDatasource(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1RangeMapOptions": schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1RangeMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1RegexMapOptions": schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1RegexMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1SpecialValueMapOptions": schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1SpecialValueMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMap": schema_pkg_apis_dashboard_v2beta1_DashboardValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap": schema_pkg_apis_dashboard_v2beta1_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMappingResult": schema_pkg_apis_dashboard_v2beta1_DashboardValueMappingResult(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption": schema_pkg_apis_dashboard_v2beta1_DashboardVariableOption(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVersionInfo": schema_pkg_apis_dashboard_v2beta1_DashboardVersionInfo(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVersionList": schema_pkg_apis_dashboard_v2beta1_DashboardVersionList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVizConfigKind": schema_pkg_apis_dashboard_v2beta1_DashboardVizConfigKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVizConfigSpec": schema_pkg_apis_dashboard_v2beta1_DashboardVizConfigSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v2beta1_DashboardWithAccessInfo(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.LibraryPanel": schema_pkg_apis_dashboard_v2beta1_LibraryPanel(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.LibraryPanelList": schema_pkg_apis_dashboard_v2beta1_LibraryPanelList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.LibraryPanelSpec": schema_pkg_apis_dashboard_v2beta1_LibraryPanelSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.LibraryPanelStatus": schema_pkg_apis_dashboard_v2beta1_LibraryPanelStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.VersionsQueryOptions": schema_pkg_apis_dashboard_v2beta1_VersionsQueryOptions(ref), } } -func schema_pkg_apis_dashboard_v2alpha2_AnnotationActions(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_AnnotationActions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -164,7 +166,7 @@ func schema_pkg_apis_dashboard_v2alpha2_AnnotationActions(ref common.ReferenceCa } } -func schema_pkg_apis_dashboard_v2alpha2_AnnotationPermission(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_AnnotationPermission(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -173,13 +175,13 @@ func schema_pkg_apis_dashboard_v2alpha2_AnnotationPermission(ref common.Referenc "dashboard": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationActions"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.AnnotationActions"), }, }, "organization": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationActions"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.AnnotationActions"), }, }, }, @@ -187,11 +189,11 @@ func schema_pkg_apis_dashboard_v2alpha2_AnnotationPermission(ref common.Referenc }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationActions"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.AnnotationActions"}, } } -func schema_pkg_apis_dashboard_v2alpha2_Dashboard(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_Dashboard(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -221,13 +223,13 @@ func schema_pkg_apis_dashboard_v2alpha2_Dashboard(ref common.ReferenceCallback) SchemaProps: spec.SchemaProps{ Description: "Spec is the spec of the Dashboard", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardSpec"), }, }, "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStatus"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStatus"), }, }, }, @@ -235,11 +237,11 @@ func schema_pkg_apis_dashboard_v2alpha2_Dashboard(ref common.ReferenceCallback) }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAccess(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAccess(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -297,7 +299,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAccess(ref common.ReferenceCall }, "annotationsPermissions": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationPermission"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.AnnotationPermission"), }, }, }, @@ -305,11 +307,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAccess(ref common.ReferenceCall }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationPermission"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.AnnotationPermission"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAdHocFilterWithLabels(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAdHocFilterWithLabels(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -397,7 +399,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAdHocFilterWithLabels(ref commo } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -411,22 +413,34 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableKind(ref common.Re Format: "", }, }, + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "datasource": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1AdhocVariableKindDatasource"), + }, + }, "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdhocVariableSpec"), }, }, }, - Required: []string{"kind", "spec"}, + Required: []string{"kind", "group", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdhocVariableSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1AdhocVariableKindDatasource"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -440,11 +454,6 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableSpec(ref common.Re Format: "", }, }, - "datasource": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"), - }, - }, "baseFilters": { SchemaProps: spec.SchemaProps{ Type: []string{"array"}, @@ -452,7 +461,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableSpec(ref common.Re Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdHocFilterWithLabels"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdHocFilterWithLabels"), }, }, }, @@ -465,7 +474,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableSpec(ref common.Re Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdHocFilterWithLabels"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdHocFilterWithLabels"), }, }, }, @@ -478,7 +487,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableSpec(ref common.Re Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMetricFindValue"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardMetricFindValue"), }, }, }, @@ -522,11 +531,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableSpec(ref common.Re }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdHocFilterWithLabels", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMetricFindValue"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdHocFilterWithLabels", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardMetricFindValue"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationPanelFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationPanelFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -561,7 +570,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationPanelFilter(ref commo } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQueryKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQueryKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -577,7 +586,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQueryKind(ref common. "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQuerySpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQuerySpec"), }, }, }, @@ -585,24 +594,20 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQueryKind(ref common. }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQuerySpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQuerySpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQuerySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQuerySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "datasource": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"), - }, - }, "query": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind"), + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataQueryKind"), }, }, "enable": { @@ -641,12 +646,12 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQuerySpec(ref common. }, "filter": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationPanelFilter"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationPanelFilter"), }, }, "legacyOptions": { SchemaProps: spec.SchemaProps{ - Description: "Catch-all field for datasource-specific properties", + Description: "Catch-all field for datasource-specific properties. Should not be available in as code tooling.", Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{ Allows: true, @@ -660,15 +665,15 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQuerySpec(ref common. }, }, }, - Required: []string{"enable", "hide", "iconColor", "name"}, + Required: []string{"query", "enable", "hide", "iconColor", "name"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationPanelFilter", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationPanelFilter", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataQueryKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAutoGridLayoutItemKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -684,7 +689,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemKind(ref comm "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutItemSpec"), }, }, }, @@ -692,11 +697,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemKind(ref comm }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutItemSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAutoGridLayoutItemSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -705,17 +710,17 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemSpec(ref comm "element": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardElementReference"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardElementReference"), }, }, "repeat": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridRepeatOptions"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridRepeatOptions"), }, }, "conditionalRendering": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingGroupKind"), }, }, }, @@ -723,11 +728,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemSpec(ref comm }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridRepeatOptions", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardElementReference"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridRepeatOptions", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardElementReference"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAutoGridLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -743,7 +748,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutKind(ref common.R "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutSpec"), }, }, }, @@ -751,11 +756,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutKind(ref common.R }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAutoGridLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -806,7 +811,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutSpec(ref common.R Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutItemKind"), }, }, }, @@ -817,11 +822,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutSpec(ref common.R }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutItemKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardAutoGridRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -848,7 +853,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridRepeatOptions(ref commo } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingDataKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingDataKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -864,7 +869,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingDataKind(re "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingDataSpec"), }, }, }, @@ -872,11 +877,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingDataKind(re }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingDataSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingDataSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingDataSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -896,7 +901,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingDataSpec(re } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingGroupKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -912,7 +917,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupKind(r "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingGroupSpec"), }, }, }, @@ -920,11 +925,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupKind(r }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingGroupSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -950,7 +955,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupSpec(r Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind"), }, }, }, @@ -961,11 +966,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupSpec(r }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingTimeRangeSizeKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingTimeRangeSizeKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -981,7 +986,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingTimeRangeSi "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingTimeRangeSizeSpec"), }, }, }, @@ -989,11 +994,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingTimeRangeSi }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingTimeRangeSizeSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingTimeRangeSizeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingTimeRangeSizeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1013,7 +1018,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingTimeRangeSi } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1029,7 +1034,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableKin "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingVariableSpec"), }, }, }, @@ -1037,11 +1042,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableKin }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingVariableSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1049,28 +1054,28 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableKin Properties: map[string]spec.Schema{ "ConditionalRenderingVariableKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingVariableKind"), }, }, "ConditionalRenderingDataKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingDataKind"), }, }, "ConditionalRenderingTimeRangeSizeKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingTimeRangeSizeKind"), }, }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingDataKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingTimeRangeSizeKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingVariableKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1104,7 +1109,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableSpe } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConstantVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1121,7 +1126,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableKind(ref common "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConstantVariableSpec"), }, }, }, @@ -1129,11 +1134,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableKind(ref common }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConstantVariableSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConstantVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1157,7 +1162,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableSpec(ref common "current": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, "label": { @@ -1191,11 +1196,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableSpec(ref common }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardConversionStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardConversionStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1233,7 +1238,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardConversionStatus(ref common.Ref } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1250,7 +1255,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableKind(ref common.R "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardCustomVariableSpec"), }, }, }, @@ -1258,11 +1263,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableKind(ref common.R }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardCustomVariableSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1286,7 +1291,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableSpec(ref common.R "current": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, "options": { @@ -1296,7 +1301,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableSpec(ref common.R Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, }, @@ -1360,11 +1365,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableSpec(ref common.R }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardDashboardLink(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardDashboardLink(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1464,7 +1469,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDashboardLink(ref common.Refere } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardDataLink(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardDataLink(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1497,7 +1502,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDataLink(ref common.ReferenceCa } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardDataQueryKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardDataQueryKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1505,10 +1510,29 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDataQueryKind(ref common.Refere Properties: map[string]spec.Schema{ "kind": { SchemaProps: spec.SchemaProps{ - Description: "The kind of a DataQueryKind is the datasource type", - Default: "", - Type: []string{"string"}, - Format: "", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "datasource": { + SchemaProps: spec.SchemaProps{ + Description: "New type for datasource reference Not creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.", + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1DataQueryKindDatasource"), }, }, "spec": { @@ -1526,39 +1550,15 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDataQueryKind(ref common.Refere }, }, }, - Required: []string{"kind", "spec"}, + Required: []string{"kind", "group", "version", "spec"}, }, }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1DataQueryKindDatasource"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardDataSourceRef(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "type": { - SchemaProps: spec.SchemaProps{ - Description: "The plugin type-id", - Type: []string{"string"}, - Format: "", - }, - }, - "uid": { - SchemaProps: spec.SchemaProps{ - Description: "Specific datasource instance", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - } -} - -func schema_pkg_apis_dashboard_v2alpha2_DashboardDataTransformerConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardDataTransformerConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1583,7 +1583,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDataTransformerConfig(ref commo "filter": { SchemaProps: spec.SchemaProps{ Description: "Optional frame matcher. When missing it will be applied to all results", - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMatcherConfig"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardMatcherConfig"), }, }, "topic": { @@ -1605,11 +1605,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDataTransformerConfig(ref commo }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMatcherConfig"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardMatcherConfig"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardDatasourceVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1626,7 +1626,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableKind(ref comm "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDatasourceVariableSpec"), }, }, }, @@ -1634,11 +1634,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableKind(ref comm }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDatasourceVariableSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardDatasourceVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1676,7 +1676,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableSpec(ref comm "current": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, "options": { @@ -1686,7 +1686,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableSpec(ref comm Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, }, @@ -1750,11 +1750,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableSpec(ref comm }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardDynamicConfigValue(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardDynamicConfigValue(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1780,7 +1780,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardDynamicConfigValue(ref common.R } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardElementReference(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardElementReference(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1807,7 +1807,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardElementReference(ref common.Ref } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldColor(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardFieldColor(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1843,7 +1843,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldColor(ref common.Reference } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardFieldConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1927,7 +1927,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfig(ref common.Referenc Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap"), }, }, }, @@ -1936,13 +1936,13 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfig(ref common.Referenc "thresholds": { SchemaProps: spec.SchemaProps{ Description: "Map numeric values to states", - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThresholdsConfig"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardThresholdsConfig"), }, }, "color": { SchemaProps: spec.SchemaProps{ Description: "Panel color configuration", - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldColor"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardFieldColor"), }, }, "links": { @@ -1985,11 +1985,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfig(ref common.Referenc }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldColor", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThresholdsConfig", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardFieldColor", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardThresholdsConfig", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfigSource(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardFieldConfigSource(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2000,7 +2000,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfigSource(ref common.Re SchemaProps: spec.SchemaProps{ Description: "Defaults are the options applied to all fields.", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfig"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardFieldConfig"), }, }, "overrides": { @@ -2011,7 +2011,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfigSource(ref common.Re Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2FieldConfigSourceOverrides"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1FieldConfigSourceOverrides"), }, }, }, @@ -2022,11 +2022,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfigSource(ref common.Re }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfig", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2FieldConfigSourceOverrides"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardFieldConfig", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1FieldConfigSourceOverrides"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutItemKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2042,7 +2042,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemKind(ref common.R "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutItemSpec"), }, }, }, @@ -2050,11 +2050,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemKind(ref common.R }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutItemSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutItemSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2092,12 +2092,12 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemSpec(ref common.R SchemaProps: spec.SchemaProps{ Description: "reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardElementReference"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardElementReference"), }, }, "repeat": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRepeatOptions"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRepeatOptions"), }, }, }, @@ -2105,11 +2105,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemSpec(ref common.R }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardElementReference", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRepeatOptions"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardElementReference", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRepeatOptions"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2125,7 +2125,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKind(ref common.Refer "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutSpec"), }, }, }, @@ -2133,11 +2133,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKind(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2145,33 +2145,33 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKindOrAutoGridLayoutK Properties: map[string]spec.Schema{ "GridLayoutKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKind"), }, }, "AutoGridLayoutKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutKind"), }, }, "TabsLayoutKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutKind"), }, }, "RowsLayoutKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutKind"), }, }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2179,33 +2179,33 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKindOrRowsLayoutKindO Properties: map[string]spec.Schema{ "GridLayoutKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKind"), }, }, "RowsLayoutKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutKind"), }, }, "AutoGridLayoutKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutKind"), }, }, "TabsLayoutKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutKind"), }, }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAutoGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardGridLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2218,7 +2218,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutSpec(ref common.Refer Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutItemKind"), }, }, }, @@ -2229,11 +2229,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutSpec(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutItemKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardGroupByVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2247,22 +2247,34 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableKind(ref common. Format: "", }, }, + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "datasource": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1GroupByVariableKindDatasource"), + }, + }, "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGroupByVariableSpec"), }, }, }, - Required: []string{"kind", "spec"}, + Required: []string{"kind", "group", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGroupByVariableSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1GroupByVariableKindDatasource"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardGroupByVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2276,20 +2288,15 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableSpec(ref common. Format: "", }, }, - "datasource": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"), - }, - }, "defaultValue": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, "current": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, "options": { @@ -2299,7 +2306,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableSpec(ref common. Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, }, @@ -2343,11 +2350,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableSpec(ref common. }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardIntervalVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2364,7 +2371,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableKind(ref common "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardIntervalVariableSpec"), }, }, }, @@ -2372,11 +2379,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableKind(ref common }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardIntervalVariableSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardIntervalVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2400,7 +2407,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableSpec(ref common "current": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, "options": { @@ -2410,7 +2417,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableSpec(ref common Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, }, @@ -2475,11 +2482,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableSpec(ref common }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardJSONCodec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardJSONCodec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2490,7 +2497,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardJSONCodec(ref common.ReferenceC } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardLibraryPanelKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2506,7 +2513,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKind(ref common.Ref "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKindSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardLibraryPanelKindSpec"), }, }, }, @@ -2514,11 +2521,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKind(ref common.Ref }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKindSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardLibraryPanelKindSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKindSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardLibraryPanelKindSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2543,7 +2550,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKindSpec(ref common "libraryPanel": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelRef"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardLibraryPanelRef"), }, }, }, @@ -2551,11 +2558,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKindSpec(ref common }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelRef"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardLibraryPanelRef"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelRef(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardLibraryPanelRef(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2585,7 +2592,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelRef(ref common.Refe } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2618,7 +2625,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardList(ref common.ReferenceCallba Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.Dashboard"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.Dashboard"), }, }, }, @@ -2629,11 +2636,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardList(ref common.ReferenceCallba }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.Dashboard", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.Dashboard", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardMatcherConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardMatcherConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2662,7 +2669,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardMatcherConfig(ref common.Refere } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardMetadata(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardMetadata(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2758,7 +2765,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardMetadata(ref common.ReferenceCa } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardMetricFindValue(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardMetricFindValue(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2774,7 +2781,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardMetricFindValue(ref common.Refe }, "value": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrFloat64"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStringOrFloat64"), }, }, "group": { @@ -2794,11 +2801,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardMetricFindValue(ref common.Refe }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrFloat64"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStringOrFloat64"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardPanelKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2814,7 +2821,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelKind(ref common.ReferenceC "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelSpec"), }, }, }, @@ -2822,11 +2829,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelKind(ref common.ReferenceC }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelKindOrLibraryPanelKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardPanelKindOrLibraryPanelKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2834,23 +2841,23 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelKindOrLibraryPanelKind(ref Properties: map[string]spec.Schema{ "PanelKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelKind"), }, }, "LibraryPanelKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardLibraryPanelKind"), }, }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQueryKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardPanelQueryKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2866,7 +2873,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQueryKind(ref common.Refer "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQuerySpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelQuerySpec"), }, }, }, @@ -2874,11 +2881,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQueryKind(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQuerySpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelQuerySpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQuerySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardPanelQuerySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2887,12 +2894,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQuerySpec(ref common.Refer "query": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind"), - }, - }, - "datasource": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataQueryKind"), }, }, "refId": { @@ -2914,11 +2916,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQuerySpec(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataQueryKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardPanelSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -2952,7 +2954,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelSpec(ref common.ReferenceC Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataLink"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataLink"), }, }, }, @@ -2961,13 +2963,13 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelSpec(ref common.ReferenceC "data": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryGroupKind"), }, }, "vizConfig": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVizConfigKind"), }, }, "transparent": { @@ -2981,11 +2983,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelSpec(ref common.ReferenceC }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVizConfigKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardQueryGroupKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3001,7 +3003,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupKind(ref common.Refer "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryGroupSpec"), }, }, }, @@ -3009,11 +3011,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupKind(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryGroupSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardQueryGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3026,7 +3028,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupSpec(ref common.Refer Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQueryKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelQueryKind"), }, }, }, @@ -3039,7 +3041,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupSpec(ref common.Refer Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTransformationKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTransformationKind"), }, }, }, @@ -3048,7 +3050,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupSpec(ref common.Refer "queryOptions": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryOptionsSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryOptionsSpec"), }, }, }, @@ -3056,11 +3058,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupSpec(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryOptionsSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTransformationKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryOptionsSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTransformationKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryOptionsSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardQueryOptionsSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3114,7 +3116,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryOptionsSpec(ref common.Ref } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardQueryVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3131,7 +3133,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableKind(ref common.Re "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryVariableSpec"), }, }, }, @@ -3139,11 +3141,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableKind(ref common.Re }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryVariableSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3151,53 +3153,53 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableKindOrTextVariable Properties: map[string]spec.Schema{ "QueryVariableKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryVariableKind"), }, }, "TextVariableKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTextVariableKind"), }, }, "ConstantVariableKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConstantVariableKind"), }, }, "DatasourceVariableKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDatasourceVariableKind"), }, }, "IntervalVariableKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardIntervalVariableKind"), }, }, "CustomVariableKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardCustomVariableKind"), }, }, "GroupByVariableKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGroupByVariableKind"), }, }, "AdhocVariableKind": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdhocVariableKind"), }, }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConstantVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardCustomVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDatasourceVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGroupByVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardIntervalVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTextVariableKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardQueryVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3214,7 +3216,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableSpec(ref common.Re "current": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, "label": { @@ -3250,15 +3252,10 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableSpec(ref common.Re Format: "", }, }, - "datasource": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"), - }, - }, "query": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataQueryKind"), }, }, "regex": { @@ -3288,7 +3285,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableSpec(ref common.Re Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, }, @@ -3334,7 +3331,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableSpec(ref common.Re Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, }, @@ -3351,11 +3348,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableSpec(ref common.Re }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardRangeMap(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardRangeMap(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3373,7 +3370,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRangeMap(ref common.ReferenceCa SchemaProps: spec.SchemaProps{ Description: "Range to match against and the result to apply when the value is within the range", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RangeMapOptions"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1RangeMapOptions"), }, }, }, @@ -3381,11 +3378,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRangeMap(ref common.ReferenceCa }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RangeMapOptions"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1RangeMapOptions"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardRegexMap(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardRegexMap(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3403,7 +3400,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRegexMap(ref common.ReferenceCa SchemaProps: spec.SchemaProps{ Description: "Regular expression to match against and the result to apply when the value matches the regex", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RegexMapOptions"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1RegexMapOptions"), }, }, }, @@ -3411,11 +3408,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRegexMap(ref common.ReferenceCa }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RegexMapOptions"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1RegexMapOptions"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3454,7 +3451,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRepeatOptions(ref common.Refere } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardRowRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardRowRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3481,7 +3478,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRowRepeatOptions(ref common.Ref } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardRowsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3497,7 +3494,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutKind(ref common.Refer "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutSpec"), }, }, }, @@ -3505,11 +3502,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutKind(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardRowsLayoutRowKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3525,7 +3522,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowKind(ref common.Re "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutRowSpec"), }, }, }, @@ -3533,11 +3530,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowKind(ref common.Re }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutRowSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardRowsLayoutRowSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3569,17 +3566,17 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowSpec(ref common.Re }, "conditionalRendering": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingGroupKind"), }, }, "repeat": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowRepeatOptions"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowRepeatOptions"), }, }, "layout": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind"), }, }, }, @@ -3587,11 +3584,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowSpec(ref common.Re }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowRepeatOptions"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowRepeatOptions"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardRowsLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3604,7 +3601,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutSpec(ref common.Refer Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutRowKind"), }, }, }, @@ -3615,11 +3612,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutSpec(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRowsLayoutRowKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3632,7 +3629,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardSpec(ref common.ReferenceCallba Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQueryKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQueryKind"), }, }, }, @@ -3667,7 +3664,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardSpec(ref common.ReferenceCallba Allows: true, Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKindOrLibraryPanelKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelKindOrLibraryPanelKind"), }, }, }, @@ -3675,7 +3672,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardSpec(ref common.ReferenceCallba }, "layout": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind"), }, }, "links": { @@ -3686,7 +3683,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardSpec(ref common.ReferenceCallba Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDashboardLink"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDashboardLink"), }, }, }, @@ -3732,7 +3729,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardSpec(ref common.ReferenceCallba "timeSettings": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeSettingsSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTimeSettingsSpec"), }, }, "title": { @@ -3750,7 +3747,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardSpec(ref common.ReferenceCallba Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind"), }, }, }, @@ -3761,11 +3758,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardSpec(ref common.ReferenceCallba }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDashboardLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKindOrLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeSettingsSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDashboardLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardPanelKindOrLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTimeSettingsSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardSpecialValueMap(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardSpecialValueMap(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3782,7 +3779,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardSpecialValueMap(ref common.Refe "options": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2SpecialValueMapOptions"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1SpecialValueMapOptions"), }, }, }, @@ -3790,11 +3787,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardSpecialValueMap(ref common.Refe }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2SpecialValueMapOptions"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardV2beta1SpecialValueMapOptions"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3803,18 +3800,18 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardStatus(ref common.ReferenceCall "conversion": { SchemaProps: spec.SchemaProps{ Description: "Optional conversion status.", - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConversionStatus"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConversionStatus"), }, }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConversionStatus"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConversionStatus"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardStringOrArrayOfString(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardStringOrArrayOfString(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3846,7 +3843,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardStringOrArrayOfString(ref commo } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardStringOrFloat64(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardStringOrFloat64(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3870,7 +3867,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardStringOrFloat64(ref common.Refe } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardTabRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardTabRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3897,7 +3894,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTabRepeatOptions(ref common.Ref } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardTabsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3913,7 +3910,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutKind(ref common.Refer "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutSpec"), }, }, }, @@ -3921,11 +3918,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutKind(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardTabsLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3938,7 +3935,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutSpec(ref common.Refer Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutTabKind"), }, }, }, @@ -3949,11 +3946,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutSpec(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutTabKind"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardTabsLayoutTabKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3969,7 +3966,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabKind(ref common.Re "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutTabSpec"), }, }, }, @@ -3977,11 +3974,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabKind(ref common.Re }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabsLayoutTabSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardTabsLayoutTabSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3995,17 +3992,17 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabSpec(ref common.Re }, "layout": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind"), }, }, "conditionalRendering": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingGroupKind"), }, }, "repeat": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabRepeatOptions"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabRepeatOptions"), }, }, }, @@ -4013,11 +4010,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabSpec(ref common.Re }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabRepeatOptions"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTabRepeatOptions"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardTextVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4034,7 +4031,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableKind(ref common.Ref "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTextVariableSpec"), }, }, }, @@ -4042,11 +4039,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableKind(ref common.Ref }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTextVariableSpec"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardTextVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4063,7 +4060,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableSpec(ref common.Ref "current": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"), }, }, "query": { @@ -4104,11 +4101,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableSpec(ref common.Ref }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVariableOption"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardThreshold(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardThreshold(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4135,7 +4132,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardThreshold(ref common.ReferenceC } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardThresholdsConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardThresholdsConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4155,7 +4152,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardThresholdsConfig(ref common.Ref Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThreshold"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardThreshold"), }, }, }, @@ -4166,11 +4163,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardThresholdsConfig(ref common.Ref }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThreshold"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardThreshold"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardTimeRangeOption(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardTimeRangeOption(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4204,7 +4201,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTimeRangeOption(ref common.Refe } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardTimeSettingsSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardTimeSettingsSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4265,7 +4262,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTimeSettingsSpec(ref common.Ref Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeRangeOption"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTimeRangeOption"), }, }, }, @@ -4306,11 +4303,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTimeSettingsSpec(ref common.Ref }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeRangeOption"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardTimeRangeOption"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardTransformationKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardTransformationKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4327,7 +4324,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTransformationKind(ref common.R "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataTransformerConfig"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataTransformerConfig"), }, }, }, @@ -4335,11 +4332,47 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardTransformationKind(ref common.R }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataTransformerConfig"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataTransformerConfig"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2FieldConfigSourceOverrides(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1AdhocVariableKindDatasource(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{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1DataQueryKindDatasource(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{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1FieldConfigSourceOverrides(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4348,7 +4381,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2FieldConfigSourceOverri "matcher": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMatcherConfig"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardMatcherConfig"), }, }, "properties": { @@ -4358,7 +4391,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2FieldConfigSourceOverri Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDynamicConfigValue"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDynamicConfigValue"), }, }, }, @@ -4369,11 +4402,29 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2FieldConfigSourceOverri }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDynamicConfigValue", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMatcherConfig"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDynamicConfigValue", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardMatcherConfig"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RangeMapOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1GroupByVariableKindDatasource(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{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1RangeMapOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4397,7 +4448,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RangeMapOptions(ref com SchemaProps: spec.SchemaProps{ Description: "Config to apply when the value is within the range", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMappingResult"), }, }, }, @@ -4405,11 +4456,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RangeMapOptions(ref com }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMappingResult"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RegexMapOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1RegexMapOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4427,7 +4478,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RegexMapOptions(ref com SchemaProps: spec.SchemaProps{ Description: "Config to apply when the value matches the regex", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMappingResult"), }, }, }, @@ -4435,11 +4486,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RegexMapOptions(ref com }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMappingResult"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2SpecialValueMapOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1SpecialValueMapOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4457,7 +4508,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2SpecialValueMapOptions( SchemaProps: spec.SchemaProps{ Description: "Config to apply when the value matches the special value", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMappingResult"), }, }, }, @@ -4465,11 +4516,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2SpecialValueMapOptions( }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMappingResult"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardValueMap(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardValueMap(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4492,7 +4543,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardValueMap(ref common.ReferenceCa Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMappingResult"), }, }, }, @@ -4503,11 +4554,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardValueMap(ref common.ReferenceCa }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMappingResult"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4515,33 +4566,33 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardValueMapOrRangeMapOrRegexMapOrS Properties: map[string]spec.Schema{ "ValueMap": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMap"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMap"), }, }, "RangeMap": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRangeMap"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRangeMap"), }, }, "RegexMap": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRegexMap"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRegexMap"), }, }, "SpecialValueMap": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpecialValueMap"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardSpecialValueMap"), }, }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRangeMap", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRegexMap", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpecialValueMap", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMap"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRangeMap", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardRegexMap", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardSpecialValueMap", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardValueMap"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardValueMappingResult(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardValueMappingResult(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4582,7 +4633,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardValueMappingResult(ref common.R } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardVariableOption(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardVariableOption(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4599,13 +4650,13 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardVariableOption(ref common.Refer "text": { SchemaProps: spec.SchemaProps{ Description: "Text to be displayed for the option", - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrArrayOfString"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStringOrArrayOfString"), }, }, "value": { SchemaProps: spec.SchemaProps{ Description: "Value of the option", - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrArrayOfString"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStringOrArrayOfString"), }, }, }, @@ -4613,11 +4664,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardVariableOption(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrArrayOfString"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStringOrArrayOfString"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardVersionInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardVersionInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4667,7 +4718,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardVersionInfo(ref common.Referenc } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardVersionList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardVersionList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4700,7 +4751,7 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardVersionList(ref common.Referenc Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVersionInfo"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVersionInfo"), }, }, }, @@ -4711,11 +4762,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardVersionList(ref common.Referenc }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVersionInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVersionInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardVizConfigKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardVizConfigKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4723,41 +4774,48 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardVizConfigKind(ref common.Refere Properties: map[string]spec.Schema{ "kind": { SchemaProps: spec.SchemaProps{ - Description: "The kind of a VizConfigKind is the plugin ID", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "group": { + SchemaProps: spec.SchemaProps{ + Description: "The group is the plugin ID", Default: "", Type: []string{"string"}, Format: "", }, }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigSpec"), - }, - }, - }, - Required: []string{"kind", "spec"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigSpec"}, - } -} - -func schema_pkg_apis_dashboard_v2alpha2_DashboardVizConfigSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "--- Kinds ---", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "pluginVersion": { + "version": { SchemaProps: spec.SchemaProps{ Default: "", Type: []string{"string"}, Format: "", }, }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVizConfigSpec"), + }, + }, + }, + Required: []string{"kind", "group", "version", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardVizConfigSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2beta1_DashboardVizConfigSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "--- Kinds ---", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ "options": { SchemaProps: spec.SchemaProps{ Type: []string{"object"}, @@ -4775,19 +4833,19 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardVizConfigSpec(ref common.Refere "fieldConfig": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfigSource"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardFieldConfigSource"), }, }, }, - Required: []string{"pluginVersion", "options", "fieldConfig"}, + Required: []string{"options", "fieldConfig"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfigSource"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardFieldConfigSource"}, } } -func schema_pkg_apis_dashboard_v2alpha2_DashboardWithAccessInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_DashboardWithAccessInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4818,19 +4876,19 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardWithAccessInfo(ref common.Refer SchemaProps: spec.SchemaProps{ Description: "Spec is the spec of the Dashboard", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardSpec"), }, }, "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStatus"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStatus"), }, }, "access": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAccess"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAccess"), }, }, }, @@ -4838,11 +4896,11 @@ func schema_pkg_apis_dashboard_v2alpha2_DashboardWithAccessInfo(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAccess", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAccess", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } -func schema_pkg_apis_dashboard_v2alpha2_LibraryPanel(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_LibraryPanel(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4873,13 +4931,13 @@ func schema_pkg_apis_dashboard_v2alpha2_LibraryPanel(ref common.ReferenceCallbac SchemaProps: spec.SchemaProps{ Description: "Panel properties", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelSpec"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.LibraryPanelSpec"), }, }, "status": { SchemaProps: spec.SchemaProps{ Description: "Status will show errors", - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelStatus"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.LibraryPanelStatus"), }, }, }, @@ -4887,11 +4945,11 @@ func schema_pkg_apis_dashboard_v2alpha2_LibraryPanel(ref common.ReferenceCallbac }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.LibraryPanelSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.LibraryPanelStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } -func schema_pkg_apis_dashboard_v2alpha2_LibraryPanelList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_LibraryPanelList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4924,7 +4982,7 @@ func schema_pkg_apis_dashboard_v2alpha2_LibraryPanelList(ref common.ReferenceCal Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanel"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.LibraryPanel"), }, }, }, @@ -4935,11 +4993,11 @@ func schema_pkg_apis_dashboard_v2alpha2_LibraryPanelList(ref common.ReferenceCal }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanel", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.LibraryPanel", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } -func schema_pkg_apis_dashboard_v2alpha2_LibraryPanelSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_LibraryPanelSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -5019,7 +5077,7 @@ func schema_pkg_apis_dashboard_v2alpha2_LibraryPanelSpec(ref common.ReferenceCal } } -func schema_pkg_apis_dashboard_v2alpha2_LibraryPanelStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_LibraryPanelStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -5054,7 +5112,7 @@ func schema_pkg_apis_dashboard_v2alpha2_LibraryPanelStatus(ref common.ReferenceC } } -func schema_pkg_apis_dashboard_v2alpha2_VersionsQueryOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2beta1_VersionsQueryOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi_violation_exceptions.list new file mode 100644 index 00000000000..d516d5c8748 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi_violation_exceptions.list @@ -0,0 +1,67 @@ +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardAdHocFilterWithLabels,ValueLabels +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardAdHocFilterWithLabels,Values +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardAdhocVariableSpec,BaseFilters +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardAdhocVariableSpec,DefaultKeys +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardAdhocVariableSpec,Filters +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardAnnotationPanelFilter,Ids +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardAutoGridLayoutSpec,Items +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardConditionalRenderingGroupSpec,Items +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardCustomVariableSpec,Options +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardDashboardLink,Tags +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardDatasourceVariableSpec,Options +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardFieldConfig,Links +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardFieldConfig,Mappings +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardFieldConfigSource,Overrides +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardGridLayoutSpec,Items +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardGroupByVariableSpec,Options +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardIntervalVariableSpec,Options +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardMetadata,Finalizers +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardPanelSpec,Links +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryGroupSpec,Queries +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryGroupSpec,Transformations +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryVariableSpec,Options +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryVariableSpec,StaticOptions +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardRowsLayoutSpec,Rows +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardSpec,Annotations +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardSpec,Links +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardSpec,Tags +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardSpec,Variables +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardStringOrArrayOfString,ArrayOfString +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardTabsLayoutSpec,Tabs +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardThresholdsConfig,Steps +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardTimeSettingsSpec,AutoRefreshIntervals +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardTimeSettingsSpec,QuickRanges +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardV2beta1FieldConfigSourceOverrides,Properties +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,LibraryPanelStatus,Warnings +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingDataKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingTimeRangeSizeKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,AutoGridLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,GridLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,RowsLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,TabsLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,AutoGridLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,GridLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,RowsLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,TabsLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardIntervalVariableSpec,AutoCount +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardIntervalVariableSpec,AutoMin +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardPanelKindOrLibraryPanelKind,LibraryPanelKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardPanelKindOrLibraryPanelKind,PanelKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,AdhocVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,ConstantVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,CustomVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,DatasourceVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,GroupByVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,IntervalVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,QueryVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,TextVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardStringOrArrayOfString,ArrayOfString +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardStringOrArrayOfString,String +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardStringOrFloat64,Float64 +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardStringOrFloat64,String +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,RangeMap +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,RegexMap +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,SpecialValueMap +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,ValueMap +API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardList,ListMeta diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index 591ce095303..9154093d2a8 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -15,7 +15,7 @@ import ( v0alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" v1beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" v2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - v2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + v2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" ) var appManifestData = app.ManifestData{ @@ -62,7 +62,7 @@ var appManifestData = app.ManifestData{ }, { - Name: "v2alpha2", + Name: "v2beta1", Served: true, Kinds: []app.ManifestVersionKind{ { @@ -88,7 +88,7 @@ var kindVersionToGoType = map[string]resource.Kind{ "Dashboard/v0alpha1": v0alpha1.DashboardKind(), "Dashboard/v1beta1": v1beta1.DashboardKind(), "Dashboard/v2alpha1": v2alpha1.DashboardKind(), - "Dashboard/v2alpha2": v2alpha2.DashboardKind(), + "Dashboard/v2beta1": v2beta1.DashboardKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. diff --git a/apps/dashboard/pkg/migration/conversion/conversion.go b/apps/dashboard/pkg/migration/conversion/conversion.go index 8fb35a45dbe..fb78859fc3c 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion.go +++ b/apps/dashboard/pkg/migration/conversion/conversion.go @@ -7,7 +7,7 @@ import ( dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" ) func RegisterConversions(s *runtime.Scheme) error { @@ -22,8 +22,8 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2alpha2.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V0_to_V2alpha2(a.(*dashv0.Dashboard), b.(*dashv2alpha2.Dashboard), scope) + if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2beta1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V0_to_V2beta1(a.(*dashv0.Dashboard), b.(*dashv2beta1.Dashboard), scope) }); err != nil { return err } @@ -39,8 +39,8 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2alpha2.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V1_to_V2alpha2(a.(*dashv1.Dashboard), b.(*dashv2alpha2.Dashboard), scope) + if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2beta1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V1_to_V2beta1(a.(*dashv1.Dashboard), b.(*dashv2beta1.Dashboard), scope) }); err != nil { return err } @@ -56,25 +56,25 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddConversionFunc((*dashv2alpha1.Dashboard)(nil), (*dashv2alpha2.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2alpha1_to_V2alpha2(a.(*dashv2alpha1.Dashboard), b.(*dashv2alpha2.Dashboard), scope) + if err := s.AddConversionFunc((*dashv2alpha1.Dashboard)(nil), (*dashv2beta1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2alpha1_to_V2beta1(a.(*dashv2alpha1.Dashboard), b.(*dashv2beta1.Dashboard), scope) }); err != nil { return err } - // v2alpha2 conversions - if err := s.AddConversionFunc((*dashv2alpha2.Dashboard)(nil), (*dashv0.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2alpha2_to_V0(a.(*dashv2alpha2.Dashboard), b.(*dashv0.Dashboard), scope) + // v2beta1 conversions + if err := s.AddConversionFunc((*dashv2beta1.Dashboard)(nil), (*dashv0.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2beta1_to_V0(a.(*dashv2beta1.Dashboard), b.(*dashv0.Dashboard), scope) }); err != nil { return err } - if err := s.AddConversionFunc((*dashv2alpha2.Dashboard)(nil), (*dashv1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2alpha2_to_V1(a.(*dashv2alpha2.Dashboard), b.(*dashv1.Dashboard), scope) + if err := s.AddConversionFunc((*dashv2beta1.Dashboard)(nil), (*dashv1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2beta1_to_V1(a.(*dashv2beta1.Dashboard), b.(*dashv1.Dashboard), scope) }); err != nil { return err } - if err := s.AddConversionFunc((*dashv2alpha2.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2alpha2_to_V2alpha1(a.(*dashv2alpha2.Dashboard), b.(*dashv2alpha1.Dashboard), scope) + if err := s.AddConversionFunc((*dashv2beta1.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2beta1_to_V2alpha1(a.(*dashv2beta1.Dashboard), b.(*dashv2alpha1.Dashboard), scope) }); err != nil { return err } diff --git a/apps/dashboard/pkg/migration/conversion/conversion_test.go b/apps/dashboard/pkg/migration/conversion/conversion_test.go index 0efd0ab6205..ce59a99f53b 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_test.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_test.go @@ -1,7 +1,10 @@ package conversion import ( + "encoding/json" "fmt" + "os" + "path/filepath" "strings" "testing" @@ -9,10 +12,11 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "github.com/grafana/grafana/apps/dashboard/pkg/apis" dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" "github.com/grafana/grafana/apps/dashboard/pkg/migration" "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" @@ -27,7 +31,7 @@ func TestConversionMatrixExist(t *testing.T) { &dashv0.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV0"}}}, &dashv1.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV1"}}}, &dashv2alpha1.Dashboard{Spec: dashv2alpha1.DashboardSpec{Title: "dashboardV2alpha1"}}, - &dashv2alpha2.Dashboard{Spec: dashv2alpha2.DashboardSpec{Title: "dashboardV2alpha2"}}, + &dashv2beta1.Dashboard{Spec: dashv2beta1.DashboardSpec{Title: "dashboardV2beta1"}}, } scheme := runtime.NewScheme() @@ -69,3 +73,150 @@ func TestDeepCopyValid(t *testing.T) { metaCopy.SetFolder("XYZ") require.Equal(t, "f1", meta1.GetFolder()) // 💣💣💣 } + +func TestDashboardConversionToAllVersions(t *testing.T) { + // Initialize the migrator with a test data source provider + migration.Initialize(testutil.GetTestProvider()) + + // Set up conversion scheme + scheme := runtime.NewScheme() + err := RegisterConversions(scheme) + require.NoError(t, err) + + // Read all files from input directory + files, err := os.ReadDir(filepath.Join("testdata", "input")) + require.NoError(t, err, "Failed to read input directory") + + for _, file := range files { + if file.IsDir() { + continue + } + + t.Run(fmt.Sprintf("Convert_%s", file.Name()), func(t *testing.T) { + // Read input dashboard file + inputFile := filepath.Join("testdata", "input", file.Name()) + // ignore gosec G304 as this function is only used in the test process + //nolint:gosec + inputData, err := os.ReadFile(inputFile) + require.NoError(t, err, "Failed to read input file") + + // Parse the input dashboard to get its version + var rawDash map[string]interface{} + err = json.Unmarshal(inputData, &rawDash) + require.NoError(t, err, "Failed to unmarshal dashboard JSON") + + // Extract apiVersion + apiVersion, ok := rawDash["apiVersion"].(string) + require.True(t, ok, "apiVersion not found or not a string") + + // Parse group and version from apiVersion (format: "group/version") + parts := strings.Split(apiVersion, "/") + require.Equal(t, 2, len(parts), "apiVersion should be in format 'group/version'") + sourceVersion := parts[1] + + // Create source object based on version + var sourceDash v1.Object + switch sourceVersion { + case "v0alpha1": + var dash dashv0.Dashboard + err = json.Unmarshal(inputData, &dash) + sourceDash = &dash + case "v1beta1": + var dash dashv1.Dashboard + err = json.Unmarshal(inputData, &dash) + sourceDash = &dash + case "v2alpha1": + var dash dashv2alpha1.Dashboard + err = json.Unmarshal(inputData, &dash) + sourceDash = &dash + case "v2beta1": + var dash dashv2beta1.Dashboard + err = json.Unmarshal(inputData, &dash) + sourceDash = &dash + default: + t.Fatalf("Unsupported source version: %s", sourceVersion) + } + require.NoError(t, err, "Failed to unmarshal dashboard into typed object") + + // Ensure output directory exists + outDir := filepath.Join("testdata", "output") + // ignore gosec G301 as this function is only used in the test process + //nolint:gosec + err = os.MkdirAll(outDir, 0755) + require.NoError(t, err, "Failed to create output directory") + + // Get target versions from the dashboard manifest + manifest := apis.LocalManifest() + targetVersions := make(map[string]runtime.Object) + + // Get original filename without extension + originalName := strings.TrimSuffix(file.Name(), ".json") + + // Get all Dashboard versions from the manifest + for _, kind := range manifest.ManifestData.Kinds() { + if kind.Kind == "Dashboard" { + for _, version := range kind.Versions { + // Skip converting to the same version + if version.VersionName == sourceVersion { + continue + } + + filename := fmt.Sprintf("%s.%s.json", originalName, version.VersionName) + + // Create target object based on version + switch version.VersionName { + case "v0alpha1": + targetVersions[filename] = &dashv0.Dashboard{} + case "v1beta1": + targetVersions[filename] = &dashv1.Dashboard{} + case "v2alpha1": + targetVersions[filename] = &dashv2alpha1.Dashboard{} + case "v2beta1": + targetVersions[filename] = &dashv2beta1.Dashboard{} + default: + t.Logf("Unknown version %s, skipping", version.VersionName) + } + } + break + } + } + + // Convert to each target version + for filename, target := range targetVersions { + t.Run(fmt.Sprintf("Convert_to_%s", filename), func(t *testing.T) { + // Create a copy of the input dashboard for conversion + inputCopy := sourceDash.(runtime.Object).DeepCopyObject() + + // Convert to target version + err = scheme.Convert(inputCopy, target, nil) + require.NoError(t, err, "Conversion failed for %s", filename) + + // Test the changes in the conversion result + testConversion(t, target.(v1.Object), filename, outDir) + }) + } + }) + } +} + +func testConversion(t *testing.T, convertedDash v1.Object, filename, outputDir string) { + t.Helper() + + outPath := filepath.Join(outputDir, filename) + outBytes, err := json.MarshalIndent(convertedDash, "", " ") + require.NoError(t, err, "failed to marshal converted 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 %s", outPath) + t.Logf("✓ Created new output file: %s", filename) + return + } + + // ignore gosec G304 as this function is only used in the test process + //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) + t.Logf("✓ Conversion to %s matches existing file", filename) +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.complete.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.complete.json new file mode 100644 index 00000000000..277a8b2ebc2 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.complete.json @@ -0,0 +1,462 @@ +{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": { + "name": "test-v2alpha1-complete", + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": { + "title": "Test: Complete V2alpha1 Dashboard Example", + "description": "This dashboard demonstrates all features that need to be converted from v2alpha1 to v2beta1", + "editable": true, + "liveNow": true, + "preload": true, + "cursorSync": "Tooltip", + "tags": ["test", "example", "migration"], + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": true, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "query": { + "kind": "grafana", + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Prometheus Annotations", + "query": { + "kind": "prometheus", + "spec": { + "expr": "changes(process_start_time_seconds[1m])", + "refId": "Anno" + } + } + } + } + ], + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "prometheus_query", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "refresh": "time", + "regex": "", + "sort": "alphabetical", + "multi": true, + "includeAll": true, + "current": { + "text": "All", + "value": ["$__all"] + }, + "hide": "dontHide", + "label": "Prometheus Query", + "description": "Shows all up metrics", + "skipUrlSync": false, + "definition": "up" + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "text_var", + "label": "Text Variable", + "description": "A simple text variable", + "query": "server1,server2,server3", + "current": { + "selected": true, + "text": "server1", + "value": "server1" + }, + "hide": "dontHide", + "skipUrlSync": false + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "constant_var", + "label": "Constant", + "description": "A constant value", + "query": "production", + "current": { + "selected": true, + "text": "production", + "value": "production" + }, + "hide": "dontHide", + "skipUrlSync": true + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "ds_var", + "label": "Datasource", + "description": "Select a datasource", + "pluginId": "prometheus", + "refresh": "load", + "regex": "/^gdev-/", + "current": { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + }, + "options": [ + { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + } + ], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval", + "label": "Interval", + "description": "Time interval selection", + "query": "1m,5m,10m,30m,1h,6h,12h,1d", + "current": { + "selected": true, + "text": "5m", + "value": "5m" + }, + "options": [ + { + "text": "1m", + "value": "1m" + }, + { + "text": "5m", + "value": "5m" + }, + { + "text": "10m", + "value": "10m" + }, + { + "text": "30m", + "value": "30m" + }, + { + "text": "1h", + "value": "1h" + }, + { + "text": "6h", + "value": "6h" + }, + { + "text": "12h", + "value": "12h" + }, + { + "text": "1d", + "value": "1d" + } + ], + "auto": true, + "auto_min": "10s", + "auto_count": 30, + "refresh": "load", + "hide": "dontHide", + "skipUrlSync": false + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_var", + "label": "Custom Options", + "description": "Custom multi-value variable", + "query": "prod : Production, staging : Staging, dev : Development", + "current": { + "text": ["Production"], + "value": ["prod"] + }, + "options": [ + { + "text": "Production", + "value": "prod" + }, + { + "text": "Staging", + "value": "staging" + }, + { + "text": "Development", + "value": "dev" + } + ], + "multi": true, + "includeAll": true, + "allValue": "*", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "GroupByVariable", + "spec": { + "name": "group_by", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "current": { + "text": "instance", + "value": "instance" + }, + "description": "Group metrics by label", + "hide": "dontHide", + "label": "Group By" + } + }, + { + "kind": "AdhocVariable", + "spec": { + "name": "filters", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "baseFilters": [ + { + "key": "job", + "operator": "=", + "value": "grafana", + "condition": "AND" + } + ], + "filters": [], + "hide": "dontHide", + "label": "Filters", + "defaultKeys": [ + { + "text": "job", + "value": "job", + "expandable": true + }, + { + "text": "instance", + "value": "instance", + "expandable": true + } + ] + } + } + ], + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Conditional Rendering", + "description": "This panel demonstrates conditional rendering features", + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "refId": "A", + "datasource": { + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up{job=\"grafana\"}" + } + } + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "mode": "reduceFields", + "includeTimeField": false, + "reducers": ["mean"] + } + } + } + ] + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 1 + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Down", + "color": "red" + }, + "1": { + "text": "Up", + "color": "green" + } + } + } + ] + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "Row", + "spec": { + "title": "Conditional Row", + "collapse": false, + "hideHeader": false, + "fillScreen": false, + "conditionalRendering": { + "kind": "ConditionalRenderingGroup", + "spec": { + "visibility": "show", + "condition": "and", + "items": [ + { + "kind": "ConditionalRenderingVariable", + "spec": { + "variable": "group_by", + "operator": "includes", + "value": "instance" + } + }, + { + "kind": "ConditionalRenderingData", + "spec": { + "value": true + } + }, + { + "kind": "ConditionalRenderingTimeRangeSize", + "spec": { + "value": "1h" + } + } + ] + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + } + } + } + ] + } + }, + "timeSettings": { + "timezone": "browser", + "weekStart": "monday", + "fiscalYearStartMonth": 0, + "autoRefresh": "10s", + "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "from": "now-6h", + "to": "now", + "hideTimepicker": false + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.ds-data-query.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.ds-data-query.json new file mode 100644 index 00000000000..408a3cc9873 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.ds-data-query.json @@ -0,0 +1,1001 @@ +{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": { + "name": "test-v2alpha1-annotations", + "labels": {}, + "annotations": {} + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": true, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "query": { + "kind": "grafana", + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "query": { + "kind": "prometheus", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "prom-annos", + "query": { + "kind": "prometheus", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "no-ds-prom-annos", + "query": { + "kind": "prometheus", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PBBCEC2D313BC06C3" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "query": { + "kind": "grafana-postgresql-datasource", + "spec": { + "editorMode": "builder", + "format": "table", + "lines": 10, + "rawSql": "", + "refId": "Anno", + "scenarioId": "annotations", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "datasource": { + "type": "elasticsearch", + "uid": "gdev-elasticsearch" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "legacyOptions": { + "tagsField": "asd", + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" + }, + "name": "elastic - annos", + "query": { + "kind": "elasticsearch", + "spec": { + "lines": 10, + "query": "test query", + "refId": "Anno", + "scenarioId": "annotations" + } + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "datasource": { + "uid": "gdev-testdata" + }, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 1, + "links": [], + "title": "Simple timeseries (WITH DS REF)", + "vizConfig": { + "kind": "timeseries", + "spec": { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 4 + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "Simple stat (NO DS REF)", + "vizConfig": { + "kind": "stat", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 3, + "links": [], + "title": "Panel with NO REF to gdev-prometheus", + "vizConfig": { + "kind": "timeseries", + "spec": { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "datasource": { + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 4, + "links": [], + "title": "Panel with ref to gdev-prometheus", + "vizConfig": { + "kind": "timeseries", + "spec": { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "datasource": { + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + }, + "version": "v0" + }, + "refId": "A" + } + }, + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "datasource": { + "uid": "gdev-testdata" + }, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {}, + "version": "v0" + }, + "refId": "B" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 5, + "links": [], + "title": "Mixed DS WITH REFS", + "vizConfig": { + "kind": "timeseries", + "spec": { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + }, + "version": "v0" + }, + "refId": "A" + } + }, + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {}, + "version": "v0" + }, + "refId": "B" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 6, + "links": [], + "title": "Mixed DS WITHOUT REFS", + "vizConfig": { + "kind": "timeseries", + "spec": { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "columnWidthMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ], + "maxColumnCount": 3, + "rowHeightMode": "standard" + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "autoRefresh": "", + "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "fiscalYearStartMonth": 0, + "from": "now-6h", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "Test: V2alpha1 dashboard with annotations", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "datasource": { + "uid": "gdev-prometheus" + }, + "enable": true, + "hide": "dontHide", + "name": "variable-ds-prometheus", + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + } + } + }, + { + "kind": "QueryVariable", + "spec": { + "enable": true, + "hide": "dontHide", + "name": "variable-no-ds", + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "csv_metric_values", + "csv": "1,2,3,4" + } + } + } + }, + { + "kind": "QueryVariable", + "spec": { + "enable": true, + "hide": "dontHide", + "name": "variable-no-ds-empty-query", + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + } + } + }, + { + "kind": "QueryVariable", + "spec": { + "enable": true, + "hide": "dontHide", + "name": "variable-no-default-ds", + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + } + } + } + ] + }, + "status": {} +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.groupby-adhoc-vars.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.groupby-adhoc-vars.json new file mode 100644 index 00000000000..65c2b91ae72 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.groupby-adhoc-vars.json @@ -0,0 +1,73 @@ +{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars" + }, + "spec": { + "title": "Test: V2alpha1 dashboard with group by and adhoc variables", + "variables": [ + { + "kind": "GroupByVariable", + "spec": { + "current": { + "text": "text7", + "value": "value7" + }, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "description": "A group by variable", + "hide": "dontHide", + "label": "Group By Variable" + } + }, + { + "kind": "AdhocVariable", + "spec": { + "baseFilters": [ + { + "condition": "AND", + "key": "key1", + "operator": "=", + "value": "value1" + }, + { + "condition": "OR", + "key": "key2", + "operator": "=", + "value": "value2" + } + ], + "datasource": { + "type": "prometheus", + "uid": "datasource-3" + }, + "defaultKeys": [ + { + "expandable": true, + "group": "defaultGroup1", + "text": "defaultKey1", + "value": "defaultKey1" + } + ], + "description": "An adhoc variable", + "filters": [ + { + "condition": "AND", + "key": "key3", + "operator": "=", + "value": "value3" + } + ], + "hide": "dontHide", + "label": "Adhoc Variable", + "name": "adhocVar", + "skipUrlSync": false, + "allowCustomValue": true + } + } + ] + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.viz-config.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.viz-config.json new file mode 100644 index 00000000000..5d591e64ca0 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.viz-config.json @@ -0,0 +1,202 @@ +{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": { + "name": "test-v2alpha1-viz-config", + "labels": {}, + "annotations": {} + }, + "spec": { + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "datasource": { + "uid": "gdev-testdata" + }, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 1, + "links": [], + "title": "Simple timeseries (WITH DS REF)", + "vizConfig": { + "kind": "timeseries", + "spec": { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "columnWidthMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ], + "maxColumnCount": 3, + "rowHeightMode": "standard" + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "autoRefresh": "", + "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "fiscalYearStartMonth": 0, + "from": "now-6h", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "Test: V2alpha1 dashboard with viz config" + }, + "status": {} + } + \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json new file mode 100644 index 00000000000..0c8b072ab49 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json @@ -0,0 +1,20 @@ +{ + "metadata": { + "name": "test-v2alpha1-complete", + "creationTimestamp": null, + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "storedVersion": "v2alpha1", + "error": "backend conversion not yet implemented" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json new file mode 100644 index 00000000000..0c8b072ab49 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json @@ -0,0 +1,20 @@ +{ + "metadata": { + "name": "test-v2alpha1-complete", + "creationTimestamp": null, + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "storedVersion": "v2alpha1", + "error": "backend conversion not yet implemented" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json new file mode 100644 index 00000000000..617b33a8108 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json @@ -0,0 +1,511 @@ +{ + "kind": "Dashboard", + "apiVersion": "v2beta1", + "metadata": { + "name": "test-v2alpha1-complete", + "creationTimestamp": null, + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "changes(process_start_time_seconds[1m])", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Prometheus Annotations", + "builtIn": false + } + } + ], + "cursorSync": "Tooltip", + "description": "This dashboard demonstrates all features that need to be converted from v2alpha1 to v2beta1", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Conditional Rendering", + "description": "This panel demonstrates conditional rendering features", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up{job=\"grafana\"}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "includeTimeField": false, + "mode": "reduceFields", + "reducers": [ + "mean" + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "12.1.0-pre", + "spec": { + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Down", + "color": "red" + }, + "1": { + "text": "Up", + "color": "green" + } + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "red" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "Row", + "spec": { + "title": "Conditional Row", + "collapse": false, + "hideHeader": false, + "fillScreen": false, + "conditionalRendering": { + "kind": "ConditionalRenderingGroup", + "spec": { + "visibility": "show", + "condition": "and", + "items": [ + { + "kind": "ConditionalRenderingVariable", + "spec": { + "variable": "group_by", + "operator": "includes", + "value": "instance" + } + }, + { + "kind": "ConditionalRenderingData", + "spec": { + "value": true + } + }, + { + "kind": "ConditionalRenderingTimeRangeSize", + "spec": { + "value": "1h" + } + } + ] + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": true, + "preload": true, + "tags": [ + "test", + "example", + "migration" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "10s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "weekStart": "monday", + "fiscalYearStartMonth": 0 + }, + "title": "Test: Complete V2alpha1 Dashboard Example", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "prometheus_query", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "label": "Prometheus Query", + "hide": "dontHide", + "refresh": "time", + "skipUrlSync": false, + "description": "Shows all up metrics", + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "alphabetical", + "definition": "up", + "options": null, + "multi": true, + "includeAll": true, + "allowCustomValue": false + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "text_var", + "current": { + "selected": true, + "text": "server1", + "value": "server1" + }, + "query": "server1,server2,server3", + "label": "Text Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A simple text variable" + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "constant_var", + "query": "production", + "current": { + "selected": true, + "text": "production", + "value": "production" + }, + "label": "Constant", + "hide": "dontHide", + "skipUrlSync": true, + "description": "A constant value" + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "ds_var", + "pluginId": "prometheus", + "refresh": "load", + "regex": "/^gdev-/", + "current": { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + }, + "options": [ + { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + } + ], + "multi": false, + "includeAll": false, + "label": "Datasource", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Select a datasource", + "allowCustomValue": false + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval", + "query": "1m,5m,10m,30m,1h,6h,12h,1d", + "current": { + "selected": true, + "text": "5m", + "value": "5m" + }, + "options": [ + { + "text": "1m", + "value": "1m" + }, + { + "text": "5m", + "value": "5m" + }, + { + "text": "10m", + "value": "10m" + }, + { + "text": "30m", + "value": "30m" + }, + { + "text": "1h", + "value": "1h" + }, + { + "text": "6h", + "value": "6h" + }, + { + "text": "12h", + "value": "12h" + }, + { + "text": "1d", + "value": "1d" + } + ], + "auto": true, + "auto_min": "10s", + "auto_count": 30, + "refresh": "load", + "label": "Interval", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Time interval selection" + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_var", + "query": "prod : Production, staging : Staging, dev : Development", + "current": { + "text": [ + "Production" + ], + "value": [ + "prod" + ] + }, + "options": [ + { + "text": "Production", + "value": "prod" + }, + { + "text": "Staging", + "value": "staging" + }, + { + "text": "Development", + "value": "dev" + } + ], + "multi": true, + "includeAll": true, + "allValue": "*", + "label": "Custom Options", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Custom multi-value variable", + "allowCustomValue": true + } + }, + { + "kind": "GroupByVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "group_by", + "current": { + "text": "instance", + "value": "instance" + }, + "options": null, + "multi": false, + "label": "Group By", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Group metrics by label" + } + }, + { + "kind": "AdhocVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "filters", + "baseFilters": [ + { + "key": "job", + "operator": "=", + "value": "grafana", + "condition": "AND" + } + ], + "filters": [], + "defaultKeys": [ + { + "text": "job", + "value": "job", + "expandable": true + }, + { + "text": "instance", + "value": "instance", + "expandable": true + } + ], + "label": "Filters", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1", + "error": "" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json new file mode 100644 index 00000000000..0a9980b5ff0 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json @@ -0,0 +1,14 @@ +{ + "metadata": { + "name": "test-v2alpha1-annotations", + "creationTimestamp": null + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "storedVersion": "v2alpha1", + "error": "backend conversion not yet implemented" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json new file mode 100644 index 00000000000..0a9980b5ff0 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json @@ -0,0 +1,14 @@ +{ + "metadata": { + "name": "test-v2alpha1-annotations", + "creationTimestamp": null + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "storedVersion": "v2alpha1", + "error": "backend conversion not yet implemented" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json new file mode 100644 index 00000000000..a7f9231756a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json @@ -0,0 +1,1094 @@ +{ + "kind": "Dashboard", + "apiVersion": "v2beta1", + "metadata": { + "name": "test-v2alpha1-annotations", + "creationTimestamp": null + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "PD8C576611E62080A" + }, + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "no-ds-prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-postgresql-datasource", + "version": "v0", + "datasource": { + "name": "PBBCEC2D313BC06C3" + }, + "spec": { + "editorMode": "builder", + "format": "table", + "lines": 10, + "rawSql": "", + "refId": "Anno", + "scenarioId": "annotations", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "gdev-elasticsearch" + }, + "spec": { + "lines": 10, + "query": "test query", + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "elastic - annos", + "builtIn": false, + "legacyOptions": { + "tagsField": "asd", + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Simple stat (NO DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 4 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "12.1.0-pre", + "spec": { + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with NO REF to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with ref to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Mixed DS WITH REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Mixed DS WITHOUT REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with annotations", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "variable-ds-prometheus", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "csv": "1,2,3,4", + "scenarioId": "csv_metric_values" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds-empty-query", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-default-ds", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1", + "error": "" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json new file mode 100644 index 00000000000..aa3ee7b86ef --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json @@ -0,0 +1,14 @@ +{ + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars", + "creationTimestamp": null + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "storedVersion": "v2alpha1", + "error": "backend conversion not yet implemented" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json new file mode 100644 index 00000000000..aa3ee7b86ef --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json @@ -0,0 +1,14 @@ +{ + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars", + "creationTimestamp": null + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "storedVersion": "v2alpha1", + "error": "backend conversion not yet implemented" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v2beta1.json new file mode 100644 index 00000000000..ff37296efe6 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v2beta1.json @@ -0,0 +1,100 @@ +{ + "kind": "Dashboard", + "apiVersion": "v2beta1", + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars", + "creationTimestamp": null + }, + "spec": { + "annotations": [], + "cursorSync": "", + "elements": {}, + "layout": null, + "links": [], + "preload": false, + "tags": null, + "timeSettings": { + "from": "", + "to": "", + "autoRefresh": "", + "autoRefreshIntervals": null, + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with group by and adhoc variables", + "variables": [ + { + "kind": "GroupByVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "", + "current": { + "text": "text7", + "value": "value7" + }, + "options": null, + "multi": false, + "label": "Group By Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A group by variable" + } + }, + { + "kind": "AdhocVariable", + "group": "prometheus", + "datasource": { + "name": "datasource-3" + }, + "spec": { + "name": "adhocVar", + "baseFilters": [ + { + "key": "key1", + "operator": "=", + "value": "value1", + "condition": "AND" + }, + { + "key": "key2", + "operator": "=", + "value": "value2", + "condition": "OR" + } + ], + "filters": [ + { + "key": "key3", + "operator": "=", + "value": "value3", + "condition": "AND" + } + ], + "defaultKeys": [ + { + "text": "defaultKey1", + "value": "defaultKey1", + "group": "defaultGroup1", + "expandable": true + } + ], + "label": "Adhoc Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "An adhoc variable", + "allowCustomValue": true + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1", + "error": "" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json new file mode 100644 index 00000000000..9c74ecaa74f --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json @@ -0,0 +1,14 @@ +{ + "metadata": { + "name": "test-v2alpha1-viz-config", + "creationTimestamp": null + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "storedVersion": "v2alpha1", + "error": "backend conversion not yet implemented" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json new file mode 100644 index 00000000000..9c74ecaa74f --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json @@ -0,0 +1,14 @@ +{ + "metadata": { + "name": "test-v2alpha1-viz-config", + "creationTimestamp": null + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "storedVersion": "v2alpha1", + "error": "backend conversion not yet implemented" + } + } +} \ No newline at end of file diff --git a/conf/provisioning/sample/dashboard-v2.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json similarity index 54% rename from conf/provisioning/sample/dashboard-v2.json rename to apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json index ad4671943c3..0fe38667e6e 100644 --- a/conf/provisioning/sample/dashboard-v2.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json @@ -1,33 +1,21 @@ { - "apiVersion": "dashboard.grafana.app/v2alpha1", "kind": "Dashboard", + "apiVersion": "v2beta1", "metadata": { - "name": "sample-dash-v2" + "name": "test-v2alpha1-viz-config", + "creationTimestamp": null }, "spec": { - "annotations": [ - { - "kind": "AnnotationQuery", - "spec": { - "builtIn": true, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations \u0026 Alerts" - } - } - ], - "cursorSync": "Off", - "description": "", - "editable": true, + "annotations": [], + "cursorSync": "", "elements": { "panel-1": { "kind": "Panel", "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], "data": { "kind": "QueryGroup", "spec": { @@ -35,28 +23,60 @@ { "kind": "PanelQuery", "spec": { - "hidden": false, "query": { - "kind": "grafana-testdata-datasource", - "spec": {} + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } }, - "refId": "A" + "refId": "A", + "hidden": false } } ], - "queryOptions": {}, - "transformations": [] + "transformations": [], + "queryOptions": {} } }, - "description": "", - "id": 1, - "links": [], - "title": "Simle timeseries", "vizConfig": { - "kind": "timeseries", + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, "fieldConfig": { "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, "color": { "mode": "palette-classic" }, @@ -92,108 +112,10 @@ "thresholdsStyle": { "mode": "off" } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] } }, "overrides": [] - }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.0.0-pre" - } - } - } - }, - "panel-2": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "hidden": false, - "query": { - "kind": "grafana-testdata-datasource", - "spec": {} - }, - "refId": "A" - } - } - ], - "queryOptions": {}, - "transformations": [] - } - }, - "description": "", - "id": 2, - "links": [], - "title": "Simple stat", - "vizConfig": { - "kind": "stat", - "spec": { - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.0.0-pre" + } } } } @@ -202,8 +124,28 @@ "layout": { "kind": "AutoGridLayout", "spec": { + "maxColumnCount": 3, "columnWidthMode": "standard", + "rowHeightMode": "standard", "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, { "kind": "AutoGridLayoutItem", "spec": { @@ -221,10 +163,26 @@ "name": "panel-1" } } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } } - ], - "maxColumnCount": 3, - "rowHeightMode": "standard" + ] } }, "links": [], @@ -232,15 +190,33 @@ "preload": false, "tags": [], "timeSettings": { - "autoRefresh": "", - "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "fiscalYearStartMonth": 0, - "from": "now-6h", - "hideTimepicker": false, "timezone": "browser", - "to": "now" + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 }, - "title": "v2alpha1 dashboard", + "title": "Test: V2alpha1 dashboard with viz config", "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1", + "error": "" + } } -} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/v0.go b/apps/dashboard/pkg/migration/conversion/v0.go index 94f3d5b57f4..a63fe00f611 100644 --- a/apps/dashboard/pkg/migration/conversion/v0.go +++ b/apps/dashboard/pkg/migration/conversion/v0.go @@ -6,7 +6,7 @@ import ( dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" "github.com/grafana/grafana/apps/dashboard/pkg/migration" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) @@ -61,13 +61,13 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s return nil } -func Convert_V0_to_V2alpha2(in *dashv0.Dashboard, out *dashv2alpha2.Dashboard, scope conversion.Scope) error { +func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta - // TODO: implement V0 to V2alpha2 conversion + // TODO: implement V0 to v2beta1 conversion - out.Status = dashv2alpha2.DashboardStatus{ - Conversion: &dashv2alpha2.DashboardConversionStatus{ + out.Status = dashv2beta1.DashboardStatus{ + Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: dashv0.VERSION, Failed: true, Error: "backend conversion not yet implemented", diff --git a/apps/dashboard/pkg/migration/conversion/v1.go b/apps/dashboard/pkg/migration/conversion/v1.go index d71d9e40caf..0e1bb8fc8ed 100644 --- a/apps/dashboard/pkg/migration/conversion/v1.go +++ b/apps/dashboard/pkg/migration/conversion/v1.go @@ -6,7 +6,7 @@ import ( dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" ) func Convert_V1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { @@ -54,13 +54,13 @@ func Convert_V1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, s return nil } -func Convert_V1_to_V2alpha2(in *dashv1.Dashboard, out *dashv2alpha2.Dashboard, scope conversion.Scope) error { +func Convert_V1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta - // TODO: implement V1 to V2alpha2 conversion + // TODO: implement V1 to v2beta1 conversion - out.Status = dashv2alpha2.DashboardStatus{ - Conversion: &dashv2alpha2.DashboardConversionStatus{ + out.Status = dashv2beta1.DashboardStatus{ + Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: dashv1.VERSION, Failed: true, Error: "backend conversion not yet implemented", diff --git a/apps/dashboard/pkg/migration/conversion/v2.go b/apps/dashboard/pkg/migration/conversion/v2.go index 5186f9257e2..ccdb98fdcf4 100644 --- a/apps/dashboard/pkg/migration/conversion/v2.go +++ b/apps/dashboard/pkg/migration/conversion/v2.go @@ -6,7 +6,7 @@ import ( dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" ) func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { @@ -41,29 +41,40 @@ func Convert_V2alpha1_to_V1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, s return nil } -func Convert_V2alpha1_to_V2alpha2(in *dashv2alpha1.Dashboard, out *dashv2alpha2.Dashboard, scope conversion.Scope) error { +func Convert_V2alpha1_to_V2beta1(in *dashv2alpha1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta - // TODO: implement V2alpha1 to V2alpha2 conversion + // Convert the spec + if err := ConvertDashboard_V2alpha1_to_V2beta1(in, out, scope); err != nil { + out.Status = dashv2beta1.DashboardStatus{ + Conversion: &dashv2beta1.DashboardConversionStatus{ + StoredVersion: dashv2alpha1.VERSION, + Failed: true, + Error: err.Error(), + }, + } + return err + } - out.Status = dashv2alpha2.DashboardStatus{ - Conversion: &dashv2alpha2.DashboardConversionStatus{ + // Set successful conversion status + out.Status = dashv2beta1.DashboardStatus{ + Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: dashv2alpha1.VERSION, - Failed: true, - Error: "backend conversion not yet implemented", + Failed: false, }, } return nil } -func Convert_V2alpha2_to_V0(in *dashv2alpha2.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { + +func Convert_V2beta1_to_V0(in *dashv2beta1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta - // TODO: implement V2alpha2 to V0 conversion + // TODO: implement v2beta1 to V0 conversion out.Status = dashv0.DashboardStatus{ Conversion: &dashv0.DashboardConversionStatus{ - StoredVersion: dashv2alpha2.VERSION, + StoredVersion: dashv2beta1.VERSION, Failed: true, Error: "backend conversion not yet implemented", }, @@ -72,14 +83,14 @@ func Convert_V2alpha2_to_V0(in *dashv2alpha2.Dashboard, out *dashv0.Dashboard, s return nil } -func Convert_V2alpha2_to_V1(in *dashv2alpha2.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { +func Convert_V2beta1_to_V1(in *dashv2beta1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta - // TODO: implement V2alpha2 to V1 conversion + // TODO: implement v2beta1 to V1 conversion out.Status = dashv1.DashboardStatus{ Conversion: &dashv1.DashboardConversionStatus{ - StoredVersion: dashv2alpha2.VERSION, + StoredVersion: dashv2beta1.VERSION, Failed: true, Error: "backend conversion not yet implemented", }, @@ -88,14 +99,14 @@ func Convert_V2alpha2_to_V1(in *dashv2alpha2.Dashboard, out *dashv1.Dashboard, s return nil } -func Convert_V2alpha2_to_V2alpha1(in *dashv2alpha2.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope) error { +func Convert_V2beta1_to_V2alpha1(in *dashv2beta1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta - // TODO: implement V2alpha2 to V2alpha1 conversion + // TODO: implement v2beta1 to V2alpha1 conversion out.Status = dashv2alpha1.DashboardStatus{ Conversion: &dashv2alpha1.DashboardConversionStatus{ - StoredVersion: dashv2alpha2.VERSION, + StoredVersion: dashv2beta1.VERSION, Failed: true, Error: "backend conversion not yet implemented", }, diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go new file mode 100644 index 00000000000..05d7aa0ada0 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -0,0 +1,981 @@ +package conversion + +import ( + "k8s.io/apimachinery/pkg/conversion" + + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" +) + +// Schema Migration: v2alpha1 → v2beta1 +// +// This file handles the conversion from Dashboard v2alpha1 to v2beta1 schema. +// The main changes between these versions are: +// +// 1. DataQueryKind Structure Changes: +// - v2alpha1: kind = datasource type (e.g., "prometheus", "elasticsearch") +// - v2beta1: kind = "DataQuery" (hardcoded), group = datasource type, version = "v0" +// +// 2. Datasource Reference Migration: +// - v2alpha1: Datasource references stored at spec level (PanelQuerySpec.datasource, AnnotationQuerySpec.datasource, etc.) +// - v2beta1: Datasource references moved inside DataQueryKind.datasource +// - v2alpha1: {type?: string, uid?: string} → v2beta1: {name?: string} +// +// 3. Query Requirements: +// - v2alpha1: AnnotationQuerySpec.query? is optional (can be nil) +// - v2beta1: AnnotationQuerySpec.query is required +// - Conversion creates default query structure when v2alpha1 query is nil +// +// 4. DataSourceRef Usage: +// - v2alpha1: Used widely across different specs +// - v2beta1: Kept only for backward compatibility in GroupBy and Adhoc variables +// +// 5. Datasource Mapping Strategy: +// - Type → Name (if type is available) +// - Uid → Name (if type is not available but uid is) +// +// The conversion preserves all dashboard functionality while restructuring +// the data model to consolidate datasource references into the DataQueryKind. + +func ConvertDashboard_V2alpha1_to_V2beta1(in *dashv2alpha1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv2beta1.VERSION + out.Kind = in.Kind + + return convertDashboardSpec_V2alpha1_to_V2beta1(&in.Spec, &out.Spec, scope) +} + +func convertDashboardSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardSpec, out *dashv2beta1.DashboardSpec, scope conversion.Scope) error { + // Convert annotations + out.Annotations = make([]dashv2beta1.DashboardAnnotationQueryKind, len(in.Annotations)) + for i, annotation := range in.Annotations { + if err := convertAnnotationQuery_V2alpha1_to_V2beta1(&annotation, &out.Annotations[i], scope); err != nil { + return err + } + } + + // Copy simple fields + out.CursorSync = dashv2beta1.DashboardDashboardCursorSync(in.CursorSync) + out.Description = in.Description + out.Editable = in.Editable + out.LiveNow = in.LiveNow + out.Preload = in.Preload + out.Revision = in.Revision + out.Tags = in.Tags + out.Title = in.Title + + // Convert elements + out.Elements = make(map[string]dashv2beta1.DashboardElement, len(in.Elements)) + for key, element := range in.Elements { + var convertedElement dashv2beta1.DashboardElement + if err := convertElement_V2alpha1_to_V2beta1(&element, &convertedElement, scope); err != nil { + return err + } + out.Elements[key] = convertedElement + } + + // Convert layout + if err := convertLayout_V2alpha1_to_V2beta1(&in.Layout, &out.Layout, scope); err != nil { + return err + } + + // Convert links + out.Links = make([]dashv2beta1.DashboardDashboardLink, len(in.Links)) + for i, link := range in.Links { + convertDashboardLink_V2alpha1_to_V2beta1(&link, &out.Links[i]) + } + + // Convert time settings + if err := convertTimeSettings_V2alpha1_to_V2beta1(&in.TimeSettings, &out.TimeSettings, scope); err != nil { + return err + } + + // Convert variables + out.Variables = make([]dashv2beta1.DashboardVariableKind, len(in.Variables)) + for i, variable := range in.Variables { + if err := convertVariable_V2alpha1_to_V2beta1(&variable, &out.Variables[i], scope); err != nil { + return err + } + } + + return nil +} + +func convertAnnotationQuery_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardAnnotationQueryKind, out *dashv2beta1.DashboardAnnotationQueryKind, scope conversion.Scope) error { + out.Kind = in.Kind + + // Convert spec + out.Spec.Enable = in.Spec.Enable + out.Spec.Hide = in.Spec.Hide + out.Spec.IconColor = in.Spec.IconColor + out.Spec.Name = in.Spec.Name + out.Spec.BuiltIn = in.Spec.BuiltIn + out.Spec.Filter = (*dashv2beta1.DashboardAnnotationPanelFilter)(in.Spec.Filter) + out.Spec.LegacyOptions = in.Spec.LegacyOptions + + // Convert query - move datasource from annotation spec to query + if err := convertDataQuery_V2alpha1_to_V2beta1(in.Spec.Query, &out.Spec.Query, in.Spec.Datasource, scope); err != nil { + return err + } + + return nil +} + +func convertDataQuery_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardDataQueryKind, out *dashv2beta1.DashboardDataQueryKind, datasource *dashv2alpha1.DashboardDataSourceRef, scope conversion.Scope) error { + if in == nil { + // v2beta1 requires a query even if v2alpha1 had none, so create a default + out.Kind = "DataQuery" + out.Group = "" + out.Version = "v0" + out.Spec = make(map[string]interface{}) + } else { + out.Kind = "DataQuery" + out.Group = in.Kind + out.Version = "v0" + out.Spec = in.Spec + } + + // Convert datasource reference + if datasource != nil { + out.Datasource = &dashv2beta1.DashboardV2beta1DataQueryKindDatasource{} + if datasource.Uid != nil { + out.Datasource.Name = datasource.Uid + } + } + + return nil +} + +func convertElement_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardElement, out *dashv2beta1.DashboardElement, scope conversion.Scope) error { + if in.PanelKind != nil { + out.PanelKind = &dashv2beta1.DashboardPanelKind{} + return convertPanelKind_V2alpha1_to_V2beta1(in.PanelKind, out.PanelKind, scope) + } + + if in.LibraryPanelKind != nil { + out.LibraryPanelKind = &dashv2beta1.DashboardLibraryPanelKind{} + return convertLibraryPanelKind_V2alpha1_to_V2beta1(in.LibraryPanelKind, out.LibraryPanelKind, scope) + } + + return nil +} + +func convertPanelKind_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardPanelKind, out *dashv2beta1.DashboardPanelKind, scope conversion.Scope) error { + out.Kind = in.Kind + + // Convert spec + out.Spec.Id = in.Spec.Id + out.Spec.Title = in.Spec.Title + out.Spec.Description = in.Spec.Description + out.Spec.Transparent = in.Spec.Transparent + + // Convert links + out.Spec.Links = make([]dashv2beta1.DashboardDataLink, len(in.Spec.Links)) + for i, link := range in.Spec.Links { + convertDataLink_V2alpha1_to_V2beta1(&link, &out.Spec.Links[i]) + } + + // Convert data (QueryGroup) + if err := convertQueryGroup_V2alpha1_to_V2beta1(&in.Spec.Data, &out.Spec.Data, scope); err != nil { + return err + } + + // Convert vizConfig + if err := convertVizConfig_V2alpha1_to_V2beta1(&in.Spec.VizConfig, &out.Spec.VizConfig); err != nil { + return err + } + + return nil +} + +func convertLibraryPanelKind_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardLibraryPanelKind, out *dashv2beta1.DashboardLibraryPanelKind, scope conversion.Scope) error { + out.Kind = in.Kind + out.Spec.Id = in.Spec.Id + out.Spec.Title = in.Spec.Title + out.Spec.LibraryPanel = dashv2beta1.DashboardLibraryPanelRef{ + Name: in.Spec.LibraryPanel.Name, + Uid: in.Spec.LibraryPanel.Uid, + } + return nil +} + +func convertQueryGroup_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardQueryGroupKind, out *dashv2beta1.DashboardQueryGroupKind, scope conversion.Scope) error { + out.Kind = in.Kind + + // Convert queries + out.Spec.Queries = make([]dashv2beta1.DashboardPanelQueryKind, len(in.Spec.Queries)) + for i, query := range in.Spec.Queries { + if err := convertPanelQuery_V2alpha1_to_V2beta1(&query, &out.Spec.Queries[i], scope); err != nil { + return err + } + } + + // Convert transformations + out.Spec.Transformations = make([]dashv2beta1.DashboardTransformationKind, len(in.Spec.Transformations)) + for i, transformation := range in.Spec.Transformations { + convertTransformation_V2alpha1_to_V2beta1(&transformation, &out.Spec.Transformations[i]) + } + + // Convert query options + convertQueryOptions_V2alpha1_to_V2beta1(&in.Spec.QueryOptions, &out.Spec.QueryOptions) + + return nil +} + +func convertPanelQuery_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardPanelQueryKind, out *dashv2beta1.DashboardPanelQueryKind, scope conversion.Scope) error { + out.Kind = in.Kind // PanelQueryKind keeps "PanelQuery" as its kind + out.Spec.RefId = in.Spec.RefId + out.Spec.Hidden = in.Spec.Hidden + + // Convert query - move datasource from panel query spec to query + if err := convertDataQuery_V2alpha1_to_V2beta1(&in.Spec.Query, &out.Spec.Query, in.Spec.Datasource, scope); err != nil { + return err + } + + return nil +} + +func convertTransformation_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardTransformationKind, out *dashv2beta1.DashboardTransformationKind) { + out.Kind = in.Kind + out.Spec.Id = in.Spec.Id + out.Spec.Disabled = in.Spec.Disabled + out.Spec.Filter = (*dashv2beta1.DashboardMatcherConfig)(in.Spec.Filter) + out.Spec.Topic = (*dashv2beta1.DashboardDataTopic)(in.Spec.Topic) + out.Spec.Options = in.Spec.Options +} + +func convertQueryOptions_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardQueryOptionsSpec, out *dashv2beta1.DashboardQueryOptionsSpec) { + out.TimeFrom = in.TimeFrom + out.MaxDataPoints = in.MaxDataPoints + out.TimeShift = in.TimeShift + out.QueryCachingTTL = in.QueryCachingTTL + out.Interval = in.Interval + out.CacheTimeout = in.CacheTimeout + out.HideTimeOverride = in.HideTimeOverride +} + +func convertVizConfig_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVizConfigKind, out *dashv2beta1.DashboardVizConfigKind) error { + out.Kind = "VizConfig" + out.Group = in.Kind + out.Version = in.Spec.PluginVersion + out.Spec = dashv2beta1.DashboardVizConfigSpec{ + Options: in.Spec.Options, + } + + // Convert field config + convertFieldConfigSource_V2alpha1_to_V2beta1(&in.Spec.FieldConfig, &out.Spec.FieldConfig) + + return nil +} + +func convertFieldConfigSource_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardFieldConfigSource, out *dashv2beta1.DashboardFieldConfigSource) { + // Convert defaults + convertFieldConfig_V2alpha1_to_V2beta1(&in.Defaults, &out.Defaults) + + // Convert overrides + out.Overrides = make([]dashv2beta1.DashboardV2beta1FieldConfigSourceOverrides, len(in.Overrides)) + for i, override := range in.Overrides { + convertFieldConfigOverride_V2alpha1_to_V2beta1(&override, &out.Overrides[i]) + } +} + +func convertFieldConfig_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardFieldConfig, out *dashv2beta1.DashboardFieldConfig) { + *out = dashv2beta1.DashboardFieldConfig{ + DisplayName: in.DisplayName, + DisplayNameFromDS: in.DisplayNameFromDS, + Description: in.Description, + Path: in.Path, + Writeable: in.Writeable, + Filterable: in.Filterable, + Unit: in.Unit, + Decimals: in.Decimals, + Min: in.Min, + Max: in.Max, + Links: in.Links, + NoValue: in.NoValue, + Custom: in.Custom, + } + + // Convert thresholds + if in.Thresholds != nil { + out.Thresholds = &dashv2beta1.DashboardThresholdsConfig{ + Mode: dashv2beta1.DashboardThresholdsMode(in.Thresholds.Mode), + Steps: make([]dashv2beta1.DashboardThreshold, len(in.Thresholds.Steps)), + } + for i, step := range in.Thresholds.Steps { + out.Thresholds.Steps[i] = dashv2beta1.DashboardThreshold{ + Value: step.Value, + Color: step.Color, + } + } + } + + // Convert color + if in.Color != nil { + out.Color = &dashv2beta1.DashboardFieldColor{ + Mode: dashv2beta1.DashboardFieldColorModeId(in.Color.Mode), + FixedColor: in.Color.FixedColor, + SeriesBy: (*dashv2beta1.DashboardFieldColorSeriesByMode)(in.Color.SeriesBy), + } + } + + // Convert mappings + if in.Mappings != nil { + out.Mappings = make([]dashv2beta1.DashboardValueMapping, len(in.Mappings)) + for i, mapping := range in.Mappings { + convertValueMapping_V2alpha1_to_V2beta1(&mapping, &out.Mappings[i]) + } + } +} + +func convertFieldConfigOverride_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides, out *dashv2beta1.DashboardV2beta1FieldConfigSourceOverrides) { + out.Matcher = dashv2beta1.DashboardMatcherConfig{ + Id: in.Matcher.Id, + Options: in.Matcher.Options, + } + + out.Properties = make([]dashv2beta1.DashboardDynamicConfigValue, len(in.Properties)) + for i, prop := range in.Properties { + out.Properties[i] = dashv2beta1.DashboardDynamicConfigValue{ + Id: prop.Id, + Value: prop.Value, + } + } +} + +func convertValueMapping_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardValueMapping, out *dashv2beta1.DashboardValueMapping) { + if in.ValueMap != nil { + out.ValueMap = &dashv2beta1.DashboardValueMap{ + Type: dashv2beta1.DashboardMappingType(in.ValueMap.Type), + Options: make(map[string]dashv2beta1.DashboardValueMappingResult, len(in.ValueMap.Options)), + } + for k, v := range in.ValueMap.Options { + out.ValueMap.Options[k] = dashv2beta1.DashboardValueMappingResult{ + Text: v.Text, + Color: v.Color, + Icon: v.Icon, + Index: v.Index, + } + } + } + + if in.RangeMap != nil { + out.RangeMap = &dashv2beta1.DashboardRangeMap{ + Type: dashv2beta1.DashboardMappingType(in.RangeMap.Type), + Options: dashv2beta1.DashboardV2beta1RangeMapOptions{ + From: in.RangeMap.Options.From, + To: in.RangeMap.Options.To, + Result: dashv2beta1.DashboardValueMappingResult{ + Text: in.RangeMap.Options.Result.Text, + Color: in.RangeMap.Options.Result.Color, + Icon: in.RangeMap.Options.Result.Icon, + Index: in.RangeMap.Options.Result.Index, + }, + }, + } + } + + if in.RegexMap != nil { + out.RegexMap = &dashv2beta1.DashboardRegexMap{ + Type: dashv2beta1.DashboardMappingType(in.RegexMap.Type), + Options: dashv2beta1.DashboardV2beta1RegexMapOptions{ + Pattern: in.RegexMap.Options.Pattern, + Result: dashv2beta1.DashboardValueMappingResult{ + Text: in.RegexMap.Options.Result.Text, + Color: in.RegexMap.Options.Result.Color, + Icon: in.RegexMap.Options.Result.Icon, + Index: in.RegexMap.Options.Result.Index, + }, + }, + } + } + + if in.SpecialValueMap != nil { + out.SpecialValueMap = &dashv2beta1.DashboardSpecialValueMap{ + Type: dashv2beta1.DashboardMappingType(in.SpecialValueMap.Type), + Options: dashv2beta1.DashboardV2beta1SpecialValueMapOptions{ + Match: dashv2beta1.DashboardSpecialValueMatch(in.SpecialValueMap.Options.Match), + Result: dashv2beta1.DashboardValueMappingResult{ + Text: in.SpecialValueMap.Options.Result.Text, + Color: in.SpecialValueMap.Options.Result.Color, + Icon: in.SpecialValueMap.Options.Result.Icon, + Index: in.SpecialValueMap.Options.Result.Index, + }, + }, + } + } +} + +func convertLayout_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, out *dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, scope conversion.Scope) error { + if in.GridLayoutKind != nil { + out.GridLayoutKind = &dashv2beta1.DashboardGridLayoutKind{ + Kind: in.GridLayoutKind.Kind, + } + return convertGridLayoutSpec_V2alpha1_to_V2beta1(&in.GridLayoutKind.Spec, &out.GridLayoutKind.Spec, scope) + } + + if in.RowsLayoutKind != nil { + out.RowsLayoutKind = &dashv2beta1.DashboardRowsLayoutKind{ + Kind: in.RowsLayoutKind.Kind, + } + return convertRowsLayoutSpec_V2alpha1_to_V2beta1(&in.RowsLayoutKind.Spec, &out.RowsLayoutKind.Spec, scope) + } + + if in.AutoGridLayoutKind != nil { + out.AutoGridLayoutKind = &dashv2beta1.DashboardAutoGridLayoutKind{ + Kind: in.AutoGridLayoutKind.Kind, + } + return convertAutoGridLayoutSpec_V2alpha1_to_V2beta1(&in.AutoGridLayoutKind.Spec, &out.AutoGridLayoutKind.Spec, scope) + } + + if in.TabsLayoutKind != nil { + out.TabsLayoutKind = &dashv2beta1.DashboardTabsLayoutKind{ + Kind: in.TabsLayoutKind.Kind, + } + return convertTabsLayoutSpec_V2alpha1_to_V2beta1(&in.TabsLayoutKind.Spec, &out.TabsLayoutKind.Spec, scope) + } + + return nil +} + +func convertGridLayoutSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutSpec, out *dashv2beta1.DashboardGridLayoutSpec, scope conversion.Scope) error { + out.Items = make([]dashv2beta1.DashboardGridLayoutItemKind, len(in.Items)) + for i, item := range in.Items { + out.Items[i] = dashv2beta1.DashboardGridLayoutItemKind{ + Kind: item.Kind, + Spec: dashv2beta1.DashboardGridLayoutItemSpec{ + X: item.Spec.X, + Y: item.Spec.Y, + Width: item.Spec.Width, + Height: item.Spec.Height, + Element: dashv2beta1.DashboardElementReference{ + Kind: item.Spec.Element.Kind, + Name: item.Spec.Element.Name, + }, + Repeat: convertRepeatOptions_V2alpha1_to_V2beta1(item.Spec.Repeat), + }, + } + } + return nil +} + +func convertRowsLayoutSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardRowsLayoutSpec, out *dashv2beta1.DashboardRowsLayoutSpec, scope conversion.Scope) error { + out.Rows = make([]dashv2beta1.DashboardRowsLayoutRowKind, len(in.Rows)) + for i, row := range in.Rows { + out.Rows[i] = dashv2beta1.DashboardRowsLayoutRowKind{ + Kind: row.Kind, + Spec: dashv2beta1.DashboardRowsLayoutRowSpec{ + Title: row.Spec.Title, + Collapse: row.Spec.Collapse, + HideHeader: row.Spec.HideHeader, + FillScreen: row.Spec.FillScreen, + ConditionalRendering: convertConditionalRenderingGroupKind_V2alpha1_to_V2beta1(row.Spec.ConditionalRendering), + Repeat: convertRowRepeatOptions_V2alpha1_to_V2beta1(row.Spec.Repeat), + }, + } + if err := convertRowLayout_V2alpha1_to_V2beta1(&row.Spec.Layout, &out.Rows[i].Spec.Layout, scope); err != nil { + return err + } + } + return nil +} + +func convertAutoGridLayoutSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardAutoGridLayoutSpec, out *dashv2beta1.DashboardAutoGridLayoutSpec, scope conversion.Scope) error { + out.MaxColumnCount = in.MaxColumnCount + out.ColumnWidthMode = dashv2beta1.DashboardAutoGridLayoutSpecColumnWidthMode(in.ColumnWidthMode) + out.ColumnWidth = in.ColumnWidth + out.RowHeightMode = dashv2beta1.DashboardAutoGridLayoutSpecRowHeightMode(in.RowHeightMode) + out.RowHeight = in.RowHeight + out.FillScreen = in.FillScreen + + out.Items = make([]dashv2beta1.DashboardAutoGridLayoutItemKind, len(in.Items)) + for i, item := range in.Items { + out.Items[i] = dashv2beta1.DashboardAutoGridLayoutItemKind{ + Kind: item.Kind, + Spec: dashv2beta1.DashboardAutoGridLayoutItemSpec{ + Element: dashv2beta1.DashboardElementReference{ + Kind: item.Spec.Element.Kind, + Name: item.Spec.Element.Name, + }, + Repeat: convertAutoGridRepeatOptions_V2alpha1_to_V2beta1(item.Spec.Repeat), + ConditionalRendering: convertConditionalRenderingGroupKind_V2alpha1_to_V2beta1(item.Spec.ConditionalRendering), + }, + } + } + return nil +} + +func convertTabsLayoutSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardTabsLayoutSpec, out *dashv2beta1.DashboardTabsLayoutSpec, scope conversion.Scope) error { + out.Tabs = make([]dashv2beta1.DashboardTabsLayoutTabKind, len(in.Tabs)) + for i, tab := range in.Tabs { + out.Tabs[i] = dashv2beta1.DashboardTabsLayoutTabKind{ + Kind: tab.Kind, + Spec: dashv2beta1.DashboardTabsLayoutTabSpec{ + Title: tab.Spec.Title, + ConditionalRendering: convertConditionalRenderingGroupKind_V2alpha1_to_V2beta1(tab.Spec.ConditionalRendering), + Repeat: convertTabRepeatOptions_V2alpha1_to_V2beta1(tab.Spec.Repeat), + }, + } + if err := convertTabLayout_V2alpha1_to_V2beta1(&tab.Spec.Layout, &out.Tabs[i].Spec.Layout, scope); err != nil { + return err + } + } + return nil +} + +func convertDashboardLink_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardDashboardLink, out *dashv2beta1.DashboardDashboardLink) { + out.Title = in.Title + out.Type = dashv2beta1.DashboardDashboardLinkType(in.Type) + out.Icon = in.Icon + out.Tooltip = in.Tooltip + out.Url = in.Url + out.Tags = in.Tags + out.AsDropdown = in.AsDropdown + out.TargetBlank = in.TargetBlank + out.IncludeVars = in.IncludeVars + out.KeepTime = in.KeepTime +} + +func convertDataLink_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardDataLink, out *dashv2beta1.DashboardDataLink) { + out.Title = in.Title + out.Url = in.Url + out.TargetBlank = in.TargetBlank +} + +func convertTimeSettings_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardTimeSettingsSpec, out *dashv2beta1.DashboardTimeSettingsSpec, scope conversion.Scope) error { + out.Timezone = in.Timezone + out.From = in.From + out.To = in.To + out.AutoRefresh = in.AutoRefresh + out.AutoRefreshIntervals = in.AutoRefreshIntervals + out.HideTimepicker = in.HideTimepicker + out.WeekStart = (*dashv2beta1.DashboardTimeSettingsSpecWeekStart)(in.WeekStart) + out.FiscalYearStartMonth = in.FiscalYearStartMonth + out.NowDelay = in.NowDelay + + // Convert quick ranges + if in.QuickRanges != nil { + out.QuickRanges = make([]dashv2beta1.DashboardTimeRangeOption, len(in.QuickRanges)) + for i, qr := range in.QuickRanges { + out.QuickRanges[i] = dashv2beta1.DashboardTimeRangeOption{ + Display: qr.Display, + From: qr.From, + To: qr.To, + } + } + } + + return nil +} + +func convertVariable_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableKind, out *dashv2beta1.DashboardVariableKind, scope conversion.Scope) error { + if in.QueryVariableKind != nil { + out.QueryVariableKind = &dashv2beta1.DashboardQueryVariableKind{ + Kind: in.QueryVariableKind.Kind, + } + return convertQueryVariableSpec_V2alpha1_to_V2beta1(&in.QueryVariableKind.Spec, &out.QueryVariableKind.Spec, scope) + } + + if in.TextVariableKind != nil { + out.TextVariableKind = &dashv2beta1.DashboardTextVariableKind{ + Kind: in.TextVariableKind.Kind, + Spec: dashv2beta1.DashboardTextVariableSpec{ + Name: in.TextVariableKind.Spec.Name, + Current: convertVariableOption_V2alpha1_to_V2beta1(in.TextVariableKind.Spec.Current), + Query: in.TextVariableKind.Spec.Query, + Label: in.TextVariableKind.Spec.Label, + Hide: dashv2beta1.DashboardVariableHide(in.TextVariableKind.Spec.Hide), + SkipUrlSync: in.TextVariableKind.Spec.SkipUrlSync, + Description: in.TextVariableKind.Spec.Description, + }, + } + } + + if in.ConstantVariableKind != nil { + out.ConstantVariableKind = &dashv2beta1.DashboardConstantVariableKind{ + Kind: in.ConstantVariableKind.Kind, + Spec: dashv2beta1.DashboardConstantVariableSpec{ + Name: in.ConstantVariableKind.Spec.Name, + Query: in.ConstantVariableKind.Spec.Query, + Current: convertVariableOption_V2alpha1_to_V2beta1(in.ConstantVariableKind.Spec.Current), + Label: in.ConstantVariableKind.Spec.Label, + Hide: dashv2beta1.DashboardVariableHide(in.ConstantVariableKind.Spec.Hide), + SkipUrlSync: in.ConstantVariableKind.Spec.SkipUrlSync, + Description: in.ConstantVariableKind.Spec.Description, + }, + } + } + + if in.DatasourceVariableKind != nil { + out.DatasourceVariableKind = &dashv2beta1.DashboardDatasourceVariableKind{ + Kind: in.DatasourceVariableKind.Kind, + Spec: dashv2beta1.DashboardDatasourceVariableSpec{ + Name: in.DatasourceVariableKind.Spec.Name, + PluginId: in.DatasourceVariableKind.Spec.PluginId, + Refresh: dashv2beta1.DashboardVariableRefresh(in.DatasourceVariableKind.Spec.Refresh), + Regex: in.DatasourceVariableKind.Spec.Regex, + Current: convertVariableOption_V2alpha1_to_V2beta1(in.DatasourceVariableKind.Spec.Current), + Options: convertVariableOptions_V2alpha1_to_V2beta1(in.DatasourceVariableKind.Spec.Options), + Multi: in.DatasourceVariableKind.Spec.Multi, + IncludeAll: in.DatasourceVariableKind.Spec.IncludeAll, + AllValue: in.DatasourceVariableKind.Spec.AllValue, + Label: in.DatasourceVariableKind.Spec.Label, + Hide: dashv2beta1.DashboardVariableHide(in.DatasourceVariableKind.Spec.Hide), + SkipUrlSync: in.DatasourceVariableKind.Spec.SkipUrlSync, + Description: in.DatasourceVariableKind.Spec.Description, + AllowCustomValue: in.DatasourceVariableKind.Spec.AllowCustomValue, + }, + } + } + + if in.IntervalVariableKind != nil { + out.IntervalVariableKind = &dashv2beta1.DashboardIntervalVariableKind{ + Kind: in.IntervalVariableKind.Kind, + Spec: dashv2beta1.DashboardIntervalVariableSpec{ + Name: in.IntervalVariableKind.Spec.Name, + Query: in.IntervalVariableKind.Spec.Query, + Current: convertVariableOption_V2alpha1_to_V2beta1(in.IntervalVariableKind.Spec.Current), + Options: convertVariableOptions_V2alpha1_to_V2beta1(in.IntervalVariableKind.Spec.Options), + Auto: in.IntervalVariableKind.Spec.Auto, + AutoMin: in.IntervalVariableKind.Spec.AutoMin, + AutoCount: in.IntervalVariableKind.Spec.AutoCount, + Refresh: dashv2beta1.DashboardVariableRefresh(in.IntervalVariableKind.Spec.Refresh), + Label: in.IntervalVariableKind.Spec.Label, + Hide: dashv2beta1.DashboardVariableHide(in.IntervalVariableKind.Spec.Hide), + SkipUrlSync: in.IntervalVariableKind.Spec.SkipUrlSync, + Description: in.IntervalVariableKind.Spec.Description, + }, + } + } + + if in.CustomVariableKind != nil { + out.CustomVariableKind = &dashv2beta1.DashboardCustomVariableKind{ + Kind: in.CustomVariableKind.Kind, + Spec: dashv2beta1.DashboardCustomVariableSpec{ + Name: in.CustomVariableKind.Spec.Name, + Query: in.CustomVariableKind.Spec.Query, + Current: convertVariableOption_V2alpha1_to_V2beta1(in.CustomVariableKind.Spec.Current), + Options: convertVariableOptions_V2alpha1_to_V2beta1(in.CustomVariableKind.Spec.Options), + Multi: in.CustomVariableKind.Spec.Multi, + IncludeAll: in.CustomVariableKind.Spec.IncludeAll, + AllValue: in.CustomVariableKind.Spec.AllValue, + Label: in.CustomVariableKind.Spec.Label, + Hide: dashv2beta1.DashboardVariableHide(in.CustomVariableKind.Spec.Hide), + SkipUrlSync: in.CustomVariableKind.Spec.SkipUrlSync, + Description: in.CustomVariableKind.Spec.Description, + AllowCustomValue: in.CustomVariableKind.Spec.AllowCustomValue, + }, + } + } + + if in.GroupByVariableKind != nil { + out.GroupByVariableKind = &dashv2beta1.DashboardGroupByVariableKind{ + Kind: in.GroupByVariableKind.Kind, + Group: *in.GroupByVariableKind.Spec.Datasource.Type, + Datasource: &dashv2beta1.DashboardV2beta1GroupByVariableKindDatasource{ + Name: in.GroupByVariableKind.Spec.Datasource.Uid, + }, + Spec: dashv2beta1.DashboardGroupByVariableSpec{ + Name: in.GroupByVariableKind.Spec.Name, + DefaultValue: convertVariableOptionPtr_V2alpha1_to_V2beta1(in.GroupByVariableKind.Spec.DefaultValue), + Current: convertVariableOption_V2alpha1_to_V2beta1(in.GroupByVariableKind.Spec.Current), + Options: convertVariableOptions_V2alpha1_to_V2beta1(in.GroupByVariableKind.Spec.Options), + Multi: in.GroupByVariableKind.Spec.Multi, + Label: in.GroupByVariableKind.Spec.Label, + Hide: dashv2beta1.DashboardVariableHide(in.GroupByVariableKind.Spec.Hide), + SkipUrlSync: in.GroupByVariableKind.Spec.SkipUrlSync, + Description: in.GroupByVariableKind.Spec.Description, + }, + } + } + + if in.AdhocVariableKind != nil { + out.AdhocVariableKind = &dashv2beta1.DashboardAdhocVariableKind{ + Kind: in.AdhocVariableKind.Kind, + Group: *in.AdhocVariableKind.Spec.Datasource.Type, + Datasource: &dashv2beta1.DashboardV2beta1AdhocVariableKindDatasource{ + Name: in.AdhocVariableKind.Spec.Datasource.Uid, + }, + Spec: dashv2beta1.DashboardAdhocVariableSpec{ + Name: in.AdhocVariableKind.Spec.Name, + BaseFilters: convertAdHocFilters_V2alpha1_to_V2beta1(in.AdhocVariableKind.Spec.BaseFilters), + Filters: convertAdHocFilters_V2alpha1_to_V2beta1(in.AdhocVariableKind.Spec.Filters), + DefaultKeys: convertMetricFindValues_V2alpha1_to_V2beta1(in.AdhocVariableKind.Spec.DefaultKeys), + Label: in.AdhocVariableKind.Spec.Label, + Hide: dashv2beta1.DashboardVariableHide(in.AdhocVariableKind.Spec.Hide), + SkipUrlSync: in.AdhocVariableKind.Spec.SkipUrlSync, + Description: in.AdhocVariableKind.Spec.Description, + AllowCustomValue: in.AdhocVariableKind.Spec.AllowCustomValue, + }, + } + } + + return nil +} + +func convertQueryVariableSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardQueryVariableSpec, out *dashv2beta1.DashboardQueryVariableSpec, scope conversion.Scope) error { + out.Name = in.Name + out.Current = convertVariableOption_V2alpha1_to_V2beta1(in.Current) + out.Label = in.Label + out.Hide = dashv2beta1.DashboardVariableHide(in.Hide) + out.Refresh = dashv2beta1.DashboardVariableRefresh(in.Refresh) + out.SkipUrlSync = in.SkipUrlSync + out.Description = in.Description + out.Regex = in.Regex + out.Sort = dashv2beta1.DashboardVariableSort(in.Sort) + out.Definition = in.Definition + out.Options = convertVariableOptions_V2alpha1_to_V2beta1(in.Options) + out.Multi = in.Multi + out.IncludeAll = in.IncludeAll + out.AllValue = in.AllValue + out.Placeholder = in.Placeholder + out.AllowCustomValue = in.AllowCustomValue + out.StaticOptions = convertVariableOptions_V2alpha1_to_V2beta1(in.StaticOptions) + out.StaticOptionsOrder = (*dashv2beta1.DashboardQueryVariableSpecStaticOptionsOrder)(in.StaticOptionsOrder) + + // Convert query - move datasource from variable spec to query + if err := convertDataQuery_V2alpha1_to_V2beta1(&in.Query, &out.Query, in.Datasource, scope); err != nil { + return err + } + + return nil +} + +func convertVariableOption_V2alpha1_to_V2beta1(in dashv2alpha1.DashboardVariableOption) dashv2beta1.DashboardVariableOption { + return dashv2beta1.DashboardVariableOption{ + Selected: in.Selected, + Text: dashv2beta1.DashboardStringOrArrayOfString(in.Text), + Value: dashv2beta1.DashboardStringOrArrayOfString(in.Value), + } +} + +func convertVariableOptionPtr_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableOption) *dashv2beta1.DashboardVariableOption { + if in == nil { + return nil + } + out := convertVariableOption_V2alpha1_to_V2beta1(*in) + return &out +} + +func convertVariableOptions_V2alpha1_to_V2beta1(in []dashv2alpha1.DashboardVariableOption) []dashv2beta1.DashboardVariableOption { + if in == nil { + return nil + } + out := make([]dashv2beta1.DashboardVariableOption, len(in)) + for i, option := range in { + out[i] = convertVariableOption_V2alpha1_to_V2beta1(option) + } + return out +} + +func convertAdHocFilters_V2alpha1_to_V2beta1(in []dashv2alpha1.DashboardAdHocFilterWithLabels) []dashv2beta1.DashboardAdHocFilterWithLabels { + if in == nil { + return nil + } + out := make([]dashv2beta1.DashboardAdHocFilterWithLabels, len(in)) + for i, filter := range in { + out[i] = dashv2beta1.DashboardAdHocFilterWithLabels{ + Key: filter.Key, + Operator: filter.Operator, + Value: filter.Value, + Values: filter.Values, + KeyLabel: filter.KeyLabel, + ValueLabels: filter.ValueLabels, + ForceEdit: filter.ForceEdit, + Origin: filter.Origin, + Condition: filter.Condition, + } + } + return out +} + +func convertMetricFindValues_V2alpha1_to_V2beta1(in []dashv2alpha1.DashboardMetricFindValue) []dashv2beta1.DashboardMetricFindValue { + if in == nil { + return nil + } + out := make([]dashv2beta1.DashboardMetricFindValue, len(in)) + for i, value := range in { + out[i] = dashv2beta1.DashboardMetricFindValue{ + Text: value.Text, + Value: (*dashv2beta1.DashboardStringOrFloat64)(value.Value), + Group: value.Group, + Expandable: value.Expandable, + } + } + return out +} + +func convertRepeatOptions_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardRepeatOptions) *dashv2beta1.DashboardRepeatOptions { + if in == nil { + return nil + } + return &dashv2beta1.DashboardRepeatOptions{ + Mode: in.Mode, + Value: in.Value, + Direction: (*dashv2beta1.DashboardRepeatOptionsDirection)(in.Direction), + MaxPerRow: in.MaxPerRow, + } +} + +func convertRowRepeatOptions_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardRowRepeatOptions) *dashv2beta1.DashboardRowRepeatOptions { + if in == nil { + return nil + } + return &dashv2beta1.DashboardRowRepeatOptions{ + Mode: in.Mode, + Value: in.Value, + } +} + +func convertAutoGridRepeatOptions_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardAutoGridRepeatOptions) *dashv2beta1.DashboardAutoGridRepeatOptions { + if in == nil { + return nil + } + return &dashv2beta1.DashboardAutoGridRepeatOptions{ + Mode: in.Mode, + Value: in.Value, + } +} + +func convertTabRepeatOptions_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardTabRepeatOptions) *dashv2beta1.DashboardTabRepeatOptions { + if in == nil { + return nil + } + return &dashv2beta1.DashboardTabRepeatOptions{ + Mode: in.Mode, + Value: in.Value, + } +} + +func convertConditionalRenderingGroupKind_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardConditionalRenderingGroupKind) *dashv2beta1.DashboardConditionalRenderingGroupKind { + if in == nil { + return nil + } + + out := &dashv2beta1.DashboardConditionalRenderingGroupKind{ + Kind: in.Kind, + Spec: dashv2beta1.DashboardConditionalRenderingGroupSpec{ + Visibility: dashv2beta1.DashboardConditionalRenderingGroupSpecVisibility(in.Spec.Visibility), + Condition: dashv2beta1.DashboardConditionalRenderingGroupSpecCondition(in.Spec.Condition), + Items: make([]dashv2beta1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind, len(in.Spec.Items)), + }, + } + + // Convert each item in the Items slice + for i, item := range in.Spec.Items { + out.Spec.Items[i] = dashv2beta1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind{} + + if item.ConditionalRenderingVariableKind != nil { + out.Spec.Items[i].ConditionalRenderingVariableKind = &dashv2beta1.DashboardConditionalRenderingVariableKind{ + Kind: item.ConditionalRenderingVariableKind.Kind, + Spec: dashv2beta1.DashboardConditionalRenderingVariableSpec{ + Variable: item.ConditionalRenderingVariableKind.Spec.Variable, + Operator: dashv2beta1.DashboardConditionalRenderingVariableSpecOperator(item.ConditionalRenderingVariableKind.Spec.Operator), + Value: item.ConditionalRenderingVariableKind.Spec.Value, + }, + } + } + + if item.ConditionalRenderingDataKind != nil { + out.Spec.Items[i].ConditionalRenderingDataKind = &dashv2beta1.DashboardConditionalRenderingDataKind{ + Kind: item.ConditionalRenderingDataKind.Kind, + Spec: dashv2beta1.DashboardConditionalRenderingDataSpec{ + Value: item.ConditionalRenderingDataKind.Spec.Value, + }, + } + } + + if item.ConditionalRenderingTimeRangeSizeKind != nil { + out.Spec.Items[i].ConditionalRenderingTimeRangeSizeKind = &dashv2beta1.DashboardConditionalRenderingTimeRangeSizeKind{ + Kind: item.ConditionalRenderingTimeRangeSizeKind.Kind, + Spec: dashv2beta1.DashboardConditionalRenderingTimeRangeSizeSpec{ + Value: item.ConditionalRenderingTimeRangeSizeKind.Spec.Value, + }, + } + } + } + + return out +} + +func convertRowLayout_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, out *dashv2beta1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, scope conversion.Scope) error { + // Handle the different union type orderings by converting through the main layout function + // Create a temporary variable with the correct type ordering + var tempIn dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind + var tempOut dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind + + // Copy the layout data with the correct ordering + if in.GridLayoutKind != nil { + tempIn.GridLayoutKind = in.GridLayoutKind + } + if in.RowsLayoutKind != nil { + tempIn.RowsLayoutKind = in.RowsLayoutKind + } + if in.AutoGridLayoutKind != nil { + tempIn.AutoGridLayoutKind = in.AutoGridLayoutKind + } + if in.TabsLayoutKind != nil { + tempIn.TabsLayoutKind = in.TabsLayoutKind + } + + if err := convertLayout_V2alpha1_to_V2beta1(&tempIn, &tempOut, scope); err != nil { + return err + } + + // Copy back to the output with the correct ordering + if tempOut.GridLayoutKind != nil { + out.GridLayoutKind = tempOut.GridLayoutKind + } + if tempOut.RowsLayoutKind != nil { + out.RowsLayoutKind = tempOut.RowsLayoutKind + } + if tempOut.AutoGridLayoutKind != nil { + out.AutoGridLayoutKind = tempOut.AutoGridLayoutKind + } + if tempOut.TabsLayoutKind != nil { + out.TabsLayoutKind = tempOut.TabsLayoutKind + } + + return nil +} + +func convertTabLayout_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, out *dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, scope conversion.Scope) error { + // Handle the different union type orderings by converting through the main layout function + // Create a temporary variable with the correct type ordering + var tempIn dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind + var tempOut dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind + + // Copy the layout data with the correct ordering + if in.GridLayoutKind != nil { + tempIn.GridLayoutKind = in.GridLayoutKind + } + if in.RowsLayoutKind != nil { + tempIn.RowsLayoutKind = in.RowsLayoutKind + } + if in.AutoGridLayoutKind != nil { + tempIn.AutoGridLayoutKind = in.AutoGridLayoutKind + } + if in.TabsLayoutKind != nil { + tempIn.TabsLayoutKind = in.TabsLayoutKind + } + + if err := convertLayout_V2alpha1_to_V2beta1(&tempIn, &tempOut, scope); err != nil { + return err + } + + // Copy back to the output with the correct ordering + if tempOut.GridLayoutKind != nil { + out.GridLayoutKind = tempOut.GridLayoutKind + } + if tempOut.RowsLayoutKind != nil { + out.RowsLayoutKind = tempOut.RowsLayoutKind + } + if tempOut.AutoGridLayoutKind != nil { + out.AutoGridLayoutKind = tempOut.AutoGridLayoutKind + } + if tempOut.TabsLayoutKind != nil { + out.TabsLayoutKind = tempOut.TabsLayoutKind + } + + return nil +} diff --git a/conf/provisioning/sample/dashboard-v1-annotations.json b/conf/provisioning/sample/dashboard-v1-annotations.json new file mode 100644 index 00000000000..7e90452a2a5 --- /dev/null +++ b/conf/provisioning/sample/dashboard-v1-annotations.json @@ -0,0 +1,257 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "test-v1-annotations", + "annotations": { + "hello": "world" + }, + "labels": { + "region": "west" + } + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "target": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "target": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "prom-annos", + "target": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "no-ds-prom-annos", + "target": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PBBCEC2D313BC06C3" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "target": { + "editorMode": "builder", + "format": "table", + "lines": 10, + "rawSql": "", + "refId": "Anno", + "scenarioId": "annotations", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + }, + { + "datasource": { + "type": "elasticsearch", + "uid": "gdev-elasticsearch" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "elastic - annos", + "tagsField": "asd", + "target": { + "lines": 10, + "query": "test query", + "refId": "Anno", + "scenarioId": "annotations" + }, + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "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": 0 + }, + { + "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": "12.1.0-pre", + "targets": [ + { + "refId": "A" + } + ], + "title": "New panel", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Test: V1 dashboard with annotations", + "version": 8 + } +} diff --git a/conf/provisioning/sample/dashboard-v1beta1.json b/conf/provisioning/sample/dashboard-v1beta1.json new file mode 100644 index 00000000000..1d236620b5e --- /dev/null +++ b/conf/provisioning/sample/dashboard-v1beta1.json @@ -0,0 +1,257 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "test-v1beta1-annotations", + "annotations": { + "hello": "world" + }, + "labels": { + "region": "west" + } + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "target": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "target": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "prom-annos", + "target": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "no-ds-prom-annos", + "target": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PBBCEC2D313BC06C3" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "target": { + "editorMode": "builder", + "format": "table", + "lines": 10, + "rawSql": "", + "refId": "Anno", + "scenarioId": "annotations", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + }, + { + "datasource": { + "type": "elasticsearch", + "uid": "gdev-elasticsearch" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "elastic - annos", + "tagsField": "asd", + "target": { + "lines": 10, + "query": "test query", + "refId": "Anno", + "scenarioId": "annotations" + }, + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "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": 0 + }, + { + "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": "12.1.0-pre", + "targets": [ + { + "refId": "A" + } + ], + "title": "New panel", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Test: v1alpha1 dashboard with annotations", + "version": 8 + } +} diff --git a/conf/provisioning/sample/dashboard-v2alpha1.json b/conf/provisioning/sample/dashboard-v2alpha1.json new file mode 100644 index 00000000000..b078428c864 --- /dev/null +++ b/conf/provisioning/sample/dashboard-v2alpha1.json @@ -0,0 +1,1025 @@ +{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": { + "name": "test-v2alpha1-annotations" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": true, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "query": { + "kind": "grafana", + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "query": { + "kind": "grafana", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "prom-annos", + "query": { + "kind": "prometheus", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "no-ds-prom-annos", + "query": { + "kind": "grafana", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PBBCEC2D313BC06C3" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "query": { + "kind": "grafana-postgresql-datasource", + "spec": { + "editorMode": "builder", + "format": "table", + "lines": 10, + "rawSql": "", + "refId": "Anno", + "scenarioId": "annotations", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": false, + "datasource": { + "type": "elasticsearch", + "uid": "gdev-elasticsearch" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "legacyOptions": { + "tagsField": "asd", + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" + }, + "name": "elastic - annos", + "query": { + "kind": "elasticsearch", + "spec": { + "lines": 10, + "query": "test query", + "refId": "Anno", + "scenarioId": "annotations" + } + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "datasource": { + "uid": "gdev-testdata" + }, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 1, + "links": [], + "title": "With ds ref (gdev-testdata) -> use ds ref", + "vizConfig": { + "kind": "timeseries", + "spec": { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 4 + } + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "No ds ref -> fallback to first testdata datasource", + "vizConfig": { + "kind": "stat", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 3, + "links": [], + "title": "No ds ref -> fallback to first prometheus datasource or default", + "vizConfig": { + "kind": "timeseries", + "spec": { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "datasource": { + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 4, + "links": [], + "title": "With ds ref (gdev-prometheus) -> use ds ref", + "vizConfig": { + "kind": "timeseries", + "spec": { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "datasource": { + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A" + } + }, + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "datasource": { + "uid": "gdev-testdata" + }, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "B" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 5, + "links": [], + "title": "Mixed with ds refs -> use ds ref", + "vizConfig": { + "kind": "timeseries", + "spec": { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A" + } + }, + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "B" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 6, + "links": [], + "title": "Mixed without ds refs -> use first datasource of type or default", + "vizConfig": { + "kind": "timeseries", + "spec": { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "columnWidthMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ], + "maxColumnCount": 3, + "rowHeightMode": "standard" + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "autoRefresh": "", + "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "fiscalYearStartMonth": 0, + "from": "now-6h", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "Test: v2alpha1 dashboard with annotations", + "description": "this dasboard contains cases for migrating to v2beta1", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": "200", + "value": "200" + }, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "definition": "label_values(status_code)", + "description": "Prometheus query variable with reference", + "hide": "dontHide", + "includeAll": false, + "multi": false, + "name": "promDsVar", + "options": [], + "query": { + "kind": "prometheus", + "spec": { + "qryType": 1, + "query": "label_values(status_code)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + } + }, + "refresh": "onDashboardLoad", + "regex": "", + "skipUrlSync": false, + "sort": "disabled" + } + }, + { + "kind": "AdhocVariable", + "spec": { + "allowCustomValue": true, + "baseFilters": [], + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "defaultKeys": [], + "filters": [], + "hide": "dontHide", + "name": "filtersProm", + "skipUrlSync": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "includeAll": false, + "multi": false, + "name": "query0", + "options": [], + "query": { + "kind": "grafana", + "spec": { + "__legacyStringValue": "" + } + }, + "refresh": "onDashboardLoad", + "regex": "", + "skipUrlSync": false, + "sort": "disabled" + } + }, + { + "kind": "GroupByVariable", + "spec": { + "current": { + "text": [], + "value": [] + }, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "hide": "dontHide", + "multi": true, + "name": "groupByWithDs", + "options": [], + "skipUrlSync": false + } + } + ] + } +} diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts index f42082587d8..95651fc869e 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts @@ -23,6 +23,8 @@ test.describe( await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard)); await page.getByTestId(selectors.components.DashboardImportPage.submit).click(); await page.getByTestId(selectors.components.ImportDashboardForm.name).fill('Paste tab'); + await page.getByTestId(selectors.components.DataSourcePicker.inputV2).click(); + await page.locator('div[data-testid="data-source-card"]').first().click(); await page.getByTestId(selectors.components.ImportDashboardForm.submit).click(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts index 80f94f855b2..12023c9de6c 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -29,6 +29,8 @@ test.describe( await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard)); await page.getByTestId(selectors.components.DashboardImportPage.submit).click(); await page.getByTestId(selectors.components.ImportDashboardForm.name).fill(title); + await page.getByTestId(selectors.components.DataSourcePicker.inputV2).click(); + await page.locator('div[data-testid="data-source-card"]').first().click(); await page.getByTestId(selectors.components.ImportDashboardForm.submit).click(); } diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts index d91e5ba59f2..78889efbbbe 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts @@ -381,6 +381,8 @@ async function importTestDashboard(page: Page, selectors: E2ESelectorGroups, tit await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard)); await page.getByTestId(selectors.components.DashboardImportPage.submit).click(); await page.getByTestId(selectors.components.ImportDashboardForm.name).fill(title); + await page.getByTestId(selectors.components.DataSourcePicker.inputV2).click(); + await page.locator('div[data-testid="data-source-card"]').first().click(); await page.getByTestId(selectors.components.ImportDashboardForm.submit).click(); } diff --git a/e2e-playwright/dashboards/TestV2Dashboard.json b/e2e-playwright/dashboards/TestV2Dashboard.json index da89762c3a6..25917da59fa 100644 --- a/e2e-playwright/dashboards/TestV2Dashboard.json +++ b/e2e-playwright/dashboards/TestV2Dashboard.json @@ -1,22 +1,12 @@ { - "apiVersion": "dashboard.grafana.app/v2alpha1", + "apiVersion": "dashboard.grafana.app/v2beta1", "kind": "Dashboard", "metadata": { - "name": "admjzp8", - "namespace": "default", - "uid": "hrbekBWXeM7nC7GtouIbcngFyuqt8Xx7KlE4AnTwV7AX", - "resourceVersion": "1", + "name": "fa400625-2a44-4add-a369-e6c972eb4bd6", "generation": 1, - "creationTimestamp": "2025-05-16T09:41:56Z", - "labels": { - "grafana.app/deprecatedInternalID": "182" - }, - "annotations": { - "grafana.app/createdBy": "user:cejvsh18uudxcf", - "grafana.app/updatedBy": "user:cejvsh18uudxcf", - "grafana.app/updatedTimestamp": "2025-05-16T09:41:56Z", - "grafana.app/folder": "" - } + "creationTimestamp": "2025-05-27T11:40:22Z", + "labels": {}, + "annotations": {} }, "spec": { "annotations": [ @@ -24,19 +14,20 @@ "kind": "AnnotationQuery", "spec": { "builtIn": true, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts" + "name": "Annotations & Alerts", + "query": { + "group": "grafana", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + } } } ], "cursorSync": "Off", - "description": "", "editable": true, "elements": { "panel-1": { @@ -49,14 +40,12 @@ { "kind": "PanelQuery", "spec": { - "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" - }, "hidden": false, "query": { - "kind": "grafana-testdata-datasource", - "spec": {} + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" }, "refId": "A" } @@ -155,14 +144,12 @@ { "kind": "PanelQuery", "spec": { - "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" - }, "hidden": false, "query": { - "kind": "grafana-testdata-datasource", - "spec": {} + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" }, "refId": "A" } @@ -261,14 +248,12 @@ { "kind": "PanelQuery", "spec": { - "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" - }, "hidden": false, "query": { - "kind": "grafana-testdata-datasource", - "spec": {} + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" }, "refId": "A" } @@ -380,7 +365,7 @@ "spec": { "element": { "kind": "ElementReference", - "name": "panel-2" + "name": "panel-3" }, "height": 8, "width": 12, @@ -393,7 +378,7 @@ "spec": { "element": { "kind": "ElementReference", - "name": "panel-3" + "name": "panel-2" }, "height": 8, "width": 12, @@ -417,7 +402,7 @@ "timezone": "browser", "to": "now" }, - "title": "New Test V2 Dashboard", + "title": "Test V2 Dashboard", "variables": [] }, "status": {} diff --git a/e2e/dashboards/TestV2Dashboard.json b/e2e/dashboards/TestV2Dashboard.json index da89762c3a6..25917da59fa 100644 --- a/e2e/dashboards/TestV2Dashboard.json +++ b/e2e/dashboards/TestV2Dashboard.json @@ -1,22 +1,12 @@ { - "apiVersion": "dashboard.grafana.app/v2alpha1", + "apiVersion": "dashboard.grafana.app/v2beta1", "kind": "Dashboard", "metadata": { - "name": "admjzp8", - "namespace": "default", - "uid": "hrbekBWXeM7nC7GtouIbcngFyuqt8Xx7KlE4AnTwV7AX", - "resourceVersion": "1", + "name": "fa400625-2a44-4add-a369-e6c972eb4bd6", "generation": 1, - "creationTimestamp": "2025-05-16T09:41:56Z", - "labels": { - "grafana.app/deprecatedInternalID": "182" - }, - "annotations": { - "grafana.app/createdBy": "user:cejvsh18uudxcf", - "grafana.app/updatedBy": "user:cejvsh18uudxcf", - "grafana.app/updatedTimestamp": "2025-05-16T09:41:56Z", - "grafana.app/folder": "" - } + "creationTimestamp": "2025-05-27T11:40:22Z", + "labels": {}, + "annotations": {} }, "spec": { "annotations": [ @@ -24,19 +14,20 @@ "kind": "AnnotationQuery", "spec": { "builtIn": true, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts" + "name": "Annotations & Alerts", + "query": { + "group": "grafana", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + } } } ], "cursorSync": "Off", - "description": "", "editable": true, "elements": { "panel-1": { @@ -49,14 +40,12 @@ { "kind": "PanelQuery", "spec": { - "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" - }, "hidden": false, "query": { - "kind": "grafana-testdata-datasource", - "spec": {} + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" }, "refId": "A" } @@ -155,14 +144,12 @@ { "kind": "PanelQuery", "spec": { - "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" - }, "hidden": false, "query": { - "kind": "grafana-testdata-datasource", - "spec": {} + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" }, "refId": "A" } @@ -261,14 +248,12 @@ { "kind": "PanelQuery", "spec": { - "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" - }, "hidden": false, "query": { - "kind": "grafana-testdata-datasource", - "spec": {} + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" }, "refId": "A" } @@ -380,7 +365,7 @@ "spec": { "element": { "kind": "ElementReference", - "name": "panel-2" + "name": "panel-3" }, "height": 8, "width": 12, @@ -393,7 +378,7 @@ "spec": { "element": { "kind": "ElementReference", - "name": "panel-3" + "name": "panel-2" }, "height": 8, "width": 12, @@ -417,7 +402,7 @@ "timezone": "browser", "to": "now" }, - "title": "New Test V2 Dashboard", + "title": "Test V2 Dashboard", "variables": [] }, "status": {} diff --git a/e2e/utils/flows/scenes/importDashboard.ts b/e2e/utils/flows/scenes/importDashboard.ts index e0b0a8ae929..a916f733d06 100644 --- a/e2e/utils/flows/scenes/importDashboard.ts +++ b/e2e/utils/flows/scenes/importDashboard.ts @@ -17,5 +17,9 @@ export const importV2Dashboard = ({ title }: ImportDashboardConfig) => { if (title) { e2e.components.ImportDashboardForm.name().clear().type(title); } + + e2e.components.DataSourcePicker.inputV2().click(); + cy.get('div[data-testid="data-source-card"]').first().click(); + e2e.components.ImportDashboardForm.submit().click(); }; diff --git a/go.work.sum b/go.work.sum index 4cf90b4d4d2..cd47b184cb0 100644 --- a/go.work.sum +++ b/go.work.sum @@ -579,7 +579,6 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0= github.com/RoaringBitmap/gocroaring v0.4.0 h1:5nufXUgWpBEUNEJXw7926YAA58ZAQRpWPrQV1xCoSjc= github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76 h1:ZYlhPbqQFU+AHfgtCdHGDTtRW1a8geZyiE8c6Q+Sl1s= -github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76/go.mod h1:oM0MHmQ3nDsq609SS36p+oYbRi16+oVvU2Bw4Ipv0SE= github.com/Sereal/Sereal/Go/sereal v0.0.0-20231009093132-b9187f1a92c6 h1:5kUcJJAKWWI82Xnp/CaU0eu5hLlHkmm9acjowSkwCd0= github.com/Sereal/Sereal/Go/sereal v0.0.0-20231009093132-b9187f1a92c6/go.mod h1:JwrycNnC8+sZPDyzM3MQ86LvaGzSpfxg885KOOwFRW4= github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= @@ -971,8 +970,6 @@ github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVn github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= github.com/grafana/grafana-aws-sdk v0.38.2 h1:TzQD0OpWsNjtldi5G5TLDlBRk8OyDf+B5ujcoAu4Dp0= github.com/grafana/grafana-aws-sdk v0.38.2/go.mod h1:j3vi+cXYHEFqjhBGrI6/lw1TNM+dl0Y3f0cSnDOPy+s= -github.com/grafana/grafana-aws-sdk v1.0.2 h1:98eBuHYFmgvH0xO9kKf4RBsEsgQRp8EOA/9yhDIpkss= -github.com/grafana/grafana-aws-sdk v1.0.2/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana-plugin-sdk-go v0.269.1/go.mod h1:yv2KbO4mlr9WuDK2f+2gHAMTwwLmLuqaEnrPXTRU+OI= @@ -1668,7 +1665,6 @@ golang.org/x/mod v0.13.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/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= diff --git a/packages/grafana-schema/src/schema/dashboard/v2.ts b/packages/grafana-schema/src/schema/dashboard/v2.ts new file mode 100644 index 00000000000..8d4716db7da --- /dev/null +++ b/packages/grafana-schema/src/schema/dashboard/v2.ts @@ -0,0 +1,3 @@ +export * from './v2beta1/types.spec.gen'; +export * from './v2beta1/types.status.gen'; +export * from './v2beta1/types.metadata.gen'; diff --git a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts index 4bf078783cb..651d858e799 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts @@ -1,4 +1,4 @@ -import { Spec } from './v2alpha1/types.spec.gen'; +import { defaultDataQueryKind, Spec } from './v2'; export const handyTestingSchema: Spec = { title: 'Default Dashboard', @@ -37,15 +37,16 @@ export const handyTestingSchema: Spec = { spec: { builtIn: false, query: { - kind: 'prometheus', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', + datasource: { + name: 'uid', + }, spec: { expr: 'test-query', }, }, - datasource: { - type: 'prometheus', - uid: 'uid', - }, filter: { ids: [1] }, enable: true, hide: false, @@ -57,15 +58,16 @@ export const handyTestingSchema: Spec = { kind: 'AnnotationQuery', spec: { builtIn: false, - datasource: { - type: 'grafana-testdata-datasource', - uid: 'uid', - }, enable: true, iconColor: 'red', name: 'Enabled', query: { - kind: 'grafana-testdata-datasource', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'grafana-testdata-datasource', + datasource: { + name: 'uid', + }, spec: { lines: 4, refId: 'Anno', @@ -79,15 +81,16 @@ export const handyTestingSchema: Spec = { kind: 'AnnotationQuery', spec: { builtIn: false, - datasource: { - type: 'grafana-testdata-datasource', - uid: 'uid', - }, enable: false, iconColor: 'yellow', name: 'Disabled', query: { - kind: 'grafana-testdata-datasource', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'grafana-testdata-datasource', + datasource: { + name: 'uid', + }, spec: { lines: 5, refId: 'Anno', scenarioId: 'annotations' }, }, hide: false, @@ -97,16 +100,17 @@ export const handyTestingSchema: Spec = { kind: 'AnnotationQuery', spec: { builtIn: false, - datasource: { - type: 'grafana-testdata-datasource', - uid: 'uid', - }, enable: true, hide: true, iconColor: 'dark-purple', name: 'Hidden', query: { - kind: 'grafana-testdata-datasource', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'grafana-testdata-datasource', + datasource: { + name: 'uid', + }, spec: { lines: 6, refId: 'Anno', @@ -128,12 +132,13 @@ export const handyTestingSchema: Spec = { kind: 'PanelQuery', spec: { refId: 'A', - datasource: { - type: 'prometheus', - uid: 'datasource1', - }, query: { - kind: 'prometheus', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', + datasource: { + name: 'datasource1', + }, spec: { expr: 'test-query', }, @@ -179,14 +184,15 @@ export const handyTestingSchema: Spec = { title: 'Test Panel', id: 1, vizConfig: { - kind: 'timeseries', + kind: 'VizConfig', + group: 'timeseries', + version: '7.0.0', spec: { fieldConfig: { defaults: {}, overrides: [], }, options: {}, - pluginVersion: '7.0.0', }, }, }, @@ -265,10 +271,6 @@ export const handyTestingSchema: Spec = { text: 'text1', value: 'value1', }, - datasource: { - type: 'prometheus', - uid: 'datasource1', - }, definition: 'definition1', description: 'A query variable', hide: 'dontHide', @@ -278,7 +280,12 @@ export const handyTestingSchema: Spec = { name: 'queryVar', options: [], query: { - kind: 'prometheus', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', + datasource: { + name: 'datasource1', + }, spec: { expr: 'test-query', refId: 'A', @@ -411,15 +418,15 @@ export const handyTestingSchema: Spec = { }, { kind: 'GroupByVariable', + group: 'prometheus', + datasource: { + name: 'datasource2', + }, spec: { current: { text: 'text7', value: 'value7', }, - datasource: { - type: 'prometheus', - uid: 'datasource2', - }, description: 'A group by variable', hide: 'dontHide', label: 'Group By Variable', @@ -440,6 +447,10 @@ export const handyTestingSchema: Spec = { }, { kind: 'AdhocVariable', + group: 'prometheus', + datasource: { + name: 'datasource3', + }, spec: { baseFilters: [ { @@ -455,10 +466,6 @@ export const handyTestingSchema: Spec = { value: 'value2', }, ], - datasource: { - type: 'prometheus', - uid: 'datasource3', - }, defaultKeys: [ { expandable: true, diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha2/dashboard_object_gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/dashboard_object_gen.ts similarity index 100% rename from packages/grafana-schema/src/schema/dashboard/v2alpha2/dashboard_object_gen.ts rename to packages/grafana-schema/src/schema/dashboard/v2beta1/dashboard_object_gen.ts diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.metadata.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.metadata.gen.ts similarity index 100% rename from packages/grafana-schema/src/schema/dashboard/v2alpha2/types.metadata.gen.ts rename to packages/grafana-schema/src/schema/dashboard/v2beta1/types.metadata.gen.ts diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts similarity index 98% rename from packages/grafana-schema/src/schema/dashboard/v2alpha2/types.spec.gen.ts rename to packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts index 04fc5092d8a..a6140372eb9 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts @@ -11,19 +11,19 @@ export const defaultAnnotationQueryKind = (): AnnotationQueryKind => ({ }); export interface AnnotationQuerySpec { - datasource?: DataSourceRef; - query?: DataQueryKind; + query: DataQueryKind; enable: boolean; hide: boolean; iconColor: string; name: string; builtIn?: boolean; filter?: AnnotationPanelFilter; - // Catch-all field for datasource-specific properties + // Catch-all field for datasource-specific properties. Should not be available in as code tooling. legacyOptions?: Record; } export const defaultAnnotationQuerySpec = (): AnnotationQuerySpec => ({ + query: defaultDataQueryKind(), enable: false, hide: false, iconColor: "", @@ -31,24 +31,22 @@ export const defaultAnnotationQuerySpec = (): AnnotationQuerySpec => ({ builtIn: false, }); -export interface DataSourceRef { - // The plugin type-id - type?: string; - // Specific datasource instance - uid?: string; -} - -export const defaultDataSourceRef = (): DataSourceRef => ({ -}); - export interface DataQueryKind { - // The kind of a DataQueryKind is the datasource type - kind: string; + kind: "DataQuery"; + group: string; + version: string; + // New type for datasource reference + // Not creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS. + datasource?: { + name?: string; + }; spec: Record; } export const defaultDataQueryKind = (): DataQueryKind => ({ - kind: "", + kind: "DataQuery", + group: "", + version: "v0", spec: {}, }); @@ -151,7 +149,6 @@ export const defaultPanelQueryKind = (): PanelQueryKind => ({ export interface PanelQuerySpec { query: DataQueryKind; - datasource?: DataSourceRef; refId: string; hidden: boolean; } @@ -228,25 +225,27 @@ export const defaultQueryOptionsSpec = (): QueryOptionsSpec => ({ }); export interface VizConfigKind { - // The kind of a VizConfigKind is the plugin ID - kind: string; + kind: "VizConfig"; + // The group is the plugin ID + group: string; + version: string; spec: VizConfigSpec; } export const defaultVizConfigKind = (): VizConfigKind => ({ - kind: "", + kind: "VizConfig", + group: "", + version: "", spec: defaultVizConfigSpec(), }); // --- Kinds --- export interface VizConfigSpec { - pluginVersion: string; options: Record; fieldConfig: FieldConfigSource; } export const defaultVizConfigSpec = (): VizConfigSpec => ({ - pluginVersion: "", options: {}, fieldConfig: defaultFieldConfigSource(), }); @@ -994,7 +993,6 @@ export interface QueryVariableSpec { refresh: VariableRefresh; skipUrlSync: boolean; description?: string; - datasource?: DataSourceRef; query: DataQueryKind; regex: string; sort: VariableSort; @@ -1254,18 +1252,22 @@ export const defaultCustomVariableSpec = (): CustomVariableSpec => ({ // Group variable kind export interface GroupByVariableKind { kind: "GroupByVariable"; + group: string; + datasource?: { + name?: string; + }; spec: GroupByVariableSpec; } export const defaultGroupByVariableKind = (): GroupByVariableKind => ({ kind: "GroupByVariable", + group: "", spec: defaultGroupByVariableSpec(), }); // GroupBy variable specification export interface GroupByVariableSpec { name: string; - datasource?: DataSourceRef; defaultValue?: VariableOption; current: VariableOption; options: VariableOption[]; @@ -1288,18 +1290,22 @@ export const defaultGroupByVariableSpec = (): GroupByVariableSpec => ({ // Adhoc variable kind export interface AdhocVariableKind { kind: "AdhocVariable"; + group: string; + datasource?: { + name?: string; + }; spec: AdhocVariableSpec; } export const defaultAdhocVariableKind = (): AdhocVariableKind => ({ kind: "AdhocVariable", + group: "", spec: defaultAdhocVariableSpec(), }); // Adhoc variable specification export interface AdhocVariableSpec { name: string; - datasource?: DataSourceRef; baseFilters: AdHocFilterWithLabels[]; filters: AdHocFilterWithLabels[]; defaultKeys: MetricFindValue[]; diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.status.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.status.gen.ts similarity index 100% rename from packages/grafana-schema/src/schema/dashboard/v2alpha2/types.status.gen.ts rename to packages/grafana-schema/src/schema/dashboard/v2beta1/types.status.gen.ts diff --git a/pkg/registry/apis/dashboard/large.go b/pkg/registry/apis/dashboard/large.go index 21c9d5f8832..e2ff0229721 100644 --- a/pkg/registry/apis/dashboard/large.go +++ b/pkg/registry/apis/dashboard/large.go @@ -9,7 +9,7 @@ import ( dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashboardV2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" commonV0 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/apistore" @@ -42,8 +42,8 @@ func NewDashboardLargeObjectSupport(scheme *runtime.Scheme, threshold int) *apis Description: dash.Spec.Description, Tags: dash.Spec.Tags, } - case *dashboardV2alpha2.Dashboard: - dash.Spec = dashboardV2alpha2.DashboardSpec{ + case *dashboardV2beta1.Dashboard: + dash.Spec = dashboardV2beta1.DashboardSpec{ Title: dash.Spec.Title, Description: dash.Spec.Description, Tags: dash.Spec.Tags, @@ -64,7 +64,7 @@ func NewDashboardLargeObjectSupport(scheme *runtime.Scheme, threshold int) *apis return dash.Spec.UnmarshalJSON(blob) case *dashboardV2alpha1.Dashboard: return json.Unmarshal(blob, &dash.Spec) - case *dashboardV2alpha2.Dashboard: + case *dashboardV2beta1.Dashboard: return json.Unmarshal(blob, &dash.Spec) default: return fmt.Errorf("unsupported dashboard type %T", obj) diff --git a/pkg/registry/apis/dashboard/large_test.go b/pkg/registry/apis/dashboard/large_test.go index fa62d9e62f6..94173ccce31 100644 --- a/pkg/registry/apis/dashboard/large_test.go +++ b/pkg/registry/apis/dashboard/large_test.go @@ -12,7 +12,7 @@ import ( dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" ) func TestLargeDashboardSupportV1(t *testing.T) { @@ -174,47 +174,47 @@ func TestLargeDashboardSupportV2alpha1(t *testing.T) { require.True(t, exists) } -func TestLargeDashboardSupportV2alpha2(t *testing.T) { +func TestLargeDashboardSupportV2beta1(t *testing.T) { // Test RebuildSpec functionality specifically for v2 dashboards // This tests the json.Unmarshal(blob, &dash.Spec) path for structured specs // unlike v0/v1 which use the UnmarshalJSON path for unstructured specs // Create a v2 dashboard with structured spec - originalV2Dash := &dashv2alpha2.Dashboard{ + originalV2Dash := &dashv2beta1.Dashboard{ ObjectMeta: metav1.ObjectMeta{ Name: "test-v2", Namespace: "test", }, - Spec: dashv2alpha2.DashboardSpec{ + Spec: dashv2beta1.DashboardSpec{ Title: "Test V2 Dashboard", Description: stringPtr("A test dashboard for v2 large object support"), Tags: []string{"test", "v2", "large-object"}, Editable: boolPtr(true), LiveNow: boolPtr(false), Preload: false, - Annotations: []dashv2alpha2.DashboardAnnotationQueryKind{ + Annotations: []dashv2beta1.DashboardAnnotationQueryKind{ { Kind: "AnnotationQuery", - Spec: dashv2alpha2.DashboardAnnotationQuerySpec{ + Spec: dashv2beta1.DashboardAnnotationQuerySpec{ Name: "Test Annotation", }, }, }, - Elements: map[string]dashv2alpha2.DashboardElement{ + Elements: map[string]dashv2beta1.DashboardElement{ "panel-1": { - PanelKind: &dashv2alpha2.DashboardPanelKind{}, + PanelKind: &dashv2beta1.DashboardPanelKind{}, }, }, - Layout: dashv2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{}, - TimeSettings: dashv2alpha2.DashboardTimeSettingsSpec{}, - CursorSync: dashv2alpha2.DashboardDashboardCursorSyncOff, - Variables: []dashv2alpha2.DashboardVariableKind{}, - Links: []dashv2alpha2.DashboardDashboardLink{}, + Layout: dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{}, + TimeSettings: dashv2beta1.DashboardTimeSettingsSpec{}, + CursorSync: dashv2beta1.DashboardDashboardCursorSyncOff, + Variables: []dashv2beta1.DashboardVariableKind{}, + Links: []dashv2beta1.DashboardDashboardLink{}, }, } scheme := runtime.NewScheme() - err := dashv2alpha2.AddToScheme(scheme) + err := dashv2beta1.AddToScheme(scheme) require.NoError(t, err) largeObject := NewDashboardLargeObjectSupport(scheme, 0) @@ -243,7 +243,7 @@ func TestLargeDashboardSupportV2alpha2(t *testing.T) { require.Empty(t, dashToReduce.Spec.Links) // Now test RebuildSpec - this is the key test for v2! - rehydratedDash := &dashv2alpha2.Dashboard{ + rehydratedDash := &dashv2beta1.Dashboard{ ObjectMeta: metav1.ObjectMeta{ Name: "test-v2-rehydrated", Namespace: "test", diff --git a/pkg/registry/apis/dashboard/mutate.go b/pkg/registry/apis/dashboard/mutate.go index 574cf5ea634..e1a7c05618e 100644 --- a/pkg/registry/apis/dashboard/mutate.go +++ b/pkg/registry/apis/dashboard/mutate.go @@ -12,7 +12,7 @@ import ( dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashboardV2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" "github.com/grafana/grafana/apps/dashboard/pkg/migration" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -71,17 +71,17 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute resourceInfo = dashboardV2alpha1.DashboardResourceInfo - case *dashboardV2alpha2.Dashboard: + case *dashboardV2beta1.Dashboard: // Temporary fix: The generator fails to properly initialize this property, so we'll do it here // until the generator is fixed. if v.Spec.Layout.GridLayoutKind == nil && v.Spec.Layout.RowsLayoutKind == nil && v.Spec.Layout.AutoGridLayoutKind == nil && v.Spec.Layout.TabsLayoutKind == nil { - v.Spec.Layout.GridLayoutKind = &dashboardV2alpha2.DashboardGridLayoutKind{ + v.Spec.Layout.GridLayoutKind = &dashboardV2beta1.DashboardGridLayoutKind{ Kind: "GridLayout", - Spec: dashboardV2alpha2.DashboardGridLayoutSpec{}, + Spec: dashboardV2beta1.DashboardGridLayoutSpec{}, } } - resourceInfo = dashboardV2alpha2.DashboardResourceInfo + resourceInfo = dashboardV2beta1.DashboardResourceInfo // Noop for V2 default: diff --git a/pkg/registry/apis/dashboard/mutation_test.go b/pkg/registry/apis/dashboard/mutation_test.go index bfbbb7f0109..b5a85c9d995 100644 --- a/pkg/registry/apis/dashboard/mutation_test.go +++ b/pkg/registry/apis/dashboard/mutation_test.go @@ -13,7 +13,7 @@ import ( dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" "github.com/grafana/grafana/apps/dashboard/pkg/migration" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" @@ -152,9 +152,9 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { expectedTitle: "test123", }, { - name: "v2alpha2 should set layout if it is not set", - inputObj: &dashv2alpha2.Dashboard{ - Spec: dashv2alpha2.DashboardSpec{ + name: "v2beta1 should set layout if it is not set", + inputObj: &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ Title: "test123", }, }, @@ -215,7 +215,7 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { require.Equal(t, schemaversion.LATEST_VERSION, schemaVersion, "dashboard should be migrated to the latest version") } case *dashv2alpha1.Dashboard: - case *dashv2alpha2.Dashboard: + case *dashv2beta1.Dashboard: require.Equal(t, tt.expectedTitle, v.Spec.Title, "title should be set") require.NotNil(t, v.Spec.Layout, "layout should be set") require.NotNil(t, v.Spec.Layout.GridLayoutKind, "layout should be a GridLayout") diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 8bc223b79f8..19108edadf8 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -24,7 +24,7 @@ import ( dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" "github.com/grafana/grafana/apps/dashboard/pkg/migration" "github.com/grafana/grafana/apps/dashboard/pkg/migration/conversion" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -157,20 +157,20 @@ func RegisterAPIService( func (b *DashboardsAPIBuilder) GetGroupVersions() []schema.GroupVersion { if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts) { - // If dashboards v2 is enabled, we want to use v2alpha1 as the default API version. + // If dashboards v2 is enabled, we want to use v2beta1 as the default API version. return []schema.GroupVersion{ + dashv2beta1.DashboardResourceInfo.GroupVersion(), dashv2alpha1.DashboardResourceInfo.GroupVersion(), dashv0.DashboardResourceInfo.GroupVersion(), dashv1.DashboardResourceInfo.GroupVersion(), - dashv2alpha2.DashboardResourceInfo.GroupVersion(), } } return []schema.GroupVersion{ dashv1.DashboardResourceInfo.GroupVersion(), dashv0.DashboardResourceInfo.GroupVersion(), + dashv2beta1.DashboardResourceInfo.GroupVersion(), dashv2alpha1.DashboardResourceInfo.GroupVersion(), - dashv2alpha2.DashboardResourceInfo.GroupVersion(), } } @@ -186,7 +186,7 @@ func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { return err } - if err := dashv2alpha2.AddToScheme(scheme); err != nil { + if err := dashv2beta1.AddToScheme(scheme); err != nil { return err } @@ -401,7 +401,7 @@ func getDashboardProperties(obj runtime.Object) (string, string, error) { case *dashv2alpha1.Dashboard: title = d.Spec.Title refresh = d.Spec.TimeSettings.AutoRefresh - case *dashv2alpha2.Dashboard: + case *dashv2beta1.Dashboard: title = d.Spec.Title refresh = d.Spec.TimeSettings.AutoRefresh default: @@ -483,11 +483,11 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver } if err := b.storageForVersion(apiGroupInfo, opts, largeObjects, - dashv2alpha2.DashboardResourceInfo, + dashv2beta1.DashboardResourceInfo, nil, // do not register library panel func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) { - dto := &dashv2alpha2.DashboardWithAccessInfo{} - dash, ok := obj.(*dashv2alpha2.Dashboard) + dto := &dashv2beta1.DashboardWithAccessInfo{} + dash, ok := obj.(*dashv2beta1.Dashboard) if ok { dto.Dashboard = *dash } @@ -572,7 +572,7 @@ func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefiniti defs := dashv0.GetOpenAPIDefinitions(ref) maps.Copy(defs, dashv1.GetOpenAPIDefinitions(ref)) maps.Copy(defs, dashv2alpha1.GetOpenAPIDefinitions(ref)) - maps.Copy(defs, dashv2alpha2.GetOpenAPIDefinitions(ref)) + maps.Copy(defs, dashv2beta1.GetOpenAPIDefinitions(ref)) return defs } } diff --git a/pkg/registry/apis/dashboard/register_test.go b/pkg/registry/apis/dashboard/register_test.go index 2bccf5a1453..f788d5dbe67 100644 --- a/pkg/registry/apis/dashboard/register_test.go +++ b/pkg/registry/apis/dashboard/register_test.go @@ -14,7 +14,7 @@ import ( dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -177,8 +177,8 @@ func TestDashboardAPIBuilder_GetGroupVersions(t *testing.T) { expected: []schema.GroupVersion{ dashv1.DashboardResourceInfo.GroupVersion(), dashv0.DashboardResourceInfo.GroupVersion(), + dashv2beta1.DashboardResourceInfo.GroupVersion(), dashv2alpha1.DashboardResourceInfo.GroupVersion(), - dashv2alpha2.DashboardResourceInfo.GroupVersion(), }, }, { @@ -189,8 +189,8 @@ func TestDashboardAPIBuilder_GetGroupVersions(t *testing.T) { expected: []schema.GroupVersion{ dashv1.DashboardResourceInfo.GroupVersion(), dashv0.DashboardResourceInfo.GroupVersion(), + dashv2beta1.DashboardResourceInfo.GroupVersion(), dashv2alpha1.DashboardResourceInfo.GroupVersion(), - dashv2alpha2.DashboardResourceInfo.GroupVersion(), }, }, { @@ -199,10 +199,10 @@ func TestDashboardAPIBuilder_GetGroupVersions(t *testing.T) { featuremgmt.FlagDashboardNewLayouts, }, expected: []schema.GroupVersion{ + dashv2beta1.DashboardResourceInfo.GroupVersion(), dashv2alpha1.DashboardResourceInfo.GroupVersion(), dashv0.DashboardResourceInfo.GroupVersion(), dashv1.DashboardResourceInfo.GroupVersion(), - dashv2alpha2.DashboardResourceInfo.GroupVersion(), }, }, } diff --git a/pkg/registry/apis/dashboard/schema_validation.go b/pkg/registry/apis/dashboard/schema_validation.go index 91629e5eeb5..4263d2af388 100644 --- a/pkg/registry/apis/dashboard/schema_validation.go +++ b/pkg/registry/apis/dashboard/schema_validation.go @@ -13,7 +13,7 @@ import ( v0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" v1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" v2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - v2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + v2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/services/featuremgmt" ) @@ -34,7 +34,7 @@ func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj ru case *v1.Dashboard: errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV1) case *v2alpha1.Dashboard: - case *v2alpha2.Dashboard: + case *v2beta1.Dashboard: errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV2) default: return nil, fmt.Errorf("invalid dashboard type: %T", obj) @@ -56,8 +56,8 @@ func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj ru errors, schemaVersionError = v1.ValidateDashboardSpec(v, alwaysLogSchemaValidationErrors) case *v2alpha1.Dashboard: errors = v2alpha1.ValidateDashboardSpec(v) - case *v2alpha2.Dashboard: - errors = v2alpha2.ValidateDashboardSpec(v) + case *v2beta1.Dashboard: + errors = v2beta1.ValidateDashboardSpec(v) } } diff --git a/pkg/registry/apis/provisioning/jobs/export/resources.go b/pkg/registry/apis/provisioning/jobs/export/resources.go index c226cacac00..b986392c3d6 100644 --- a/pkg/registry/apis/provisioning/jobs/export/resources.go +++ b/pkg/registry/apis/provisioning/jobs/export/resources.go @@ -54,9 +54,9 @@ func ExportResources(ctx context.Context, options provisioning.ExportJobOptions, return v2clientAlphaV1.Get(ctx, item.GetName(), metav1.GetOptions{}) } - if strings.HasPrefix(storedVersion, "v2alpha2") { + if strings.HasPrefix(storedVersion, "v2beta1") { if v2clientAlphaV2 == nil { - v2clientAlphaV2, _, err = clients.ForResource(resources.DashboardResourceV2alpha2) + v2clientAlphaV2, _, err = clients.ForResource(resources.DashboardResourceV2beta1) if err != nil { return nil, err } diff --git a/pkg/registry/apis/provisioning/jobs/export/resources_test.go b/pkg/registry/apis/provisioning/jobs/export/resources_test.go index 2e663b64e57..db8209cf414 100644 --- a/pkg/registry/apis/provisioning/jobs/export/resources_test.go +++ b/pkg/registry/apis/provisioning/jobs/export/resources_test.go @@ -434,7 +434,7 @@ func TestExportResources_Dashboards_V2Alpha1_ClientError(t *testing.T) { require.NoError(t, err) } -func TestExportResources_Dashboards_V2Alpha2(t *testing.T) { +func TestExportResources_Dashboards_V2beta1(t *testing.T) { mockItems := []unstructured.Unstructured{ { Object: map[string]interface{}{ @@ -446,7 +446,7 @@ func TestExportResources_Dashboards_V2Alpha2(t *testing.T) { "status": map[string]interface{}{ "conversion": map[string]interface{}{ "failed": true, - "storedVersion": "v2alpha2", + "storedVersion": "v2beta1", }, }, }, @@ -467,9 +467,9 @@ func TestExportResources_Dashboards_V2Alpha2(t *testing.T) { resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil) // Setup v2 client - v2Dashboard := createV2DashboardObject("v2-dashboard", "v2alpha2") + v2Dashboard := createV2DashboardObject("v2-dashboard", "v2beta1") v2Client := &mockDynamicInterface{items: []unstructured.Unstructured{v2Dashboard}} - resourceClients.On("ForResource", resources.DashboardResourceV2alpha2).Return(v2Client, gvk, nil) + resourceClients.On("ForResource", resources.DashboardResourceV2beta1).Return(v2Client, gvk, nil) options := resources.WriteOptions{ Path: "grafana", @@ -482,7 +482,7 @@ func TestExportResources_Dashboards_V2Alpha2(t *testing.T) { require.NoError(t, err) } -func TestExportResources_Dashboards_V2Alpha2_ClientError(t *testing.T) { +func TestExportResources_Dashboards_V2beta1_ClientError(t *testing.T) { mockItems := []unstructured.Unstructured{ { Object: map[string]interface{}{ @@ -494,7 +494,7 @@ func TestExportResources_Dashboards_V2Alpha2_ClientError(t *testing.T) { "status": map[string]interface{}{ "conversion": map[string]interface{}{ "failed": true, - "storedVersion": "v2alpha2", + "storedVersion": "v2beta1", }, }, }, @@ -525,7 +525,7 @@ func TestExportResources_Dashboards_V2Alpha2_ClientError(t *testing.T) { } setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) { - resourceClients.On("ForResource", resources.DashboardResourceV2alpha2).Return(nil, gvk, fmt.Errorf("v2 client error")) + resourceClients.On("ForResource", resources.DashboardResourceV2beta1).Return(nil, gvk, fmt.Errorf("v2 client error")) resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil) } diff --git a/pkg/registry/apis/provisioning/resources/client.go b/pkg/registry/apis/provisioning/resources/client.go index 2f6410c349c..e758eaf32ff 100644 --- a/pkg/registry/apis/provisioning/resources/client.go +++ b/pkg/registry/apis/provisioning/resources/client.go @@ -12,7 +12,7 @@ import ( dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashboardV2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" iam "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver" @@ -24,7 +24,7 @@ var ( FolderResource = folders.FolderResourceInfo.GroupVersionResource() DashboardResource = dashboardV1.DashboardResourceInfo.GroupVersionResource() DashboardResourceV2alpha1 = dashboardV2alpha1.DashboardResourceInfo.GroupVersionResource() - DashboardResourceV2alpha2 = dashboardV2alpha2.DashboardResourceInfo.GroupVersionResource() + DashboardResourceV2beta1 = dashboardV2beta1.DashboardResourceInfo.GroupVersionResource() // SupportedProvisioningResources is the list of resources that can fully managed from the UI SupportedProvisioningResources = []schema.GroupVersionResource{FolderResource, DashboardResource} diff --git a/pkg/services/authz/zanzana/server/server.go b/pkg/services/authz/zanzana/server/server.go index 1a1d538cc11..f599fa6ae57 100644 --- a/pkg/services/authz/zanzana/server/server.go +++ b/pkg/services/authz/zanzana/server/server.go @@ -12,7 +12,7 @@ import ( "google.golang.org/protobuf/types/known/wrapperspb" dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashboardV2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -101,8 +101,8 @@ func (s *Server) getContextuals(subject string) (*openfgav1.ContextualTupleKeys, User: subject, Relation: common.RelationSetView, Object: common.NewGroupResourceIdent( - dashboardV2alpha2.DashboardResourceInfo.GroupResource().Group, - dashboardV2alpha2.DashboardResourceInfo.GroupResource().Resource, + dashboardV2beta1.DashboardResourceInfo.GroupResource().Group, + dashboardV2beta1.DashboardResourceInfo.GroupResource().Resource, "", ), }, diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index 8d4bcb8a744..cd097a0503f 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -24,7 +24,7 @@ import ( dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashboardV2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" ) func TestMain(m *testing.M) { @@ -191,10 +191,10 @@ func TestIntegrationDashboardsAppV2alpha1(t *testing.T) { } } -func TestIntegrationDashboardsAppV2alpha2(t *testing.T) { +func TestIntegrationDashboardsAppV2beta1(t *testing.T) { gvr := schema.GroupVersionResource{ - Group: dashboardV2alpha2.GROUP, - Version: dashboardV2alpha2.VERSION, + Group: dashboardV2beta1.GROUP, + Version: dashboardV2beta1.VERSION, Resource: "dashboards", } if testing.Short() { diff --git a/pkg/tests/apis/dashboard/integration/api_validation_test.go b/pkg/tests/apis/dashboard/integration/api_validation_test.go index a3a1ae25829..70214db8cc2 100644 --- a/pkg/tests/apis/dashboard/integration/api_validation_test.go +++ b/pkg/tests/apis/dashboard/integration/api_validation_test.go @@ -17,7 +17,7 @@ import ( dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashboardV2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" foldersV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -440,12 +440,12 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { }, }, { - name: "v2alpha2 dashboard with correct spec should not throw on v2", - resourceInfo: dashboardV2alpha2.DashboardResourceInfo, + name: "v2beta1 dashboard with correct spec should not throw on v2", + resourceInfo: dashboardV2beta1.DashboardResourceInfo, expectSpecErr: false, testObject: &unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": dashboardV2alpha2.DashboardResourceInfo.TypeMeta().APIVersion, + "apiVersion": dashboardV2beta1.DashboardResourceInfo.TypeMeta().APIVersion, "kind": "Dashboard", "metadata": map[string]interface{}{ "generateName": "test-", diff --git a/pkg/tests/apis/provisioning/exportunifiedtorepository/dashboard-test-v2alpha2.yaml b/pkg/tests/apis/provisioning/exportunifiedtorepository/dashboard-test-v2alpha2.yaml deleted file mode 100644 index 7a4079cee41..00000000000 --- a/pkg/tests/apis/provisioning/exportunifiedtorepository/dashboard-test-v2alpha2.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: dashboard.grafana.app/v2alpha2 -kind: Dashboard -metadata: - name: test-v2alpha2 -spec: - title: Test dashboard. Created at v2alpha2 - layout: - kind: GridLayout - spec: - items: [] diff --git a/pkg/tests/apis/provisioning/exportunifiedtorepository/dashboard-test-v2beta1.yaml b/pkg/tests/apis/provisioning/exportunifiedtorepository/dashboard-test-v2beta1.yaml new file mode 100644 index 00000000000..88f9f3385ae --- /dev/null +++ b/pkg/tests/apis/provisioning/exportunifiedtorepository/dashboard-test-v2beta1.yaml @@ -0,0 +1,10 @@ +apiVersion: dashboard.grafana.app/v2beta1 +kind: Dashboard +metadata: + name: test-v2beta1 +spec: + title: Test dashboard. Created at v2beta1 + layout: + kind: GridLayout + spec: + items: [] diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go index d708b0e9cc3..2d5585b819f 100644 --- a/pkg/tests/apis/provisioning/helper_test.go +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -25,7 +25,7 @@ import ( dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashboardsV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - dashboardsV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + dashboardsV2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" folder "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" @@ -51,7 +51,7 @@ type provisioningTestHelper struct { DashboardsV0 *apis.K8sResourceClient DashboardsV1 *apis.K8sResourceClient DashboardsV2alpha1 *apis.K8sResourceClient - DashboardsV2alpha2 *apis.K8sResourceClient + DashboardsV2beta1 *apis.K8sResourceClient AdminREST *rest.RESTClient EditorREST *rest.RESTClient ViewerREST *rest.RESTClient @@ -270,10 +270,10 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper Namespace: "default", // actually org1 GVR: dashboardsV2alpha1.DashboardResourceInfo.GroupVersionResource(), }) - dashboardsV2alpha2 := helper.GetResourceClient(apis.ResourceClientArgs{ + dashboardsV2beta1 := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: "default", // actually org1 - GVR: dashboardsV2alpha2.DashboardResourceInfo.GroupVersionResource(), + GVR: dashboardsV2beta1.DashboardResourceInfo.GroupVersionResource(), }) // Repo client, but less guard rails. Useful for subresources. We'll need this later... @@ -313,7 +313,7 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper DashboardsV0: dashboardsV0, DashboardsV1: dashboardsV1, DashboardsV2alpha1: dashboardsV2alpha1, - DashboardsV2alpha2: dashboardsV2alpha2, + DashboardsV2beta1: dashboardsV2beta1, } } diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index a7462a2498c..cdf210e2133 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -658,9 +658,9 @@ func TestProvisioning_ExportUnifiedToRepository(t *testing.T) { _, err = helper.DashboardsV2alpha1.Resource.Create(ctx, dashboard, metav1.CreateOptions{}) require.NoError(t, err, "should be able to create v2alpha1 dashboard") - dashboard = helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v2alpha2.yaml") - _, err = helper.DashboardsV2alpha2.Resource.Create(ctx, dashboard, metav1.CreateOptions{}) - require.NoError(t, err, "should be able to create v2alpha2 dashboard") + dashboard = helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v2beta1.yaml") + _, err = helper.DashboardsV2beta1.Resource.Create(ctx, dashboard, metav1.CreateOptions{}) + require.NoError(t, err, "should be able to create v2beta1 dashboard") // Now for the repository. const repo = "local-repository" @@ -701,7 +701,7 @@ func TestProvisioning_ExportUnifiedToRepository(t *testing.T) { {title: "Test dashboard. Created at v0", apiVersion: "dashboard.grafana.app/v0alpha1", name: "test-v0", fileName: "test-dashboard-created-at-v0.json"}, {title: "Test dashboard. Created at v1", apiVersion: "dashboard.grafana.app/v1beta1", name: "test-v1", fileName: "test-dashboard-created-at-v1.json"}, {title: "Test dashboard. Created at v2alpha1", apiVersion: "dashboard.grafana.app/v2alpha1", name: "test-v2alpha1", fileName: "test-dashboard-created-at-v2alpha1.json"}, - {title: "Test dashboard. Created at v2alpha2", apiVersion: "dashboard.grafana.app/v2alpha2", name: "test-v2alpha2", fileName: "test-dashboard-created-at-v2alpha2.json"}, + {title: "Test dashboard. Created at v2beta1", apiVersion: "dashboard.grafana.app/v2beta1", name: "test-v2beta1", fileName: "test-dashboard-created-at-v2beta1.json"}, } { fpath := filepath.Join(helper.ProvisioningPath, test.fileName) //nolint:gosec // we are ok with reading files in testdata diff --git a/public/app/core/components/Select/DashboardPicker.test.tsx b/public/app/core/components/Select/DashboardPicker.test.tsx index b46c1f9d2df..8fbe7599830 100644 --- a/public/app/core/components/Select/DashboardPicker.test.tsx +++ b/public/app/core/components/Select/DashboardPicker.test.tsx @@ -6,7 +6,7 @@ import { defaultDashboard as defaultDashboardData } from '@grafana/schema'; import { Spec as DashboardV2Spec, defaultSpec as defaultDashboardV2Spec, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { DashboardSearchItemType } from 'app/features/search/types'; @@ -80,7 +80,7 @@ const mockDashboard: DashboardDTO = { }; const mockDashboardV2: DashboardWithAccessInfo = { - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', spec: { ...defaultDashboardV2Spec(), diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index d71d0c57be2..50334b939e5 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -4,7 +4,7 @@ import { AppEvents, isTruthy, locationUtil } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { folderAPIv1beta1 as folderAPI } from 'app/api/clients/folder/v1beta1'; import { createBaseQuery, handleRequestError } from 'app/api/createBaseQuery'; import appEvents from 'app/core/app_events'; diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx index ca0183e0e37..afbcbda926a 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx @@ -1,6 +1,6 @@ import { t } from '@grafana/i18n'; import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; -import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { ConditionalRenderingChangedEvent } from '../edit-pane/shared'; diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx index b39090bb1df..930fcd500f0 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react'; import { PanelData } from '@grafana/data'; import { t } from '@grafana/i18n'; import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; -import { ConditionalRenderingDataKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { ConditionalRenderingDataKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Combobox, ComboboxOption } from '@grafana/ui'; import { dashboardEditActions } from '../edit-pane/shared'; diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx index ee949871a8f..8ebba87cab3 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react'; import { t } from '@grafana/i18n'; import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; -import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Stack } from '@grafana/ui'; import { dashboardEditActions } from '../edit-pane/shared'; diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx index 28033cf18bb..82282dee713 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { rangeUtil, SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; -import { ConditionalRenderingTimeRangeSizeKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { ConditionalRenderingTimeRangeSizeKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Field, Select, useStyles2 } from '@grafana/ui'; import { dashboardEditActions } from '../edit-pane/shared'; diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx index 5045f76d885..92dee9f7c0f 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { t } from '@grafana/i18n'; import { SceneComponentProps, sceneGraph, VariableDependencyConfig } from '@grafana/scenes'; -import { ConditionalRenderingVariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { ConditionalRenderingVariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Box, Combobox, ComboboxOption, Input, Stack } from '@grafana/ui'; import { dashboardEditActions } from '../edit-pane/shared'; diff --git a/public/app/features/dashboard-scene/conditional-rendering/types.ts b/public/app/features/dashboard-scene/conditional-rendering/types.ts index 9330812b55b..6a1f94876fd 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/types.ts +++ b/public/app/features/dashboard-scene/conditional-rendering/types.ts @@ -4,7 +4,7 @@ import { ConditionalRenderingGroupKind, ConditionalRenderingTimeRangeSizeKind, ConditionalRenderingVariableKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { ConditionalRenderingData } from './ConditionalRenderingData'; import { ConditionalRenderingGroup } from './ConditionalRenderingGroup'; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index c6a484837c9..a6bab169cc6 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -4,7 +4,7 @@ import { BackendSrv, config, locationService, setBackendSrv } from '@grafana/run import { Spec as DashboardV2Spec, defaultSpec as defaultDashboardV2Spec, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import store from 'app/core/store'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardVersionError, DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; @@ -37,6 +37,39 @@ jest.mock('@grafana/runtime', () => { ...original.config.featureToggles, dashboardNewLayouts: false, // Default value }, + + bootData: { + ...original.config.bootData, + settings: { + ...original.config.bootData.settings, + datasources: { + 'gdev-testdata': { + id: 7, + uid: 'abc', + type: 'grafana-testdata-datasource', + name: 'gdev-testdata', + meta: { + id: 'grafana-testdata-datasource', + type: 'datasource', + name: 'TestData', + aliasIDs: ['testdata'], + }, + }, + '-- Grafana --': { + id: -1, + uid: 'grafana', + type: 'datasource', + name: '-- Grafana --', + meta: { + id: 'grafana', + type: 'datasource', + name: '-- Grafana --', + }, + }, + }, + defaultDatasource: 'gdev-testdata', + }, + }, }, }; }); @@ -64,7 +97,7 @@ const setupDashboardAPI = ( const setupV1FailureV2Success = ( v2Response: DashboardWithAccessInfo = { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -75,7 +108,7 @@ const setupV1FailureV2Success = ( } ) => { const getDashSpy = jest.fn(); - setupLoadDashboardMockReject(new DashboardVersionError('v2alpha1')); + setupLoadDashboardMockReject(new DashboardVersionError('v2beta1')); setupDashboardAPI(v2Response, getDashSpy); return getDashSpy; }; @@ -331,7 +364,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -378,7 +411,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -398,7 +431,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash2', @@ -421,7 +454,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -446,7 +479,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -543,7 +576,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -577,7 +610,7 @@ describe('DashboardScenePageStateManager v2', () => { loader.setDashboardCache('fake-dash', { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -600,7 +633,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -626,7 +659,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -661,7 +694,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -684,7 +717,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -709,7 +742,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -740,7 +773,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -761,7 +794,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -791,7 +824,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -825,7 +858,7 @@ describe('DashboardScenePageStateManager v2', () => { setupDashboardAPI( { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -842,7 +875,7 @@ describe('DashboardScenePageStateManager v2', () => { await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); const mockLoader = { - loadDashboard: jest.fn().mockRejectedValue(new DashboardVersionError('v2alpha1')), + loadDashboard: jest.fn().mockRejectedValue(new DashboardVersionError('v2beta1')), }; loader['dashboardLoader'] = mockLoader as unknown as DashboardLoaderSrvV2; @@ -915,7 +948,7 @@ describe('UnifiedDashboardScenePageStateManager', () => { const originalFetchDashboard = v2Manager.fetchDashboard; v2Manager.fetchDashboard = jest.fn().mockResolvedValue({ access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'fake-dash', @@ -951,7 +984,7 @@ describe('UnifiedDashboardScenePageStateManager', () => { // V2 dashboard response const v2Response: DashboardWithAccessInfo = { access: {}, - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { name: 'v2-dash', @@ -1215,7 +1248,7 @@ const customHomeDashboardV1Spec = { content: '# Welcome to the home dashboard!\n\n## Example of v2 schema home dashboard', mode: 'markdown', }, - pluginVersion: '', + pluginVersion: '1.0.0', targets: [{ refId: 'A' }], title: 'Welcome', transformations: [], @@ -1272,9 +1305,10 @@ const customHomeDashboardV2Spec = { }, }, vizConfig: { - kind: 'text', + kind: 'VizConfig', + version: '1.0.0', + group: 'text', spec: { - pluginVersion: '', options: { mode: 'markdown', content: '# Welcome to the home dashboard!\n\n## Example of v2 schema home dashboard', @@ -1557,7 +1591,7 @@ const v2ProvisionedDashboardResource = { resource: { type: { group: 'dashboard.grafana.app', - version: 'v2alpha1', + version: 'v2beta1', kind: 'Dashboard', resource: 'dashboards', }, @@ -1565,7 +1599,7 @@ const v2ProvisionedDashboardResource = { existing: {}, action: 'update', dryRun: { - apiVersion: 'dashboard.grafana.app/v2alpha1', + apiVersion: 'dashboard.grafana.app/v2beta1', kind: 'Dashboard', metadata: { annotations: { @@ -1588,9 +1622,11 @@ const v2ProvisionedDashboardResource = { kind: 'AnnotationQuery', spec: { builtIn: true, - datasource: { - type: 'grafana', - uid: '-- Grafana --', + query: { + kind: 'DataQuery', + group: 'grafana', + spec: {}, + version: 'v0', }, enable: true, hide: true, @@ -1613,13 +1649,14 @@ const v2ProvisionedDashboardResource = { { kind: 'PanelQuery', spec: { - datasource: { - type: 'grafana-testdata-datasource', - uid: 'PD8C576611E62080A', - }, hidden: false, query: { - kind: 'grafana-testdata-datasource', + kind: 'DataQuery', + group: 'grafana-testdata-datasource', + version: 'v0', + datasource: { + name: 'PD8C576611E62080A', + }, spec: { scenarioId: 'random_walk', seriesCount: 2, diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 1b2d7f80c2a..d91cf0d2230 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -2,7 +2,7 @@ import { locationUtil, UrlQueryMap } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; import { sceneGraph } from '@grafana/scenes'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { BASE_URL } from 'app/api/clients/provisioning/v0alpha1/baseAPI'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { getMessageFromError, getMessageIdFromError, getStatusFromError } from 'app/core/utils/errors'; @@ -15,7 +15,7 @@ import { } from 'app/features/apiserver/types'; import { transformDashboardV2SpecToV1 } from 'app/features/dashboard/api/ResponseTransformers'; import { DashboardVersionError, DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; -import { isDashboardV2Resource, isDashboardV2Spec } from 'app/features/dashboard/api/utils'; +import { isDashboardV2Resource, isDashboardV2Spec, isV2StoredVersion } 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'; @@ -204,7 +204,7 @@ abstract class DashboardScenePageStateManagerBase dryRun: any, provisioningPreview: ProvisioningPreview ) { - if (dryRun.apiVersion.split('/')[1] === 'v2alpha1') { + if (dryRun.apiVersion.split('/')[1] === 'v2beta1') { return { ...dryRun, kind: 'DashboardWithAccessInfo', @@ -751,7 +751,7 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta return await operation(this.activeManager); } catch (error) { if (error instanceof DashboardVersionError) { - const manager = error.data.storedVersion === 'v2alpha1' ? this.v2Manager : this.v1Manager; + const manager = isV2StoredVersion(error.data.storedVersion) ? this.v2Manager : this.v1Manager; this.activeManager = manager; return await operation(manager); } else { @@ -797,7 +797,7 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta try { return await this.v1Manager.loadSnapshotScene(slug); } catch (error) { - if (error instanceof DashboardVersionError && error.data.storedVersion === 'v2alpha1') { + if (error instanceof DashboardVersionError && isV2StoredVersion(error.data.storedVersion)) { return await this.v2Manager.loadSnapshotScene(slug); } throw new Error('Snapshot not found'); diff --git a/public/app/features/dashboard-scene/saving/DetectChangesWorker.ts b/public/app/features/dashboard-scene/saving/DetectChangesWorker.ts index 563fe0ef0f4..cb4f8d2f3ec 100644 --- a/public/app/features/dashboard-scene/saving/DetectChangesWorker.ts +++ b/public/app/features/dashboard-scene/saving/DetectChangesWorker.ts @@ -1,5 +1,5 @@ import { Dashboard } from '@grafana/schema'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { jsonDiff } from '../settings/version-history/utils'; diff --git a/public/app/features/dashboard-scene/saving/getDashboardChanges.ts b/public/app/features/dashboard-scene/saving/getDashboardChanges.ts index 17ef35baa60..33c046e84b9 100644 --- a/public/app/features/dashboard-scene/saving/getDashboardChanges.ts +++ b/public/app/features/dashboard-scene/saving/getDashboardChanges.ts @@ -7,7 +7,7 @@ import { AdhocVariableSpec, Spec as DashboardV2Spec, VariableKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { ResponseTransformers } from 'app/features/dashboard/api/ResponseTransformers'; import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; diff --git a/public/app/features/dashboard-scene/saving/shared.tsx b/public/app/features/dashboard-scene/saving/shared.tsx index 25b636144b3..fece190d860 100644 --- a/public/app/features/dashboard-scene/saving/shared.tsx +++ b/public/app/features/dashboard-scene/saving/shared.tsx @@ -4,7 +4,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { config, isFetchError } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Alert, Box, Button, Stack } from '@grafana/ui'; import { WorkflowOption } from 'app/features/provisioning/types'; diff --git a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts index 8f75ecf0751..c5e2be5151b 100644 --- a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts +++ b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts @@ -4,7 +4,7 @@ import { locationUtil } from '@grafana/data'; import { t } from '@grafana/i18n'; import { locationService, reportInteraction } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import appEvents from 'app/core/app_events'; import { useAppNotification } from 'app/core/copy/appNotification'; import { updateDashboardName } from 'app/core/reducers/navBarTree'; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 694c37469f7..662d92a32df 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -16,7 +16,7 @@ import { VizPanel, } from '@grafana/scenes'; import { Dashboard, DashboardLink, LibraryPanel } from '@grafana/schema'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import appEvents from 'app/core/app_events'; import { ScrollRefElement } from 'app/core/components/NativeScrollbar'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; @@ -752,7 +752,7 @@ export class DashboardScene extends SceneObjectBase impleme const { meta } = this.state; const spec = this.getSaveAsModel(options); - const apiVersion = this.serializer instanceof V2DashboardSerializer ? 'v2alpha1' : 'v1beta1'; // get from the dashboard? + const apiVersion = this.serializer instanceof V2DashboardSerializer ? 'v2beta1' : 'v1beta1'; // get from the dashboard? return { apiVersion: `dashboard.grafana.app/${apiVersion}`, kind: 'Dashboard', diff --git a/public/app/features/dashboard-scene/scene/export/exporters.test.ts b/public/app/features/dashboard-scene/scene/export/exporters.test.ts index 23f8ad327af..5194233f54a 100644 --- a/public/app/features/dashboard-scene/scene/export/exporters.test.ts +++ b/public/app/features/dashboard-scene/scene/export/exporters.test.ts @@ -2,13 +2,13 @@ import { find } from 'lodash'; import { DataSourceInstanceSettings, DataSourceRef, PanelPluginMeta, TypedVariableModel } from '@grafana/data'; import { Dashboard, DashboardCursorSync, ThresholdsMode } from '@grafana/schema'; -import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples'; import { DatasourceVariableKind, LibraryPanelKind, PanelKind, QueryVariableKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; +import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples'; import config from 'app/core/config'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { createAdHocVariableAdapter } from 'app/features/variables/adhoc/adapter'; @@ -600,13 +600,12 @@ describe('dashboard exporter v2', () => { { kind: 'PanelQuery', spec: { - datasource: { - type: 'prometheus', - uid: '${datasourceVar}', - }, hidden: false, query: { - kind: 'prometheus', + datasource: { + name: '${datasourceVar}', + }, + group: 'prometheus', spec: { editorMode: 'builder', expr: 'go_goroutines{job="prometheus"}', @@ -634,7 +633,7 @@ describe('dashboard exporter v2', () => { it('should replace datasource in a query variable', async () => { const { dashboard } = await setup(); const variable = dashboard.variables[0] as QueryVariableKind; - expect(variable.spec.datasource?.uid).toBeUndefined(); + expect(variable.spec.query.datasource?.name).toBeUndefined(); }); it('do not expose datasource name and id in datasource variable', async () => { @@ -648,7 +647,7 @@ describe('dashboard exporter v2', () => { const { dashboard } = await setup(); const annotationQuery = dashboard.annotations[0]; - expect(annotationQuery.spec.datasource?.uid).toBeUndefined(); + expect(annotationQuery.spec.query?.datasource?.name).toBeUndefined(); }); it('should not remove datasource ref from panel that uses a datasource variable', async () => { @@ -658,11 +657,8 @@ describe('dashboard exporter v2', () => { if (panel.kind !== 'Panel') { throw new Error('Panel should be a Panel'); } - - expect(panel.spec.data.spec.queries[0].spec.datasource).toEqual({ - type: 'prometheus', - uid: '${datasourceVar}', - }); + expect(panel.spec.data.spec.queries[0].spec.query.datasource?.name).toBe('${datasourceVar}'); + expect(panel.spec.data.spec.queries[0].spec.query.group).toBe('prometheus'); }); it('should convert library panels to inline panels when sharing externally', async () => { @@ -698,7 +694,7 @@ describe('dashboard exporter v2', () => { expect(convertedPanel.spec.id).toBe(123); // Check that the panel was properly converted - expect(convertedPanel.spec.data.spec.queries[0].spec.query.kind).toBe('testdb'); + expect(convertedPanel.spec.data.spec.queries[0].spec.query.kind).toBe('DataQuery'); expect(convertedPanel.spec.data.spec.queries[0].spec.refId).toBe('A'); }); @@ -771,7 +767,8 @@ describe('dashboard exporter v2', () => { expect(placeholderPanel.kind).toBe('Panel'); expect((placeholderPanel as PanelKind).spec.id).toBe(125); expect((placeholderPanel as PanelKind).spec.title).toBe('Invalid Library Panel'); - expect((placeholderPanel as PanelKind).spec.vizConfig.kind).toBe('text'); + expect((placeholderPanel as PanelKind).spec.vizConfig.kind).toBe('VizConfig'); + expect((placeholderPanel as PanelKind).spec.vizConfig.group).toBe('text'); // Verify console.error was called expect(consoleSpy).toHaveBeenCalledWith('Failed to load library panel invalid-uid:', expect.any(Error)); diff --git a/public/app/features/dashboard-scene/scene/export/exporters.ts b/public/app/features/dashboard-scene/scene/export/exporters.ts index 5778960dd8b..785cb8dd529 100644 --- a/public/app/features/dashboard-scene/scene/export/exporters.ts +++ b/public/app/features/dashboard-scene/scene/export/exporters.ts @@ -11,7 +11,7 @@ import { QueryVariableKind, LibraryPanelRef, LibraryPanelKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { notifyApp } from 'app/core/actions'; import config from 'app/core/config'; import { createErrorNotification } from 'app/core/copy/appNotification'; @@ -378,9 +378,10 @@ async function convertLibraryPanelToInlinePanel(libraryPanelElement: LibraryPane }, }, vizConfig: { - kind: 'text', + kind: 'VizConfig', + group: 'text', + version: '', spec: { - pluginVersion: '', options: { content: `**Library Panel Load Error**\n\nUnable to load library panel: ${libraryPanel.name} (${libraryPanel.uid})\n\nError: ${error instanceof Error ? error.message : 'Unknown error'}`, mode: 'markdown', @@ -406,7 +407,8 @@ export async function makeExportableV2(dashboard: DashboardV2Spec, isSharingExte const removeDataSourceRefs = ( obj: AnnotationQueryKind['spec'] | QueryVariableKind['spec'] | PanelQueryKind['spec'] ) => { - const datasourceUid = obj.datasource?.uid; + const datasourceUid = obj.query?.datasource?.name; + if (datasourceUid?.startsWith('${') && datasourceUid?.endsWith('}')) { const varName = datasourceUid.slice(2, -1); // if there's a match we don't want to remove the datasource ref @@ -416,7 +418,7 @@ export async function makeExportableV2(dashboard: DashboardV2Spec, isSharingExte } } - obj.datasource = undefined; + obj.query && (obj.query.datasource = undefined); }; const processPanel = (panel: PanelKind) => { diff --git a/public/app/features/dashboard-scene/scene/export/utils.ts b/public/app/features/dashboard-scene/scene/export/utils.ts index ef754171363..6f3a35003e9 100644 --- a/public/app/features/dashboard-scene/scene/export/utils.ts +++ b/public/app/features/dashboard-scene/scene/export/utils.ts @@ -1,4 +1,4 @@ -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; /** * Removes a panel reference from a layout. diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx index 8e665bc524a..69ee13494ac 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx @@ -1,7 +1,7 @@ import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObject, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { GRID_CELL_VMARGIN } from 'app/core/constants'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; 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 6fbeb6064ec..b85cb2bcf4b 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -17,7 +17,7 @@ import { SceneGridLayoutDragStartEvent, SceneObject, } from '@grafana/scenes'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { useStyles2 } from '@grafana/ui'; import { GRID_COLUMN_COUNT } from 'app/core/constants'; import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty'; 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 c1b81b2138e..7bbc2ffebe3 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx @@ -9,7 +9,7 @@ import { VariableDependencyConfig, VizPanel, } from '@grafana/scenes'; -import { RowsLayoutRowKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { RowsLayoutRowKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import appEvents from 'app/core/app_events'; import { LS_ROW_COPY_KEY } from 'app/core/constants'; import store from 'app/core/store'; 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 f72262af37b..34b2a9e3a1a 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -8,7 +8,7 @@ import { SceneObjectState, VizPanel, } from '@grafana/scenes'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { dashboardEditActions, ObjectsReorderedOnCanvasEvent } from '../../edit-pane/shared'; import { serializeRowsLayout } from '../../serialization/layoutSerializers/RowsLayoutSerializer'; diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx index 7335bb7563e..7ebc9e07b1c 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx @@ -9,7 +9,7 @@ import { SceneObject, VizPanel, } from '@grafana/scenes'; -import { TabsLayoutTabKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { TabsLayoutTabKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { LS_TAB_COPY_KEY } from 'app/core/constants'; import { appEvents } from 'app/core/core'; import store from 'app/core/store'; diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx index 3abfb5e937d..f83a91748ed 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -7,7 +7,7 @@ import { SceneObjectUrlValues, VizPanel, } from '@grafana/scenes'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { dashboardEditActions, ObjectsReorderedOnCanvasEvent } from '../../edit-pane/shared'; import { serializeTabsLayout } from '../../serialization/layoutSerializers/TabsLayoutSerializer'; diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/paste.ts b/public/app/features/dashboard-scene/scene/layouts-shared/paste.ts index 9b6300db217..9e8328f14c4 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/paste.ts +++ b/public/app/features/dashboard-scene/scene/layouts-shared/paste.ts @@ -4,7 +4,7 @@ import { GridLayoutItemKind, RowsLayoutRowKind, TabsLayoutTabKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { LS_PANEL_COPY_KEY, LS_ROW_COPY_KEY, LS_TAB_COPY_KEY } from 'app/core/constants'; import store from 'app/core/store'; diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts index 679404b6cec..73a9d7329e0 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts @@ -1,5 +1,5 @@ import { SceneObject, VizPanel } from '@grafana/scenes'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; import { LayoutRegistryItem } from './LayoutRegistryItem'; diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index 86dfce4fc4e..5987962f3ff 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -10,11 +10,14 @@ import { Dashboard, VariableModel } from '@grafana/schema'; import { Spec as DashboardV2Spec, defaultSpec as defaultDashboardV2Spec, + defaultDataQueryKind, defaultPanelSpec, defaultTimeSettingsSpec, GridLayoutKind, + PanelKind, PanelSpec, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; + QueryVariableKind, +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; import { AnnoKeyDashboardSnapshotOriginalUrl } from 'app/features/apiserver/types'; import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types'; @@ -50,11 +53,13 @@ jest.mock('@grafana/runtime', () => ({ name: 'Grafana', meta: { id: 'grafana' }, type: 'datasource', + uid: 'grafana', }, prometheus: { name: 'prometheus', meta: { id: 'prometheus' }, type: 'datasource', + uid: 'prometheus-uid', }, }, }, @@ -563,15 +568,15 @@ describe('DashboardSceneSerializer', () => { variables: [ { kind: 'GroupByVariable', + group: 'ds', + datasource: { + name: 'ds-uid', + }, spec: { current: { text: 'Host', value: 'host', }, - datasource: { - type: 'ds', - uid: 'ds-uid', - }, name: 'GroupBy', options: [ { @@ -605,14 +610,14 @@ describe('DashboardSceneSerializer', () => { variables: [ { kind: 'AdhocVariable', + group: 'prometheus', + datasource: { + name: 'gdev-prometheus', + }, spec: { name: 'adhoc', label: 'Adhoc Label', description: 'Adhoc Description', - datasource: { - uid: 'gdev-prometheus', - type: 'prometheus', - }, hide: 'dontHide', skipUrlSync: false, filters: [], @@ -726,9 +731,14 @@ describe('DashboardSceneSerializer', () => { spec: { builtIn: true, name: 'Annotations & Alerts', - datasource: { - uid: '-- Grafana --', - type: 'grafana', + query: { + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'grafana', + datasource: { + name: '-- Grafana --', + }, + spec: {}, }, enable: true, hide: true, @@ -857,14 +867,15 @@ describe('DashboardSceneSerializer', () => { id: 1, title: 'Panel 1', vizConfig: { - kind: 'graph', + kind: 'VizConfig', + group: 'graph', + version: '1.0.0', spec: { fieldConfig: { defaults: { custom: { lineWidth: 2 } }, overrides: [], }, options: { legend: { show: true } }, - pluginVersion: '1.0.0', }, }, }, @@ -876,17 +887,238 @@ describe('DashboardSceneSerializer', () => { const panelSpec = saveAsModel.elements['panel-1'].spec as PanelSpec; expect(panelSpec.vizConfig).toMatchObject({ - kind: 'graph', + kind: 'VizConfig', + group: 'graph', + version: '1.0.0', spec: { fieldConfig: { defaults: { custom: { lineWidth: 2 } }, overrides: [], }, options: { legend: { show: true } }, - pluginVersion: '1.0.0', }, }); }); + + describe('data source references persistence', () => { + it('should not fill data source references for annotations when input did not contain it', () => { + const dashboard = setupV2({ + annotations: [ + { + kind: 'AnnotationQuery', + spec: { + builtIn: false, + enable: true, + hide: false, + iconColor: 'blue', + name: 'prom-annotations', + query: { + group: 'prometheus', + kind: 'DataQuery', + spec: { + refId: 'Anno', + }, + version: 'v0', + }, + }, + }, + ], + }); + const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); + // referencing index 1 as transformation adds built in annotation query + expect(saveAsModel.annotations[1].spec.query.datasource).toBeUndefined(); + }); + + it('should fill data source references for annotations when input did contain it', () => { + const dashboard = setupV2({ + annotations: [ + { + kind: 'AnnotationQuery', + spec: { + builtIn: false, + enable: true, + hide: false, + iconColor: 'blue', + name: 'prom-annotations', + query: { + group: 'prometheus', + kind: 'DataQuery', + datasource: { + name: 'prometheus-uid', + }, + spec: { + refId: 'Anno', + }, + version: 'v0', + }, + }, + }, + ], + }); + const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); + // referencing index 1 as transformation adds built in annotation query + expect(saveAsModel.annotations[1].spec.query?.datasource).toEqual({ + name: 'prometheus-uid', + }); + }); + it('should not fill data source references for panel queries when input did not contain it', () => { + const dashboard = setupV2({ + elements: { + 'panel-1': { + kind: 'Panel', + spec: { + ...defaultPanelSpec(), + data: { + kind: 'QueryGroup', + spec: { + transformations: [], + queryOptions: {}, + queries: [ + { + kind: 'PanelQuery', + spec: { + refId: 'A', + hidden: false, + query: { + kind: 'DataQuery', + group: 'prometheus', + version: 'v0', + spec: {}, + }, + }, + }, + ], + }, + }, + }, + }, + }, + }); + const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); + expect( + (saveAsModel.elements['panel-1'] as PanelKind).spec.data.spec.queries[0].spec.query.datasource + ).toBeUndefined(); + }); + + it('should fill data source references for panel queries when input did contain it', () => { + const dashboard = setupV2({ + elements: { + 'panel-1': { + kind: 'Panel', + spec: { + ...defaultPanelSpec(), + data: { + kind: 'QueryGroup', + spec: { + transformations: [], + queryOptions: {}, + queries: [ + { + kind: 'PanelQuery', + spec: { + refId: 'A', + hidden: false, + query: { + kind: 'DataQuery', + group: 'prometheus', + version: 'v0', + datasource: { + name: 'prometheus-uid', + }, + spec: {}, + }, + }, + }, + ], + }, + }, + }, + }, + }, + }); + const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); + expect( + (saveAsModel.elements['panel-1'] as PanelKind).spec.data.spec.queries[0].spec.query.datasource + ).toEqual({ + name: 'prometheus-uid', + }); + }); + + it('should not fill data source references for query variables when input did contain it', () => { + const queryVariable: QueryVariableKind = { + kind: 'QueryVariable', + spec: { + name: 'app', + current: { + text: 'app1', + value: 'app1', + }, + hide: 'dontHide', + includeAll: false, + label: 'Query Variable', + skipUrlSync: false, + regex: '', + definition: '', + options: [], + refresh: 'never', + sort: 'alphabeticalAsc', + multi: false, + allowCustomValue: true, + query: { + kind: 'DataQuery', + group: 'prometheus', + version: 'v0', + spec: {}, + }, + }, + }; + const dashboard = setupV2({ + variables: [queryVariable], + }); + const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); + expect((saveAsModel.variables[0] as QueryVariableKind).spec.query.datasource).toBeUndefined(); + }); + + it('should fill data source references for query variables when input did contain it', () => { + const queryVariable: QueryVariableKind = { + kind: 'QueryVariable', + spec: { + name: 'app', + current: { + text: 'app1', + value: 'app1', + }, + hide: 'dontHide', + includeAll: false, + label: 'Query Variable', + skipUrlSync: false, + regex: '', + definition: '', + options: [], + refresh: 'never', + sort: 'alphabeticalAsc', + multi: false, + allowCustomValue: true, + query: { + kind: 'DataQuery', + group: 'prometheus', + version: 'v0', + datasource: { + name: 'prometheus-uid', + }, + spec: {}, + }, + }, + }; + const dashboard = setupV2({ + variables: [queryVariable], + }); + const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions); + expect((saveAsModel.variables[0] as QueryVariableKind).spec.query.datasource).toEqual({ + name: 'prometheus-uid', + }); + }); + }); }); describe('panel mapping methods', () => { @@ -1000,9 +1232,10 @@ describe('DashboardSceneSerializer', () => { description: '', links: [], vizConfig: { - kind: 'timeseries', + kind: 'VizConfig', + group: 'timeseries', + version: '1.0.0', spec: { - pluginVersion: '1.0.0', options: {}, fieldConfig: { defaults: {}, overrides: [] }, }, @@ -1017,7 +1250,7 @@ describe('DashboardSceneSerializer', () => { refId: 'A', hidden: false, // No datasource defined - query: { kind: 'sql', spec: {} }, + query: { kind: 'DataQuery', version: defaultDataQueryKind().version, group: 'sql', spec: {} }, }, }, { @@ -1025,8 +1258,15 @@ describe('DashboardSceneSerializer', () => { spec: { refId: 'B', hidden: false, - datasource: { uid: 'datasource-1', type: 'prometheus' }, - query: { kind: 'prometheus', spec: {} }, + query: { + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', + datasource: { + name: 'datasource-1', + }, + spec: {}, + }, }, }, ], @@ -1044,9 +1284,10 @@ describe('DashboardSceneSerializer', () => { description: '', links: [], vizConfig: { - kind: 'timeseries', + kind: 'VizConfig', + group: 'timeseries', + version: '1.0.0', spec: { - pluginVersion: '1.0.0', options: {}, fieldConfig: { defaults: {}, overrides: [] }, }, @@ -1061,7 +1302,7 @@ describe('DashboardSceneSerializer', () => { refId: 'C', hidden: false, // No datasource defined - query: { kind: 'sql', spec: {} }, + query: { kind: 'DataQuery', version: defaultDataQueryKind().version, group: 'sql', spec: {} }, }, }, ], @@ -1109,7 +1350,7 @@ describe('DashboardSceneSerializer', () => { kind: 'AnnotationQuery', spec: { name: 'Annotation 1', - query: { kind: 'prometheus', spec: {} }, + query: { kind: 'DataQuery', version: defaultDataQueryKind().version, group: 'prometheus', spec: {} }, enable: true, hide: false, iconColor: 'red', diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts index 66d73dd2ff9..20dafa839b0 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts @@ -1,5 +1,5 @@ import { Dashboard } from '@grafana/schema'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { AnnoKeyDashboardSnapshotOriginalUrl, ObjectMeta } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; @@ -288,7 +288,7 @@ export class V2DashboardSerializer const panelQueries = elementPanel.spec.data.spec.queries; for (const query of panelQueries) { - if (!query.spec.datasource) { + if (!query.spec.query.datasource?.name) { const elementId = this.getElementIdForPanel(elementPanel.spec.id); if (!this.defaultDsReferencesMap.panels.has(elementId)) { this.defaultDsReferencesMap.panels.set(elementId, new Set()); @@ -306,7 +306,7 @@ export class V2DashboardSerializer if (saveModel?.variables) { for (const variable of saveModel.variables) { // for query variables that dont have a ds defined add them to the list - if (variable.kind === 'QueryVariable' && !variable.spec.datasource) { + if (variable.kind === 'QueryVariable' && !variable.spec.query.datasource?.name) { this.defaultDsReferencesMap.variables.add(variable.spec.name); } } @@ -315,7 +315,7 @@ export class V2DashboardSerializer // initialize annotations ds references map if (saveModel?.annotations) { for (const annotation of saveModel.annotations) { - if (!annotation.spec.datasource) { + if (!annotation.spec.query?.datasource?.name) { this.defaultDsReferencesMap.annotations.add(annotation.spec.name); } } @@ -409,7 +409,7 @@ export class V2DashboardSerializer 'elements' in this.initialSaveModel ? Object.values(this.initialSaveModel.elements) .filter((e) => e.kind === 'Panel') - .map((p) => p.spec.vizConfig.kind) + .map((p) => p.spec.vizConfig.group) : []; const panels = getPanelPluginCounts(panelPluginIds); const variables = 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 cc3a40896c7..6bddc68fb09 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 @@ -7,38 +7,38 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model "kind": "AnnotationQuery", "spec": { "builtIn": false, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --", - }, "enable": true, "hide": false, "iconColor": "red", "name": "query1", + "query": { + "datasource": { + "name": "-- Grafana --", + }, + "group": "grafana", + "kind": "DataQuery", + "spec": {}, + "version": "v0", + }, }, }, { "kind": "AnnotationQuery", "spec": { "builtIn": false, - "datasource": { - "type": "prometheus", - "uid": "abcdef", - }, "enable": true, "hide": true, "iconColor": "blue", "name": "query2", - }, - }, - { - "kind": "AnnotationQuery", - "spec": { - "builtIn": false, - "enable": true, - "hide": true, - "iconColor": "green", - "name": "query3", + "query": { + "datasource": { + "name": "abcdef", + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": {}, + "version": "v0", + }, }, }, ], @@ -73,15 +73,16 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model "title": "Test Panel", "transparent": true, "vizConfig": { - "kind": "timeseries", + "group": "timeseries", + "kind": "VizConfig", "spec": { "fieldConfig": { "defaults": {}, "overrides": [], }, "options": {}, - "pluginVersion": "7.0.0", }, + "version": "7.0.0", }, }, }, @@ -152,10 +153,6 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model "text": "text1", "value": "value1", }, - "datasource": { - "type": "prometheus", - "uid": "datasource1", - }, "definition": "definition1", "description": "A query variable", "hide": "hideLabel", @@ -165,11 +162,16 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model "name": "queryVar", "options": [], "query": { - "kind": "prometheus", + "datasource": { + "name": "datasource1", + }, + "group": "prometheus", + "kind": "DataQuery", "spec": { "expr": "label_values(node_boot_time_seconds)", "refId": "A", }, + "version": "v0", }, "refresh": "onDashboardLoad", "regex": "regex1", @@ -296,16 +298,16 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model }, }, { + "datasource": { + "name": "datasource2", + }, + "group": "prometheus", "kind": "GroupByVariable", "spec": { "current": { "text": "text7", "value": "value7", }, - "datasource": { - "type": "prometheus", - "uid": "datasource2", - }, "description": "A group by variable", "hide": "dontHide", "label": "Group By Variable", @@ -325,6 +327,10 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model }, }, { + "datasource": { + "name": "datasource3", + }, + "group": "prometheus", "kind": "AdhocVariable", "spec": { "allowCustomValue": true, @@ -342,10 +348,6 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model "value": "value2", }, ], - "datasource": { - "type": "prometheus", - "uid": "datasource3", - }, "defaultKeys": [ { "expandable": true, diff --git a/public/app/features/dashboard-scene/serialization/annotations.test.ts b/public/app/features/dashboard-scene/serialization/annotations.test.ts new file mode 100644 index 00000000000..0c92314324e --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/annotations.test.ts @@ -0,0 +1,226 @@ +import { AnnotationQuery } from '@grafana/data'; +import { AnnotationQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; + +import { transformV1ToV2AnnotationQuery, transformV2ToV1AnnotationQuery } from './annotations'; + +describe('V1<->V2 annotation convertions', () => { + test('given grafana-built in annotations', () => { + // test case + const annotationDefinition: AnnotationQuery = { + builtIn: 1, + datasource: { + type: 'grafana', + uid: 'grafana', + }, + enable: true, + hide: false, + iconColor: 'yellow', + name: 'Annotations \u0026 Alerts', + target: { + // @ts-expect-error + limit: 100, + matchAny: false, + tags: [], + type: 'dashboard', + }, + type: 'dashboard', + }; + + const expectedV2: AnnotationQueryKind = { + kind: 'AnnotationQuery', + spec: { + builtIn: true, + enable: true, + hide: false, + iconColor: 'yellow', + name: 'Annotations \u0026 Alerts', + query: { + kind: 'DataQuery', + group: 'grafana', + version: 'v0', + datasource: { + name: 'grafana', + }, + spec: { + limit: 100, + matchAny: false, + tags: [], + type: 'dashboard', + }, + }, + }, + }; + + const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'grafana', 'grafana'); + + expect(resultV2).toEqual(expectedV2); + + const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2); + expect(resultV1).toEqual(annotationDefinition); + }); + + test('given annotations with datasource', () => { + const annotationDefinition = { + datasource: { + type: 'grafana-testdata-datasource', + uid: 'uid', + }, + enable: true, + hide: false, + iconColor: 'blue', + name: 'testdata-annos', + target: { + lines: 10, + refId: 'Anno', + scenarioId: 'annotations', + }, + }; + + const expectedV2: AnnotationQueryKind = { + kind: 'AnnotationQuery', + spec: { + enable: true, + hide: false, + iconColor: 'blue', + name: 'testdata-annos', + builtIn: false, + query: { + kind: 'DataQuery', + group: 'grafana-testdata-datasource', + version: 'v0', + datasource: { + name: 'uid', + }, + spec: { + lines: 10, + refId: 'Anno', + scenarioId: 'annotations', + }, + }, + }, + }; + + const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery( + annotationDefinition, + 'grafana-testdata-datasource', + 'uid' + ); + + expect(resultV2).toEqual(expectedV2); + + const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2); + expect(resultV1).toEqual(annotationDefinition); + }); + + test('given annotations with target', () => { + const annotationDefinition = { + datasource: { + type: 'prometheus', + uid: 'uid', + }, + enable: true, + hide: false, + iconColor: 'yellow', + name: 'prom-annos', + target: { + expr: '{action="add_client"}', + interval: '', + lines: 10, + refId: 'Anno', + scenarioId: 'annotations', + }, + }; + + const expectedV2: AnnotationQueryKind = { + kind: 'AnnotationQuery', + spec: { + enable: true, + hide: false, + iconColor: 'yellow', + name: 'prom-annos', + builtIn: false, + query: { + kind: 'DataQuery', + group: 'prometheus', + version: 'v0', + datasource: { + name: 'uid', + }, + spec: { + expr: '{action="add_client"}', + interval: '', + lines: 10, + refId: 'Anno', + scenarioId: 'annotations', + }, + }, + }, + }; + + const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'prometheus', 'uid'); + expect(resultV2).toEqual(expectedV2); + + const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2); + expect(resultV1).toEqual(annotationDefinition); + }); + + test('given annotations with non-schematised options / legacyOptions', () => { + const annotationDefinition = { + datasource: { + type: 'elasticsearch', + uid: 'uid', + }, + enable: true, + hide: false, + iconColor: 'red', + name: 'elastic - annos', + tagsField: 'asd', + target: { + lines: 10, + query: 'test query', + refId: 'Anno', + scenarioId: 'annotations', + }, + textField: 'asd', + timeEndField: 'asdas', + timeField: 'asd', + }; + + const expectedV2: AnnotationQueryKind = { + kind: 'AnnotationQuery', + spec: { + enable: true, + hide: false, + iconColor: 'red', + name: 'elastic - annos', + builtIn: false, + query: { + kind: 'DataQuery', + group: 'elasticsearch', + version: 'v0', + datasource: { + name: 'uid', + }, + spec: { + lines: 10, + query: 'test query', + refId: 'Anno', + scenarioId: 'annotations', + }, + }, + legacyOptions: { + tagsField: 'asd', + textField: 'asd', + timeEndField: 'asdas', + timeField: 'asd', + }, + }, + }; + + const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'elasticsearch', 'uid'); + expect(resultV2).toEqual(expectedV2); + + const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2); + expect(resultV1).toEqual(annotationDefinition); + }); +}); diff --git a/public/app/features/dashboard-scene/serialization/annotations.ts b/public/app/features/dashboard-scene/serialization/annotations.ts new file mode 100644 index 00000000000..4e9d296f46c --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/annotations.ts @@ -0,0 +1,115 @@ +import { AnnotationQuery } from '@grafana/data'; +import { AnnotationQueryKind, defaultDataQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; + +import { getRuntimePanelDataSource } from './layoutSerializers/utils'; + +export function transformV1ToV2AnnotationQuery( + annotation: AnnotationQuery, + + dsType: string, + dsUID?: string, + // Overrides are used to provide properties based on scene's annotations data layer object state + override?: Partial +): AnnotationQueryKind { + const group = annotation.builtIn ? 'grafana' : dsType; + + const { + // known properties documented in v1 schema + enable, + hide, + iconColor, + name, + builtIn, + filter, + mappings, + datasource, + target, + snapshotData, + type, + + // unknown properties that are still available for configuration through API + ...legacyOptions + } = annotation; + + const result: AnnotationQueryKind = { + kind: 'AnnotationQuery', + spec: { + builtIn: Boolean(annotation.builtIn), + name: annotation.name, + enable: Boolean(override?.enable) || Boolean(annotation.enable), + hide: Boolean(override?.hide) || Boolean(annotation.hide), + iconColor: annotation.iconColor, + + query: { + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group, // Annotation layer has a datasource type provided in runtime. + spec: target || {}, + }, + }, + }; + + if (dsUID) { + result.spec.query.datasource = { + name: dsUID, + }; + } + + // if legacy options is not an empty object, add it to the result + if (Object.keys(legacyOptions).length > 0) { + result.spec.legacyOptions = legacyOptions; + } + + if (annotation.filter?.ids?.length) { + result.spec.filter = annotation.filter; + } + + // TODO: add mappings + + return result; +} + +export function transformV2ToV1AnnotationQuery(annotation: AnnotationQueryKind): AnnotationQuery { + let { query: dataQuery, ...annotationQuery } = annotation.spec; + + // Mapping from AnnotationQueryKind to AnnotationQuery used by scenes. + let annoQuerySpec: AnnotationQuery = { + enable: annotation.spec.enable, + hide: annotation.spec.hide, + iconColor: annotation.spec.iconColor, + name: annotation.spec.name, + // TOOO: mappings + }; + + if (Object.keys(dataQuery.spec).length > 0) { + // @ts-expect-error DataQueryKind spec should be typed as DataQuery interface + annoQuerySpec.target = { + ...dataQuery?.spec, + }; + } + + if (annotation.spec.builtIn) { + annoQuerySpec.type = 'dashboard'; + annoQuerySpec.builtIn = 1; + } + + if (annotation.spec.filter) { + annoQuerySpec.filter = annotation.spec.filter; + } + + // some annotations will contain in the legacyOptions properties that need to be + // added to the root level AnnotationQuery + if (annotationQuery.legacyOptions) { + annoQuerySpec = { + ...annoQuerySpec, + ...annotationQuery.legacyOptions, + }; + } + + // get data source from annotation query + const datasource = getRuntimePanelDataSource(dataQuery); + + annoQuerySpec.datasource = datasource; + + return annoQuerySpec; +} diff --git a/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts b/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts index 436563c8379..f690d58b035 100644 --- a/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts @@ -9,7 +9,7 @@ import { defaultTimeSettingsSpec, GroupByVariableKind, Spec as DashboardV2Spec, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { AnnoKeyFolder } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -92,14 +92,21 @@ export async function buildNewDashboardSaveModelV2( const filterVariable: AdhocVariableKind = { kind: 'AdhocVariable', - spec: { ...defaultAdhocVariableSpec(), name: 'Filter', datasource: datasourceRef }, + group: datasourceRef.type, + datasource: { + name: datasourceRef.uid, + }, + spec: { ...defaultAdhocVariableSpec(), name: 'Filter' }, }; const groupByVariable: GroupByVariableKind = { kind: 'GroupByVariable', + group: datasourceRef.type, + datasource: { + name: datasourceRef.uid, + }, spec: { ...defaultGroupByVariableSpec(), - datasource: datasourceRef, name: 'Group by', }, }; @@ -109,7 +116,7 @@ export async function buildNewDashboardSaveModelV2( } const data: DashboardWithAccessInfo = { - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', spec: { ...defaultDashboardV2Spec(), diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/AutoGridLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/AutoGridLayoutSerializer.ts index 543cff7dfad..f095e54f962 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/AutoGridLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/AutoGridLayoutSerializer.ts @@ -2,7 +2,7 @@ import { Spec as DashboardV2Spec, defaultAutoGridLayoutSpec, AutoGridLayoutItemKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { AutoGridItem } from '../../scene/layout-auto-grid/AutoGridItem'; import { AutoGridLayout } from '../../scene/layout-auto-grid/AutoGridLayout'; diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts index a1eea785230..8688d1cbb08 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts @@ -8,7 +8,7 @@ import { GridLayoutItemSpec, PanelKind, LibraryPanelKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { contextSrv } from 'app/core/core'; import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem'; diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.test.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.test.ts index 9a41a35bb2b..e8506e21278 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.test.ts @@ -1,5 +1,5 @@ import { SceneGridLayout } from '@grafana/scenes'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { AutoGridLayout } from '../../scene/layout-auto-grid/AutoGridLayout'; import { AutoGridLayoutManager } from '../../scene/layout-auto-grid/AutoGridLayoutManager'; diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts index 0b039b09a63..284d0a4fec2 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts @@ -1,7 +1,4 @@ -import { - Spec as DashboardV2Spec, - RowsLayoutRowKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec, RowsLayoutRowKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { RowItem } from '../../scene/layout-rows/RowItem'; import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager'; diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.test.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.test.ts index 7132e4dc118..26fd92e1dce 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.test.ts @@ -1,4 +1,4 @@ -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { AutoGridLayoutManager } from '../../scene/layout-auto-grid/AutoGridLayoutManager'; import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts index 9a8cbe50bdb..bfcec4787b3 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts @@ -1,7 +1,4 @@ -import { - Spec as DashboardV2Spec, - TabsLayoutTabKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec, TabsLayoutTabKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { TabItem } from '../../scene/layout-tabs/TabItem'; import { TabItemRepeaterBehavior } from '../../scene/layout-tabs/TabItemRepeaterBehavior'; diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts index dfb8555e44f..d8e20de967d 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts @@ -1,5 +1,5 @@ import { Registry, RegistryItem } from '@grafana/data'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { DashboardLayoutManager } from '../../scene/types/DashboardLayoutManager'; diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts index fce584cf4f7..1c177f84996 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts @@ -1,4 +1,4 @@ -import { PanelQueryKind, PanelKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { defaultDataQueryKind, PanelQueryKind, PanelKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { CustomTimeRangeCompare } from '../../scene/CustomTimeRangeCompare'; @@ -31,6 +31,12 @@ jest.mock('@grafana/runtime', () => ({ meta: { id: 'loki' }, type: 'datasource', }, + '-- Grafana --': { + uid: 'grafana', + name: 'Grafana', + meta: { id: 'grafana' }, + type: 'datasource', + }, }, }, }, @@ -53,11 +59,12 @@ const createTestPanel = (): PanelKind => ({ title: 'Test Panel', description: '', vizConfig: { - kind: 'timeseries', + kind: 'VizConfig', + group: 'timeseries', + version: '1.0.0', spec: { options: {}, fieldConfig: { defaults: {}, overrides: [] }, - pluginVersion: '1.0.0', }, }, data: { @@ -116,22 +123,23 @@ describe('getRuntimePanelDataSource', () => { spec: { refId: 'A', hidden: false, - datasource: { - uid: 'test-ds-uid', - type: 'test-ds-type', - }, query: { - kind: 'prometheus', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', + datasource: { + name: 'prometheus-uid', + }, spec: {}, }, }, }; - const result = getRuntimePanelDataSource(query); + const result = getRuntimePanelDataSource(query.spec.query); expect(result).toEqual({ - uid: 'test-ds-uid', - type: 'test-ds-type', + uid: 'prometheus-uid', + type: 'prometheus', }); }); @@ -141,15 +149,16 @@ describe('getRuntimePanelDataSource', () => { spec: { refId: 'A', hidden: false, - datasource: undefined, query: { - kind: 'prometheus', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', spec: {}, }, }, }; - const result = getRuntimePanelDataSource(query); + const result = getRuntimePanelDataSource(query.spec.query); expect(result).toEqual({ uid: 'default-prometheus-uid', @@ -163,15 +172,16 @@ describe('getRuntimePanelDataSource', () => { spec: { refId: 'A', hidden: false, - datasource: undefined, query: { - kind: 'loki', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'loki', spec: {}, }, }, }; - const result = getRuntimePanelDataSource(query); + const result = getRuntimePanelDataSource(query.spec.query); expect(result).toEqual({ uid: 'loki-uid', @@ -186,15 +196,16 @@ describe('getRuntimePanelDataSource', () => { spec: { refId: 'A', hidden: false, - datasource: undefined, query: { - kind: 'unknown-type', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'unknown-type', spec: {}, }, }, }; - const result = getRuntimePanelDataSource(query); + const result = getRuntimePanelDataSource(query.spec.query); expect(result).toEqual({ uid: 'default-prometheus-uid', @@ -212,18 +223,19 @@ describe('getRuntimePanelDataSource', () => { spec: { refId: 'A', hidden: false, - datasource: { - uid: '', - type: 'test-ds-type', - }, query: { - kind: 'prometheus', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', + datasource: { + name: '', + }, spec: {}, }, }, }; - const result = getRuntimePanelDataSource(query); + const result = getRuntimePanelDataSource(query.spec.query); expect(result).toEqual({ uid: 'default-prometheus-uid', diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts index b3b3325ab21..2a3208bac14 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts @@ -19,7 +19,8 @@ import { PanelQueryKind, QueryVariableKind, TabsLayoutTabKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; + DataQueryKind, +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering'; @@ -61,10 +62,10 @@ export function buildVizPanel(panel: PanelKind, id?: number): VizPanel { key: getVizPanelKeyForPanelId(id ?? panel.spec.id), title: panel.spec.title?.substring(0, 5000), description: panel.spec.description, - pluginId: panel.spec.vizConfig.kind, + pluginId: panel.spec.vizConfig.group, options: panel.spec.vizConfig.spec.options, fieldConfig: transformMappingsToV1(panel.spec.vizConfig.spec.fieldConfig), - pluginVersion: panel.spec.vizConfig.spec.pluginVersion, + pluginVersion: panel.spec.vizConfig.version, displayMode: panel.spec.transparent ? 'transparent' : 'default', hoverHeader: !panel.spec.title && !timeOverrideShown, hoverHeaderOffset: 0, @@ -189,12 +190,15 @@ function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined { panel.spec.data.spec.queries.forEach((query) => { if (!datasource) { - if (!query.spec.datasource?.uid) { - datasource = getRuntimePanelDataSource(query); + if (!query.spec.query.datasource?.name) { + datasource = getRuntimePanelDataSource(query.spec.query); } else { - datasource = query.spec.datasource; + datasource = { + uid: query.spec.query.datasource?.name, + type: query.spec.query.group, + }; } - } else if (datasource.uid !== query.spec.datasource?.uid || datasource.type !== query.spec.datasource?.type) { + } else if (datasource.uid !== query.spec.query.datasource?.name || datasource.type !== query.spec.query.group) { isMixedDatasource = true; } }); @@ -203,11 +207,19 @@ function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined { } export function getRuntimeVariableDataSource(variable: QueryVariableKind): DataSourceRef | undefined { - return getDataSourceForQuery(variable.spec.datasource, variable.spec.query.kind); + const ds: DataSourceRef = { + uid: variable.spec.query.datasource?.name, + type: variable.spec.query.group, + }; + return getDataSourceForQuery(ds, variable.spec.query.group); } -export function getRuntimePanelDataSource(query: PanelQueryKind): DataSourceRef | undefined { - return getDataSourceForQuery(query.spec.datasource, query.spec.query.kind); +export function getRuntimePanelDataSource(query: DataQueryKind): DataSourceRef { + const ds: DataSourceRef = { + uid: query.datasource?.name, + type: query.group, + }; + return getDataSourceForQuery(ds, query.group); } /** @@ -215,10 +227,7 @@ export function getRuntimePanelDataSource(query: PanelQueryKind): DataSourceRef * @param queryKind - The kind of query being performed * @returns The resolved DataSourceRef */ -function getDataSourceForQuery( - querySpecDS: DataSourceRef | undefined | null, - queryKind: string -): DataSourceRef | undefined { +export function getDataSourceForQuery(querySpecDS: DataSourceRef | undefined | null, queryKind: string): DataSourceRef { // If datasource is specified and has a uid, use it if (querySpecDS?.uid) { return querySpecDS; @@ -257,14 +266,22 @@ function getDataSourceForQuery( }; } - // If we don't find a default datasource, return undefined - return undefined; + if (dsList && !dsList[defaultDatasource]) { + throw new Error(`Default datasource ${defaultDatasource} not found in datasource list`); + } + + // In the datasource list from bootData "id" is the type and the uid could be uid or the name + // in cases like grafana, dashboard or mixed datasource + return { + uid: dsList[defaultDatasource].uid || dsList[defaultDatasource].name, + type: dsList[defaultDatasource].meta.id, + }; } function panelQueryKindToSceneQuery(query: PanelQueryKind): SceneDataQuery { return { refId: query.spec.refId, - datasource: getRuntimePanelDataSource(query), + datasource: getRuntimePanelDataSource(query.spec.query), hide: query.spec.hidden, ...query.spec.query.spec, }; diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts index e0a3cfe305d..931ee847770 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -45,8 +45,8 @@ const getDataSourceMock = jest.fn(); const fakeDsMock: DataSourceApi = { name: 'fake-std', - type: 'fake-std', - getRef: () => ({ type: 'fake-std', uid: 'fake-std' }), + type: 'fake-type', + getRef: () => ({ type: 'fake-type', uid: 'fake-uid' }), query: () => Promise.resolve({ data: [], @@ -74,7 +74,7 @@ const fakeDsMock: DataSourceApi = { toDataQuery: (q) => ({ ...q, refId: 'FakeDataSource-refId' }), }, id: 1, - uid: 'fake-std', + uid: 'fake-uid', }; jest.mock('@grafana/runtime', () => ({ @@ -95,7 +95,7 @@ describe('sceneVariablesSetToVariables', () => { description: 'test-desc', value: ['selected-value'], text: ['selected-value-text'], - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, query: 'query', includeAll: true, allowCustomValue: true, @@ -125,8 +125,8 @@ describe('sceneVariablesSetToVariables', () => { ], }, "datasource": { - "type": "fake-std", - "uid": "fake-std", + "type": "fake-type", + "uid": "fake-uid", }, "definition": undefined, "description": "test-desc", @@ -157,7 +157,7 @@ describe('sceneVariablesSetToVariables', () => { description: 'test-desc', value: ['selected-value'], text: ['selected-value-text'], - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, query: 'query', definition: 'query', includeAll: true, @@ -187,8 +187,8 @@ describe('sceneVariablesSetToVariables', () => { ], }, "datasource": { - "type": "fake-std", - "uid": "fake-std", + "type": "fake-type", + "uid": "fake-uid", }, "definition": "query", "description": "test-desc", @@ -219,7 +219,7 @@ describe('sceneVariablesSetToVariables', () => { description: 'test-desc', value: ['selected-value'], text: ['selected-value-text'], - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, query: 'query', options: [ { label: 'test', value: 'test' }, @@ -246,7 +246,7 @@ describe('sceneVariablesSetToVariables', () => { description: 'test-desc', value: ['test'], text: ['test'], - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, query: 'query', options: [ { label: 'test', value: 'test' }, @@ -517,7 +517,7 @@ describe('sceneVariablesSetToVariables', () => { allowCustomValue: true, label: 'test-label', description: 'test-desc', - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, filters: [ { key: 'filterTest', @@ -551,8 +551,8 @@ describe('sceneVariablesSetToVariables', () => { }, ], "datasource": { - "type": "fake-std", - "uid": "fake-std", + "type": "fake-type", + "uid": "fake-uid", }, "defaultKeys": undefined, "description": "test-desc", @@ -680,7 +680,7 @@ describe('sceneVariablesSetToVariables', () => { allowCustomValue: true, label: 'test-label', description: 'test-desc', - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultKeys: [ { text: 'some', @@ -728,8 +728,8 @@ describe('sceneVariablesSetToVariables', () => { }, ], "datasource": { - "type": "fake-std", - "uid": "fake-std", + "type": "fake-type", + "uid": "fake-uid", }, "defaultKeys": [ { @@ -775,7 +775,7 @@ describe('sceneVariablesSetToVariables', () => { label: 'test-label', description: 'test-desc', allowCustomValue: true, - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultOptions: [ { text: 'Foo', @@ -802,8 +802,8 @@ describe('sceneVariablesSetToVariables', () => { "value": [], }, "datasource": { - "type": "fake-std", - "uid": "fake-std", + "type": "fake-type", + "uid": "fake-uid", }, "defaultValue": undefined, "description": "test-desc", @@ -831,7 +831,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultOptions: [ { text: 'Foo', @@ -859,7 +859,7 @@ describe('sceneVariablesSetToVariables', () => { description: 'test-desc', value: ['selected-value'], text: ['selected-value-text'], - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, query: 'query', includeAll: true, allValue: 'test-all', @@ -876,51 +876,52 @@ describe('sceneVariablesSetToVariables', () => { expect(result).toHaveLength(1); expect(result[0]).toMatchInlineSnapshot(` - { - "kind": "QueryVariable", - "spec": { - "allValue": "test-all", - "allowCustomValue": true, - "current": { - "text": [ - "selected-value-text", - ], - "value": [ - "selected-value", - ], - }, - "datasource": { - "type": "fake-std", - "uid": "fake-std", - }, - "definition": undefined, - "description": "test-desc", - "hide": "dontHide", - "includeAll": true, - "label": "test-label", - "multi": true, - "name": "test", - "options": [], - "query": { - "kind": "fake-std", + { + "kind": "QueryVariable", "spec": { - "__legacyStringValue": "query", + "allValue": "test-all", + "allowCustomValue": true, + "current": { + "text": [ + "selected-value-text", + ], + "value": [ + "selected-value", + ], + }, + "definition": undefined, + "description": "test-desc", + "hide": "dontHide", + "includeAll": true, + "label": "test-label", + "multi": true, + "name": "test", + "options": [], + "query": { + "datasource": { + "name": "fake-uid", + }, + "group": "fake-type", + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "query", + }, + "version": "v0", + }, + "refresh": "onDashboardLoad", + "regex": "", + "skipUrlSync": false, + "sort": "disabled", + "staticOptions": [ + { + "text": "test", + "value": "test", + }, + ], + "staticOptionsOrder": "after", }, - }, - "refresh": "onDashboardLoad", - "regex": "", - "skipUrlSync": false, - "sort": "disabled", - "staticOptions": [ - { - "text": "test", - "value": "test", - }, - ], - "staticOptionsOrder": "after", - }, - } - `); + } + `); }); it('should handle CustomVariable', () => { @@ -1179,7 +1180,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, filters: [ { key: 'filterTest', @@ -1204,6 +1205,10 @@ describe('sceneVariablesSetToVariables', () => { expect(result).toHaveLength(1); expect(result[0]).toMatchInlineSnapshot(` { + "datasource": { + "name": "fake-uid", + }, + "group": "fake-type", "kind": "AdhocVariable", "spec": { "allowCustomValue": true, @@ -1214,10 +1219,6 @@ describe('sceneVariablesSetToVariables', () => { "value": "test", }, ], - "datasource": { - "type": "fake-std", - "uid": "fake-std", - }, "defaultKeys": [], "description": "test-desc", "filters": [ @@ -1241,7 +1242,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultKeys: [ { text: 'some', @@ -1280,6 +1281,10 @@ describe('sceneVariablesSetToVariables', () => { expect(result).toHaveLength(1); expect(result[0]).toMatchInlineSnapshot(` { + "datasource": { + "name": "fake-uid", + }, + "group": "fake-type", "kind": "AdhocVariable", "spec": { "allowCustomValue": true, @@ -1290,10 +1295,6 @@ describe('sceneVariablesSetToVariables', () => { "value": "test", }, ], - "datasource": { - "type": "fake-std", - "uid": "fake-std", - }, "defaultKeys": [ { "text": "some", @@ -1339,7 +1340,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultOptions: [ { text: 'Foo', @@ -1360,16 +1361,16 @@ describe('sceneVariablesSetToVariables', () => { expect(result).toHaveLength(1); expect(result[0]).toMatchInlineSnapshot(` { + "datasource": { + "name": "fake-uid", + }, + "group": "fake-type", "kind": "GroupByVariable", "spec": { "current": { "text": [], "value": [], }, - "datasource": { - "type": "fake-std", - "uid": "fake-std", - }, "defaultValue": undefined, "description": "test-desc", "hide": "dontHide", @@ -1399,7 +1400,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', - datasource: { uid: 'fake-std', type: 'fake-std' }, + datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultOptions: [ { text: 'Foo', diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index d90254a5de7..87cde15a7ea 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -24,12 +24,15 @@ import { GroupByVariableKind, defaultVariableHide, VariableOption, + defaultDataQueryKind, AdHocFilterWithLabels, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; +import { getDefaultDatasource } from 'app/features/dashboard/api/ResponseTransformers'; import { getIntervalsQueryFromNewIntervalModel } from '../utils/utils'; import { DSReferencesMapping } from './DashboardSceneSerializer'; +import { getDataSourceForQuery } from './layoutSerializers/utils'; import { getDataQueryKind, getDataQuerySpec, getElementDatasource } from './transformSceneToSaveModelSchemaV2'; import { transformVariableRefreshToEnum, @@ -299,14 +302,30 @@ export function sceneVariablesSetToSchemaV2Variables( } const query = variable.state.query; let dataQuery: DataQueryKind | string; + const datasource = getElementDatasource(set, variable, 'variable', undefined, dsReferencesMapping); + if (typeof query !== 'string') { dataQuery = { - kind: variable.state.datasource?.type ?? getDataQueryKind(query), + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: datasource?.type ?? getDataQueryKind(query), + ...(datasource?.uid && { + datasource: { + name: datasource.uid, + }, + }), spec: getDataQuerySpec(query), }; } else { dataQuery = { - kind: variable.state.datasource?.type ?? getDataQueryKind(query), + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: datasource?.type ?? getDataQueryKind(query), + ...(datasource?.uid && { + datasource: { + name: datasource.uid, + }, + }), spec: { [LEGACY_STRING_VALUE_KEY]: query, }, @@ -320,7 +339,6 @@ export function sceneVariablesSetToSchemaV2Variables( options, query: dataQuery, definition: variable.state.definition, - datasource: getElementDatasource(set, variable, 'variable', undefined, dsReferencesMapping), sort: transformSortVariableToEnum(variable.state.sort), refresh: transformVariableRefreshToEnum(variable.state.refresh), regex: variable.state.regex, @@ -440,11 +458,19 @@ export function sceneVariablesSetToSchemaV2Variables( } : undefined; + const ds = getDataSourceForQuery( + variable.state.datasource, + variable.state.datasource?.type || getDefaultDatasource().type! + ); + const groupVariable: GroupByVariableKind = { kind: 'GroupByVariable', + group: ds.type!, + datasource: { + name: ds.uid, + }, spec: { ...commonProperties, - datasource: variable.state.datasource || {}, // FIXME what is the default value?, // Only persist the statically defined options options: variable.state.defaultOptions?.map((option) => ({ @@ -458,12 +484,20 @@ export function sceneVariablesSetToSchemaV2Variables( }; variables.push(groupVariable); } else if (sceneUtils.isAdHocVariable(variable)) { + const ds = getDataSourceForQuery( + variable.state.datasource, + variable.state.datasource?.type || getDefaultDatasource().type! + ); const adhocVariable: AdhocVariableKind = { kind: 'AdhocVariable', + group: ds.type!, + datasource: { + name: ds.uid, + }, spec: { ...commonProperties, name: variable.state.name, - datasource: variable.state.datasource || {}, //FIXME what is the default value? + baseFilters: validateFiltersOrigin(variable.state.baseFilters) || [], filters: [ ...validateFiltersOrigin(variable.state.originFilters), diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index ce31ae57d0e..87391044813 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -15,20 +15,21 @@ import { SceneDataTransformer, SceneGridItem, } from '@grafana/scenes'; -import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples'; import { AdhocVariableKind, ConstantVariableKind, CustomVariableKind, Spec as DashboardV2Spec, DatasourceVariableKind, + defaultDataQueryKind, GridLayoutItemSpec, GridLayoutSpec, GroupByVariableKind, IntervalVariableKind, QueryVariableKind, TextVariableKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; +import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples'; import { AnnoKeyDashboardIsSnapshot } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; @@ -138,6 +139,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { // Variables const variables = scene.state?.$variables; expect(variables?.state.variables).toHaveLength(dash.variables.length); + validateVariable({ sceneVariable: variables?.state.variables[0], variableKind: dash.variables[0] as QueryVariableKind, @@ -206,36 +208,26 @@ describe('transformSaveModelSchemaV2ToScene', () => { // Annotations expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet); const dataLayers = scene.state.$data as DashboardDataLayerSet; - // we should get two annotations, Grafana built-in and the custom ones expect(dataLayers.state.annotationLayers).toHaveLength(dash.annotations.length); - expect(dataLayers.state.annotationLayers).toHaveLength(5); - - // Built-in - const builtInAnnotation = dataLayers.state.annotationLayers[0] as unknown as DashboardAnnotationsDataLayer; - expect(builtInAnnotation.state.name).toBe('Annotations & Alerts'); - expect(builtInAnnotation.state.isEnabled).toBe(true); - expect(builtInAnnotation.state.isHidden).toBe(true); - expect(builtInAnnotation.state?.query.builtIn).toBe(1); + expect(dataLayers.state.annotationLayers[0].state.name).toBe(dash.annotations[0].spec.name); + expect(dataLayers.state.annotationLayers[0].state.isEnabled).toBe(dash.annotations[0].spec.enable); + expect(dataLayers.state.annotationLayers[0].state.isHidden).toBe(dash.annotations[0].spec.hide); // Enabled expect(dataLayers.state.annotationLayers[1].state.name).toBe(dash.annotations[1].spec.name); expect(dataLayers.state.annotationLayers[1].state.isEnabled).toBe(dash.annotations[1].spec.enable); expect(dataLayers.state.annotationLayers[1].state.isHidden).toBe(dash.annotations[1].spec.hide); + // Disabled expect(dataLayers.state.annotationLayers[2].state.name).toBe(dash.annotations[2].spec.name); expect(dataLayers.state.annotationLayers[2].state.isEnabled).toBe(dash.annotations[2].spec.enable); expect(dataLayers.state.annotationLayers[2].state.isHidden).toBe(dash.annotations[2].spec.hide); - // Disabled + // Hidden expect(dataLayers.state.annotationLayers[3].state.name).toBe(dash.annotations[3].spec.name); expect(dataLayers.state.annotationLayers[3].state.isEnabled).toBe(dash.annotations[3].spec.enable); expect(dataLayers.state.annotationLayers[3].state.isHidden).toBe(dash.annotations[3].spec.hide); - // Hidden - expect(dataLayers.state.annotationLayers[4].state.name).toBe(dash.annotations[4].spec.name); - expect(dataLayers.state.annotationLayers[4].state.isEnabled).toBe(dash.annotations[4].spec.enable); - expect(dataLayers.state.annotationLayers[4].state.isHidden).toBe(dash.annotations[4].spec.hide); - // VizPanel const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); expect(vizPanels).toHaveLength(2); @@ -279,13 +271,14 @@ describe('transformSaveModelSchemaV2ToScene', () => { kind: 'PanelQuery', spec: { refId: 'A', - datasource: { - type: 'graphite', - uid: 'datasource1', - }, hidden: false, query: { - kind: 'prometheus', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'graphite', + datasource: { + name: 'datasource1', + }, spec: { expr: 'test-query', }, @@ -307,13 +300,14 @@ describe('transformSaveModelSchemaV2ToScene', () => { kind: 'PanelQuery', spec: { refId: 'A', - datasource: { - type: 'prometheus', - uid: 'datasource1', - }, hidden: false, query: { - kind: 'prometheus', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', + datasource: { + name: 'datasource1', + }, spec: { expr: 'test-query', }, @@ -340,7 +334,12 @@ describe('transformSaveModelSchemaV2ToScene', () => { refId: 'A', hidden: false, query: { - kind: 'prometheus', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', + datasource: { + name: 'abc123', + }, spec: { expr: 'test-query', }, @@ -679,7 +678,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { // Create a dashboard with an annotation that has options const dashboardWithAnnotationOptions: DashboardWithAccessInfo = { kind: 'DashboardWithAccessInfo', - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', metadata: { name: 'test-dashboard', namespace: 'default', @@ -722,10 +721,6 @@ describe('transformSaveModelSchemaV2ToScene', () => { enable: true, hide: false, iconColor: 'purple', - datasource: { - type: 'prometheus', - uid: 'abc123', - }, legacyOptions: { expr: 'rate(http_requests_total[5m])', queryType: 'range', @@ -733,6 +728,15 @@ describe('transformSaveModelSchemaV2ToScene', () => { useValueAsTime: true, step: '1m', }, + query: { + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', + datasource: { + name: 'abc123', + }, + spec: {}, + }, }, }, ], @@ -764,23 +768,35 @@ describe('transformSaveModelSchemaV2ToScene', () => { // Get the annotation layers const dataLayerSet = scene.state.$data as DashboardDataLayerSet; expect(dataLayerSet).toBeDefined(); - // it should have two annotation layers, built-in and custom expect(dataLayerSet.state.annotationLayers.length).toBe(2); + const defaultAnnotationLayer = dataLayerSet.state.annotationLayers[0] as DashboardAnnotationsDataLayer; + + // Verify that the default annotation layer has been correctly initialized + expect(defaultAnnotationLayer.state.query).toEqual({ + datasource: { + uid: '-- Grafana --', + type: 'grafana', + }, + builtIn: 1, + enable: true, + iconColor: 'rgba(0, 211, 255, 1)', + name: 'Annotations & Alerts', + hide: true, + type: 'dashboard', + }); const annotationLayer = dataLayerSet.state.annotationLayers[1] as DashboardAnnotationsDataLayer; // Verify that the legacyOptions have been merged into the query object expect(annotationLayer.state.query).toMatchObject({ + datasource: { + uid: 'abc123', + type: 'prometheus', + }, name: 'Annotation with legacy options', - expr: 'rate(http_requests_total[5m])', - queryType: 'range', - legendFormat: '{{method}} {{endpoint}}', - useValueAsTime: true, - step: '1m', - }); - - // Verify the original legacyOptions object is also preserved - expect(annotationLayer.state.query.legacyOptions).toMatchObject({ + enable: true, + hide: false, + iconColor: 'purple', expr: 'rate(http_requests_total[5m])', queryType: 'range', legendFormat: '{{method}} {{endpoint}}', diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index ae132430e0c..986e0018329 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -28,6 +28,7 @@ import { defaultAdhocVariableKind, defaultConstantVariableKind, defaultCustomVariableKind, + defaultDataQueryKind, defaultDatasourceVariableKind, defaultGroupByVariableKind, defaultIntervalVariableKind, @@ -39,7 +40,7 @@ import { PanelKind, QueryVariableKind, TextVariableKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; import { AnnoKeyCreatedBy, @@ -62,9 +63,10 @@ import { DashboardScene } from '../scene/DashboardScene'; import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; import { getIntervalsFromQueryString } from '../utils/utils'; +import { transformV2ToV1AnnotationQuery } from './annotations'; import { SnapshotVariable } from './custom-variables/SnapshotVariable'; import { layoutDeserializerRegistry } from './layoutSerializers/layoutSerializerRegistry'; -import { getRuntimeVariableDataSource } from './layoutSerializers/utils'; +import { getDataSourceForQuery, getRuntimeVariableDataSource } from './layoutSerializers/utils'; import { registerPanelInteractionsReporter } from './transformSaveModelToScene'; import { transformCursorSyncV2ToV1, @@ -90,32 +92,23 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo { - let annoQuerySpec = annotation.spec; - // some annotations will contain in the legacyOptions properties that need to be - // added to the root level annotation spec - if (annoQuerySpec?.legacyOptions) { - annoQuerySpec = { - ...annoQuerySpec, - ...annoQuerySpec.legacyOptions, - }; - } - return new DashboardAnnotationsDataLayer({ + const annotationQuerySpec = transformV2ToV1AnnotationQuery(annotation); + + const layerState = { key: uniqueId('annotations-'), - query: { - ...annoQuerySpec, - builtIn: annotation.spec.builtIn ? 1 : 0, - }, + query: annotationQuerySpec, name: annotation.spec.name, isEnabled: Boolean(annotation.spec.enable), isHidden: Boolean(annotation.spec.hide), - }); + }; + + return new DashboardAnnotationsDataLayer(layerState); }); const isDashboardEditable = Boolean(dashboard.editable); @@ -267,21 +260,26 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S description: variable.spec.description, }; if (variable.kind === defaultAdhocVariableKind().kind) { + const ds = getDataSourceForQuery( + { + type: variable.group, + uid: variable.datasource?.name, + }, + variable.group + ); return new AdHocFiltersVariable({ ...commonProperties, description: variable.spec.description, skipUrlSync: variable.spec.skipUrlSync, hide: transformVariableHideToEnumV1(variable.spec.hide), - datasource: variable.spec.datasource, + datasource: ds, applyMode: 'auto', filters: variable.spec.filters ?? [], baseFilters: variable.spec.baseFilters ?? [], defaultKeys: variable.spec.defaultKeys, useQueriesAsFilterForOptions: true, layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined, - supportsMultiValueOperators: Boolean( - getDataSourceSrv().getInstanceSettings(variable.spec.datasource)?.meta.multiValueFilterOperators - ), + supportsMultiValueOperators: Boolean(getDataSourceSrv().getInstanceSettings(ds)?.meta.multiValueFilterOperators), }); } if (variable.kind === defaultCustomVariableKind().kind) { @@ -372,9 +370,17 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S hide: transformVariableHideToEnumV1(variable.spec.hide), }); } else if (config.featureToggles.groupByVariable && variable.kind === defaultGroupByVariableKind().kind) { + const ds = getDataSourceForQuery( + { + type: variable.group, + uid: variable.datasource?.name, + }, + variable.group + ); + return new GroupByVariable({ ...commonProperties, - datasource: variable.spec.datasource, + datasource: ds, value: variable.spec.current?.value || [], text: variable.spec.current?.text || [], skipUrlSync: variable.spec.skipUrlSync, @@ -422,6 +428,14 @@ export function createVariablesForSnapshot(dashboard: DashboardV2Spec): SceneVar try { // for adhoc we are using the AdHocFiltersVariable from scenes becuase of its complexity if (v.kind === 'AdhocVariable') { + const ds = getDataSourceForQuery( + { + type: v.group, + uid: v.datasource?.name, + }, + v.group + ); + return new AdHocFiltersVariable({ name: v.spec.name, label: v.spec.label, @@ -429,7 +443,7 @@ export function createVariablesForSnapshot(dashboard: DashboardV2Spec): SceneVar description: v.spec.description, skipUrlSync: v.spec.skipUrlSync, hide: transformVariableHideToEnumV1(v.spec.hide), - datasource: v.spec.datasource, + datasource: ds, applyMode: 'auto', filters: v.spec.filters ?? [], baseFilters: v.spec.baseFilters ?? [], @@ -437,7 +451,7 @@ export function createVariablesForSnapshot(dashboard: DashboardV2Spec): SceneVar useQueriesAsFilterForOptions: true, layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined, supportsMultiValueOperators: Boolean( - getDataSourceSrv().getInstanceSettings(v.spec.datasource)?.meta.multiValueFilterOperators + getDataSourceSrv().getInstanceSettings(ds)?.meta.multiValueFilterOperators ), }); } @@ -514,7 +528,15 @@ function getGrafanaBuiltInAnnotationDataLayer(dashboard: DashboardV2Spec) { const grafanaBuiltAnnotation: AnnotationQueryKind = { kind: 'AnnotationQuery', spec: { - datasource: { uid: '-- Grafana --', type: 'grafana' }, + query: { + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'grafana', + datasource: { + name: '-- Grafana --', + }, + spec: {}, + }, name: 'Annotations & Alerts', iconColor: DEFAULT_ANNOTATION_COLOR, enable: true, diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index 0bc40f8d9d6..56fcdbaef42 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -31,7 +31,7 @@ import { AutoGridLayoutSpec, RowsLayoutSpec, TabsLayoutSpec, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { DashboardEditPane } from '../edit-pane/DashboardEditPane'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; @@ -57,6 +57,7 @@ import { validateDashboardSchemaV2, getDataQueryKind, getAutoAssignedDSRef, + getVizPanelQueries, } from './transformSceneToSaveModelSchemaV2'; // Mock dependencies @@ -410,9 +411,7 @@ describe('transformSceneToSaveModelSchemaV2', () => { expect(result).toMatchSnapshot(); // Check that the annotation layers are correctly transformed - expect(result.annotations).toHaveLength(3); - // Check annotation layer 3 without initial data source isn't updated with runtime default - expect(result.annotations?.[2].spec.datasource?.type).toBe(undefined); + expect(result.annotations).toHaveLength(2); }); it('should transform the minimum scene to save model schema v2', () => { @@ -562,16 +561,8 @@ describe('transformSceneToSaveModelSchemaV2', () => { name: 'annotation-with-options', enable: true, iconColor: 'red', - legacyOptions: { - expr: 'rate(http_requests_total[5m])', - queryType: 'range', - legendFormat: '{{method}} {{endpoint}}', - useValueAsTime: true, - }, - // Some other properties that aren't in the annotation spec - // and should be moved to options - customProp1: 'value1', - customProp2: 'value2', + customProp1: true, + customProp2: 'test', }, name: 'layerWithOptions', isEnabled: true, @@ -593,19 +584,11 @@ describe('transformSceneToSaveModelSchemaV2', () => { expect(result.annotations.length).toBe(1); expect(result.annotations[0].spec.legacyOptions).toBeDefined(); expect(result.annotations[0].spec.legacyOptions).toEqual({ - expr: 'rate(http_requests_total[5m])', - queryType: 'range', - legendFormat: '{{method}} {{endpoint}}', - useValueAsTime: true, - customProp1: 'value1', - customProp2: 'value2', + customProp1: true, + customProp2: 'test', }); // Ensure these properties are not at the root level - expect(result).not.toHaveProperty('annotations[0].spec.expr'); - expect(result).not.toHaveProperty('annotations[0].spec.queryType'); - expect(result).not.toHaveProperty('annotations[0].spec.legendFormat'); - expect(result).not.toHaveProperty('annotations[0].spec.useValueAsTime'); expect(result).not.toHaveProperty('annotations[0].spec.customProp1'); expect(result).not.toHaveProperty('annotations[0].spec.customProp2'); }); @@ -834,6 +817,50 @@ describe('getElementDatasource', () => { }); }); +describe('getVizPanelQueries', () => { + it('should handle panel query datasources correctly', () => { + const queryWithDS: SceneDataQuery = { + refId: 'B', + datasource: { uid: 'prometheus-uid', type: 'prometheus' }, + }; + + const queryWithoutDS: SceneDataQuery = { + refId: 'A', + }; + + // Mock query runner + const queryRunner = new SceneQueryRunner({ + queries: [queryWithoutDS, queryWithDS], + datasource: { uid: 'default-ds', type: 'default' }, + }); + // Create test elements + const vizPanel = new VizPanel({ + key: 'panel-1', + pluginId: 'timeseries', + $data: queryRunner, + }); + + // Mock dsReferencesMapping + const dsReferencesMapping = { + panels: new Map(new Set([['panel-1', new Set(['A'])]])), + variables: new Set(), + annotations: new Set(), + }; + + const result = getVizPanelQueries(vizPanel, dsReferencesMapping); + expect(result.length).toBe(2); + expect(result[0].spec.query.kind).toBe('DataQuery'); + expect(result[0].spec.query.datasource).toBeUndefined(); // ignore datasource if it wasn't provided + expect(result[0].spec.query.group).toBe('default'); + expect(result[0].spec.query.version).toBe('v0'); + + expect(result[1].spec.query.kind).toBe('DataQuery'); + expect(result[1].spec.query.datasource?.name).toBe('prometheus-uid'); + expect(result[1].spec.query.group).toBe('prometheus'); + expect(result[1].spec.query.version).toBe('v0'); + }); +}); + function getMinimalSceneState(body: DashboardLayoutManager): Partial { return { id: 1, @@ -1065,18 +1092,6 @@ function createAnnotationLayers() { isEnabled: true, isHidden: true, }), - // this could happen if a dahboard was created from code and the datasource was not defined - new DashboardAnnotationsDataLayer({ - key: 'layer3', - query: { - name: 'query3', - enable: true, - iconColor: 'green', - }, - name: 'layer3', - isEnabled: true, - isHidden: true, - }), ]; } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index b8a0bfd3b6e..d0fe90719f1 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -43,7 +43,8 @@ import { DashboardCursorSync, FieldConfig, FieldColor, -} from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen'; + defaultDataQueryKind, +} from '../../../../../packages/grafana-schema/src/schema/dashboard/v2'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { PanelTimeRange } from '../scene/PanelTimeRange'; @@ -51,6 +52,7 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils'; import { DSReferencesMapping } from './DashboardSceneSerializer'; +import { transformV1ToV2AnnotationQuery } from './annotations'; import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables'; import { colorIdEnumToColorIdV2, transformCursorSynctoEnum } from './transformToV2TypesUtils'; @@ -230,9 +232,10 @@ export function vizPanelToSchemaV2( }, }, vizConfig: { - kind: vizPanel.state.pluginId, + kind: 'VizConfig', + group: vizPanel.state.pluginId, + version: vizPanel.state.pluginVersion ?? '', spec: { - pluginVersion: vizPanel.state.pluginVersion ?? '', options: vizPanel.state.options, fieldConfig: vizFieldConfig ?? defaultFieldConfigSource(), }, @@ -250,7 +253,7 @@ function getPanelLinks(panel: VizPanel): DataLink[] { return []; } -function getVizPanelQueries(vizPanel: VizPanel, dsReferencesMapping?: DSReferencesMapping): PanelQueryKind[] { +export function getVizPanelQueries(vizPanel: VizPanel, dsReferencesMapping?: DSReferencesMapping): PanelQueryKind[] { const queries: PanelQueryKind[] = []; const queryRunner = getQueryRunnerFor(vizPanel); const vizPanelQueries = queryRunner?.state.queries; @@ -258,12 +261,22 @@ function getVizPanelQueries(vizPanel: VizPanel, dsReferencesMapping?: DSReferenc if (vizPanelQueries) { vizPanelQueries.forEach((query) => { const queryDatasource = getElementDatasource(vizPanel, query, 'panel', queryRunner, dsReferencesMapping); + const dataQuery: DataQueryKind = { - kind: getDataQueryKind(query, queryRunner), + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: getDataQueryKind(query, queryRunner), + datasource: { + name: queryDatasource?.uid, + }, spec: omit(query, 'datasource', 'refId', 'hide'), }; + + if (!dataQuery.datasource?.name) { + delete dataQuery.datasource; + } + const querySpec: PanelQuerySpec = { - datasource: queryDatasource, query: dataQuery, refId: query.refId, hidden: Boolean(query.hide), @@ -407,78 +420,46 @@ function getAnnotations(state: DashboardSceneState, dsReferencesMapping?: DSRefe if (!(layer instanceof dataLayers.AnnotationsDataLayer)) { continue; } - const result: AnnotationQueryKind = { - kind: 'AnnotationQuery', - spec: { - builtIn: Boolean(layer.state.query.builtIn), - name: layer.state.query.name, - datasource: getElementDatasource(layer, layer.state.query, 'annotation', undefined, dsReferencesMapping), - enable: Boolean(layer.state.isEnabled), - hide: Boolean(layer.state.isHidden), - iconColor: layer.state.query.iconColor, - }, - }; - // Transform v1 dashboard (using target) to v2 structure - // adds extra condition to prioritize query over target - // if query is defined, use it - if (layer.state.query.target && !layer.state.query.query) { - // Handle built-in annotations - if (layer.state.query.builtIn) { - result.spec.query = { - kind: 'grafana', // built-in annotations are always of type grafana - spec: { - ...layer.state.query.target, - }, - }; - } else { - result.spec.query = { - kind: getAnnotationQueryKind(layer.state.query), - spec: { - ...layer.state.query.target, - }, - }; - } - } - // For annotations without query.query defined (e.g., grafana annotations without tags) - else if (layer.state.query.query?.kind) { - result.spec.query = { - kind: layer.state.query.query.kind, - spec: { - ...layer.state.query.query.spec, - }, - }; - } - // Collect datasource-specific properties not in standard annotation spec - let otherProps = omit( - layer.state.query, - 'type', - 'target', - 'builtIn', - 'name', - 'datasource', - 'iconColor', - 'enable', - 'hide', - 'filter', - 'query' - ); + const datasource = getElementDatasource(layer, layer.state.query, 'annotation', undefined, dsReferencesMapping); - // Store extra properties in the legacyOptions field instead of directly in the spec - if (Object.keys(otherProps).length > 0) { - // Extract options property and get the rest of the properties - const { legacyOptions, ...restProps } = otherProps; - // Merge options with the rest of the properties - result.spec.legacyOptions = { ...legacyOptions, ...restProps }; + let layerDs = layer.state.query.datasource; + + if (!layerDs) { + // This can happen only if we are transforming a scene that was created + // from a v1 spec. In v1 annotation layer can contain no datasource ref, which is guaranteed + // for layers created for v2 schema. See transform transformSaveModelSchemaV2ToScene.ts. + // In this case we will resolve default data source + layerDs = getDefaultDataSourceRef(); + console.error( + 'Misconfigured AnnotationsDataLayer: Data source is required for annotations. Resolving default data source', + layer, + layerDs + ); } + const result = transformV1ToV2AnnotationQuery(layer.state.query, layerDs.type!, layerDs.uid!, { + enable: layer.state.isEnabled, + hide: layer.state.isHidden, + }); + + const annotationQuery = layer.state.query; + // If filter is an empty array, don't save it - if (layer.state.query.filter?.ids?.length) { - result.spec.filter = layer.state.query.filter; + if (annotationQuery.filter?.ids?.length) { + result.spec.filter = annotationQuery.filter; + } + + // Finally, if the datasource references mapping did not containt data source ref, + // this means that the original model that was fetched did not contain it. In such scenario we don't want to save + // the explicit data source reference, so lets remove it from the save model. + if (!datasource) { + delete result.spec.query.datasource; } annotations.push(result); } + return annotations; } diff --git a/public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts b/public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts index 86b049d309b..64fe31b8426 100644 --- a/public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts +++ b/public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts @@ -16,7 +16,7 @@ import { FieldConfigSource, SpecialValueMatch, ThresholdsMode, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; export function transformVariableRefreshToEnumV1(refresh?: VariableRefresh): VariableRefreshV1 { switch (refresh) { diff --git a/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.test.ts b/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.test.ts index d085f80da5f..b1633283c97 100644 --- a/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.test.ts @@ -3,7 +3,7 @@ import { defaultVariableSort, defaultVariableRefresh, defaultDashboardCursorSync, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { transformCursorSynctoEnum, diff --git a/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts b/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts index 6c9ea39546f..cbdf630619c 100644 --- a/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts +++ b/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts @@ -17,7 +17,7 @@ import { VariableRefresh, VariableSort, FieldColorModeId as FieldColorModeIdV2, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; // 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'; diff --git a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx index c6b40b81ff0..a109dd7f808 100644 --- a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx +++ b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { SceneComponentProps, SceneObjectBase, sceneUtils } from '@grafana/scenes'; import { Dashboard } from '@grafana/schema'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Alert, Box, Button, CodeEditor, Stack, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx index 3ec58d8aecb..6543b641927 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx @@ -3,7 +3,7 @@ import { AsyncState } from 'react-use/lib/useAsync'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Alert, Label, RadioButtonGroup, Stack, Switch } from '@grafana/ui'; import { DashboardJson } from 'app/features/manage-dashboards/types'; diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx index 19791d70670..8fa8c102ee6 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx @@ -5,7 +5,7 @@ import { Spec as DashboardV2Spec, defaultQueryGroupKind, defaultVizConfigSpec, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import * as ResponseTransformers from 'app/features/dashboard/api/ResponseTransformers'; import { DashboardJson } from 'app/features/manage-dashboards/types'; import { DashboardDataDTO } from 'app/types/dashboard'; @@ -180,7 +180,7 @@ describe('ShareExportTab', () => { // Should use V2 API version expect(result.json).toMatchObject({ - apiVersion: 'dashboard.grafana.app/v2alpha1', + apiVersion: 'dashboard.grafana.app/v2beta1', kind: 'Dashboard', status: {}, }); @@ -394,7 +394,9 @@ describe('ShareExportTab', () => { links: [], data: defaultQueryGroupKind(), vizConfig: { - kind: 'stat', + kind: 'VizConfig', + group: '', + version: '1.0.0', spec: defaultVizConfigSpec(), }, }, @@ -443,7 +445,7 @@ describe('ShareExportTab', () => { // Set up the scene based on current version const currentDashboard = version === 'v1' ? mockV1Dashboard : mockV2Dashboard; const initialSaveModel = initialSaveModelVersion === 'v1' ? mockV1Dashboard : mockV2Dashboard; - const apiVersion = version === 'v1' ? 'dashboard.grafana.app/v1beta1' : 'dashboard.grafana.app/v2alpha1'; + const apiVersion = version === 'v1' ? 'dashboard.grafana.app/v1beta1' : 'dashboard.grafana.app/v2beta1'; scene.serializer.getSaveModel = jest.fn(() => currentDashboard); scene.serializer.makeExportableExternally = jest.fn(() => diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx index 8a9087f118b..ea52c8b0da3 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx @@ -8,7 +8,7 @@ import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Button, ClipboardButton, CodeEditor, Field, Modal, Stack, Switch } from '@grafana/ui'; import { ObjectMeta } from 'app/features/apiserver/types'; import { transformDashboardV2SpecToV1 } from 'app/features/dashboard/api/ResponseTransformers'; diff --git a/public/app/features/dashboard-scene/v2schema/ImportDashboardFormV2.tsx b/public/app/features/dashboard-scene/v2schema/ImportDashboardFormV2.tsx index f9dd24a99b2..0882b80310f 100644 --- a/public/app/features/dashboard-scene/v2schema/ImportDashboardFormV2.tsx +++ b/public/app/features/dashboard-scene/v2schema/ImportDashboardFormV2.tsx @@ -4,7 +4,7 @@ import { Controller, FieldErrors, UseFormReturn } from 'react-hook-form'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { ExpressionDatasourceRef } from '@grafana/runtime/internal'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Button, Field, FormFieldErrors, FormsOnSubmit, Stack, Input } from '@grafana/ui'; import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; diff --git a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx index c5cd4e4d15f..6f193e243a1 100644 --- a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx +++ b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx @@ -5,7 +5,8 @@ import { locationService, reportInteraction } from '@grafana/runtime'; import { AnnotationQueryKind, Spec as DashboardV2Spec, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; + defaultDataQueryKind, +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Form } from 'app/core/components/Form/Form'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; @@ -16,6 +17,8 @@ import { ImportDashboardFormV2 } from './ImportDashboardFormV2'; const IMPORT_FINISHED_EVENT_NAME = 'dashboard_import_imported'; +type FormData = SaveDashboardCommand & { [key: `datasource-${string}`]: string }; + export function ImportDashboardOverviewV2() { const [uidReset, setUidReset] = useState(false); const dispatch = useDispatch(); @@ -34,24 +37,29 @@ export function ImportDashboardOverviewV2() { dispatch(clearLoadedDashboard()); } - async function onSubmit(form: SaveDashboardCommand) { + async function onSubmit(form: FormData) { reportInteraction(IMPORT_FINISHED_EVENT_NAME); const dashboardWithDataSources: DashboardV2Spec = { ...dashboard, title: form.dashboard.title, annotations: dashboard.annotations?.map((annotation: AnnotationQueryKind) => { - if (annotation.spec.query?.kind) { - const dsType = annotation.spec.query.kind; + const dsType = annotation.spec.query?.spec.group; + if (dsType) { if (form[`datasource-${dsType}` as keyof typeof form]) { const ds = form[`datasource-${dsType}` as keyof typeof form] as { uid: string; type: string }; return { ...annotation, spec: { ...annotation.spec, - datasource: { - uid: ds.uid, - type: ds.type, + query: { + kind: 'DataQuery', + group: dsType, + version: defaultDataQueryKind().version, + datasource: { name: ds.uid }, + spec: { + ...annotation.spec.query?.spec, + }, }, }, }; @@ -61,18 +69,23 @@ export function ImportDashboardOverviewV2() { }), variables: dashboard.variables?.map((variable) => { if (variable.kind === 'QueryVariable') { - if (variable.spec.query?.kind) { - const dsType = variable.spec.query.kind; + const dsType = variable.spec.query?.spec.group; + if (dsType) { if (form[`datasource-${dsType}` as keyof typeof form]) { const ds = form[`datasource-${dsType}` as keyof typeof form] as { uid: string; type: string }; return { ...variable, spec: { ...variable.spec, - datasource: { - ...variable.spec.datasource, - uid: ds.uid, - type: ds.type, + query: { + ...variable.spec.query, + spec: { + ...variable.spec.query.spec, + group: ds.type, + datasource: { + name: ds.uid, + }, + }, }, options: [], current: { @@ -157,7 +170,7 @@ export function ImportDashboardOverviewV2() { return ( <> - & { [key: `datasource-${string}`]: string }> + onSubmit={onSubmit} defaultValues={{ dashboard, k8s: { annotations: { 'grafana.app/folder': folder.uid } } }} validateOnMount diff --git a/public/app/features/dashboard-scene/v2schema/test-helpers.ts b/public/app/features/dashboard-scene/v2schema/test-helpers.ts index 815762fdd31..b26066d1ea3 100644 --- a/public/app/features/dashboard-scene/v2schema/test-helpers.ts +++ b/public/app/features/dashboard-scene/v2schema/test-helpers.ts @@ -10,7 +10,7 @@ import { SceneVariableState, VizPanel, } from '@grafana/scenes'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; @@ -41,7 +41,8 @@ export function validateVariable< expect(scene.state?.$variables?.getByName(dashSpec.variables[index].spec.name)?.getValue()).toBe( `${variableKind.spec.filters[0].key}="${variableKind.spec.filters[0].value}"` ); - expect(sceneVariable?.state.datasource).toEqual(variableKind.spec.datasource); + expect(sceneVariable?.state.datasource?.type).toEqual(variableKind.group); + expect(sceneVariable?.state.datasource?.uid).toEqual(variableKind.datasource?.name); } else if (variableKind.kind !== 'AdhocVariable') { expect(sceneVariable).toBeInstanceOf(sceneVariableClass); expect(scene.state?.$variables?.getByName(dashSpec.variables[index].spec.name)?.getValue()).toBe( @@ -52,7 +53,8 @@ export function validateVariable< expect(sceneVariable?.state.pluginId).toBe(variableKind.spec.pluginId); } if (sceneVariable instanceof QueryVariable && variableKind.kind === 'QueryVariable') { - expect(sceneVariable?.state.datasource).toBe(variableKind.spec.datasource); + expect(sceneVariable?.state.datasource?.type).toBe(variableKind.spec.query?.group); + expect(sceneVariable?.state.datasource?.uid).toBe(variableKind.spec.query?.datasource?.name); expect(sceneVariable?.state.query).toEqual(variableKind.spec.query.spec); } if (sceneVariable instanceof CustomVariable && variableKind.kind === 'CustomVariable') { @@ -66,8 +68,8 @@ export function validateVizPanel(vizPanel: VizPanel, dash: DashboardV2Spec) { if (panel.kind === 'Panel') { expect(vizPanel.state.title).toBe(panel.spec.title); expect(vizPanel.state.description).toBe(panel.spec.description); - expect(vizPanel.state.pluginId).toBe(panel.spec.vizConfig.kind); - expect(vizPanel.state.pluginVersion).toBe(panel.spec.vizConfig.spec.pluginVersion); + expect(vizPanel.state.pluginId).toBe(panel.spec.vizConfig.group); + expect(vizPanel.state.pluginVersion).toBe(panel.spec.vizConfig.version); expect(vizPanel.state.options).toEqual(panel.spec.vizConfig.spec.options); expect(vizPanel.state.fieldConfig).toEqual(panel.spec.vizConfig.spec.fieldConfig); expect(getPanelIdForVizPanel(vizPanel)).toBe(panel.spec.id); diff --git a/public/app/features/dashboard-scene/v2schema/transformers.test.ts b/public/app/features/dashboard-scene/v2schema/transformers.test.ts index cc6b4999746..65924b4cf85 100644 --- a/public/app/features/dashboard-scene/v2schema/transformers.test.ts +++ b/public/app/features/dashboard-scene/v2schema/transformers.test.ts @@ -1,8 +1,8 @@ import { config } from '@grafana/runtime'; import { CustomVariable, GroupByVariable } from '@grafana/scenes'; import { LibraryPanel } from '@grafana/schema'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import * as libpanels from 'app/features/library-panels/state/api'; diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index 9187b6fc9c8..89e2d70a5a9 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -1,14 +1,15 @@ import { AnnotationQuery, DataQuery, VariableModel, VariableRefresh, Panel } from '@grafana/schema'; -import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples'; import { Spec as DashboardV2Spec, + defaultDataQueryKind, GridLayoutItemKind, GridLayoutKind, PanelKind, RowsLayoutKind, RowsLayoutRowKind, VariableKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; +import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples'; import { AnnoKeyCreatedBy, AnnoKeyDashboardGnetId, @@ -393,7 +394,7 @@ describe('ResponseTransformers', () => { const transformed = ResponseTransformers.ensureV2Response(dto); // Metadata - expect(transformed.apiVersion).toBe('v2alpha1'); + expect(transformed.apiVersion).toBe('v2beta1'); expect(transformed.kind).toBe('DashboardWithAccessInfo'); expect(transformed.metadata.annotations?.[AnnoKeyCreatedBy]).toEqual('user1'); expect(transformed.metadata.annotations?.[AnnoKeyUpdatedBy]).toEqual('user2'); @@ -449,14 +450,15 @@ describe('ResponseTransformers', () => { id: 1, links: [], vizConfig: { - kind: 'timeseries', + kind: 'VizConfig', + group: 'timeseries', + version: undefined, spec: { fieldConfig: { defaults: {}, overrides: [], }, options: {}, - pluginVersion: undefined, }, }, data: { @@ -466,10 +468,14 @@ describe('ResponseTransformers', () => { { kind: 'PanelQuery', spec: { - datasource: 'datasource1', hidden: false, query: { - kind: 'prometheus', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', + datasource: { + name: 'datasource1', + }, spec: { expr: 'test-query', }, @@ -793,7 +799,7 @@ describe('ResponseTransformers', () => { it('should transform DashboardWithAccessInfo to DashboardDTO', () => { const dashboardV2: DashboardWithAccessInfo = { - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { creationTimestamp: '2023-01-01T00:00:00Z', @@ -973,11 +979,9 @@ describe('ResponseTransformers', () => { result.forEach((query) => { expect(query.kind).toBe('PanelQuery'); - expect(query.spec.datasource).toEqual({ - type: 'theoretical-ds', - uid: 'theoretical-uid', - }); - expect(query.spec.query.kind).toBe('theoretical-ds'); + expect(query.spec.query.group).toEqual('theoretical-ds'); + expect(query.spec.query.datasource?.name).toEqual('theoretical-uid'); + expect(query.spec.query.kind).toBe('DataQuery'); }); }); @@ -1003,11 +1007,9 @@ describe('ResponseTransformers', () => { result.forEach((query) => { expect(query.kind).toBe('PanelQuery'); - expect(query.spec.datasource).toEqual({ - type: 'theoretical-ds', - uid: 'theoretical-uid', - }); - expect(query.spec.query.kind).toBe('theoretical-ds'); + expect(query.spec.query.group).toEqual('theoretical-ds'); + expect(query.spec.query.datasource?.name).toEqual('theoretical-uid'); + expect(query.spec.query.kind).toBe('DataQuery'); }); }); }); @@ -1015,14 +1017,14 @@ describe('ResponseTransformers', () => { function validateAnnotation(v1: AnnotationQuery, v2: DashboardV2Spec['annotations'][0]) { const { spec: v2Spec } = v2; - expect(v1.name).toBe(v2Spec.name); - expect(v1.datasource).toBe(v2Spec.datasource); + expect(v1.datasource?.type).toBe(v2Spec.query.group); + expect(v1.datasource?.uid).toBe(v2Spec.query.datasource?.name); expect(v1.enable).toBe(v2Spec.enable); expect(v1.hide).toBe(v2Spec.hide); expect(v1.iconColor).toBe(v2Spec.iconColor); - expect(v1.builtIn).toBe(v2Spec.builtIn ? 1 : 0); - expect(v1.target).toBe(v2Spec.query?.spec); + expect(v1.builtIn).toBe(v2Spec.builtIn ? 1 : undefined); + expect(v1.target).toEqual(v2Spec.query.spec); expect(v1.filter).toEqual(v2Spec.filter); } @@ -1031,19 +1033,22 @@ describe('ResponseTransformers', () => { expect(v1.id).toBe(v2Spec.id); expect(v1.id).toBe(v2Spec.id); - expect(v1.type).toBe(v2Spec.vizConfig.kind); + expect(v1.type).toBe(v2Spec.vizConfig.group); expect(v1.title).toBe(v2Spec.title); expect(v1.description).toBe(v2Spec.description); expect(v1.fieldConfig).toEqual(transformMappingsToV1(v2Spec.vizConfig.spec.fieldConfig)); expect(v1.options).toBe(v2Spec.vizConfig.spec.options); - expect(v1.pluginVersion).toBe(v2Spec.vizConfig.spec.pluginVersion); + expect(v1.pluginVersion).toBe(v2Spec.vizConfig.version); expect(v1.links).toEqual(v2Spec.links); expect(v1.targets).toEqual( v2Spec.data.spec.queries.map((q) => { return { refId: q.spec.refId, hide: q.spec.hidden, - datasource: q.spec.datasource, + datasource: { + type: q.spec.query.spec.group, + uid: q.spec.query.spec.datasource?.uid, + }, ...q.spec.query.spec, }; }) @@ -1093,18 +1098,24 @@ describe('ResponseTransformers', () => { }; expect(v2Common).toEqual(v1Common); - if (v2.kind === 'QueryVariable') { - expect(v2.spec.datasource).toEqual(v1.datasource); - - if (typeof v1.query === 'string') { - expect(v2.spec.query.spec[LEGACY_STRING_VALUE_KEY]).toEqual(v1.query); - } else { - expect(v2.spec.query).toEqual({ - kind: v1.datasource?.type, - spec: { - ...(typeof v1.query === 'object' ? v1.query : {}), + expect(v2.spec.query).toMatchObject({ + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: (v1.datasource?.type || getDefaultDataSourceRef()?.type) ?? 'grafana', + ...(v1.datasource?.uid && { + datasource: { + name: v1.datasource?.uid, }, + }), + }); + if (typeof v1.query === 'string') { + expect(v2.spec.query.spec).toEqual({ + [LEGACY_STRING_VALUE_KEY]: v1.query, + }); + } else { + expect(v2.spec.query.spec).toEqual({ + ...(typeof v1.query === 'object' ? v1.query : {}), }); } } @@ -1120,7 +1131,8 @@ describe('ResponseTransformers', () => { } if (v2.kind === 'AdhocVariable') { - expect(v2.spec.datasource).toEqual(v1.datasource); + expect(v2.datasource?.name).toEqual(v1.datasource?.uid); + expect(v2.group).toEqual(v1.datasource?.type); // @ts-expect-error expect(v2.spec.filters).toEqual(v1.filters); // @ts-expect-error @@ -1149,7 +1161,8 @@ describe('ResponseTransformers', () => { } if (v2.kind === 'GroupByVariable') { - expect(v2.spec.datasource).toEqual(v1.datasource); + expect(v2.datasource?.name).toEqual(v1.datasource?.uid); + expect(v2.group).toEqual(v1.datasource?.type); expect(v2.spec.options).toEqual(v1.options); } } diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 2299729a8d7..a8960b0874e 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -38,9 +38,10 @@ import { LibraryPanelKind, PanelKind, GridLayoutItemKind, + defaultDataQueryKind, RowsLayoutRowKind, GridLayoutKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { DashboardLink, DataTransformerConfig } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen'; import { isWeekStart, WeekStart } from '@grafana/ui'; import { @@ -55,6 +56,7 @@ import { DeprecatedInternalId, ObjectMeta, } from 'app/features/apiserver/types'; +import { transformV2ToV1AnnotationQuery } from 'app/features/dashboard-scene/serialization/annotations'; import { GRID_ROW_HEIGHT } from 'app/features/dashboard-scene/serialization/const'; import { validateFiltersOrigin } from 'app/features/dashboard-scene/serialization/sceneVariablesSetToVariables'; import { TypedVariableModelV2 } from 'app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene'; @@ -181,7 +183,7 @@ export function ensureV2Response( }; return { - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', kind: 'DashboardWithAccessInfo', metadata: { creationTimestamp: creationTimestamp || '', // TODO verify this empty string is valid @@ -457,9 +459,13 @@ export function getPanelQueries(targets: DataQuery[], panelDatasource: DataSourc spec: { refId: t.refId, hidden: t.hide ?? false, - datasource: t.datasource ? t.datasource : panelDatasource, query: { - kind: t.datasource?.type || panelDatasource.type!, + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: t.datasource?.type || panelDatasource.type!, + datasource: { + name: t.datasource?.uid || panelDatasource.uid!, + }, spec: { ...query, }, @@ -481,11 +487,12 @@ export function buildPanelKind(p: Panel): PanelKind { title: p.title || '', description: p.description || '', vizConfig: { - kind: p.type, + kind: 'VizConfig', + group: p.type, + version: p.pluginVersion!, spec: { fieldConfig: (p.fieldConfig as any) || defaultFieldConfigSource(), options: p.options as any, - pluginVersion: p.pluginVersion!, }, }, links: @@ -539,6 +546,9 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables'] hide: transformVariableHideToEnum(v.hide), }; + let ds: DataSourceRef | undefined; + let dsType: string | undefined; + switch (v.type) { case 'query': let query = v.query || {}; @@ -569,7 +579,12 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables'] regex: v.regex || '', sort: transformSortVariableToEnum(v.sort), query: { - kind: v.datasource?.type || getDefaultDatasourceType(), + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: v.datasource?.type ?? getDefaultDatasourceType(), + datasource: { + name: v.datasource?.uid, + }, spec: query, }, allowCustomValue: v.allowCustomValue ?? true, @@ -624,17 +639,27 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables'] variables.push(cv); break; case 'adhoc': + ds = v.datasource || getDefaultDatasource(); + dsType = ds.type ?? getDefaultDatasourceType(); + const av: AdhocVariableKind = { kind: 'AdhocVariable', + group: dsType, spec: { ...commonProperties, - datasource: v.datasource || getDefaultDatasource(), baseFilters: validateFiltersOrigin(v.baseFilters) || [], filters: validateFiltersOrigin(v.filters) || [], defaultKeys: v.defaultKeys || [], allowCustomValue: v.allowCustomValue ?? true, }, }; + + if (ds.uid) { + av.datasource = { + name: ds.uid, + }; + } + variables.push(av); break; case 'constant': @@ -688,11 +713,14 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables'] variables.push(tx); break; case 'groupby': + ds = v.datasource || getDefaultDatasource(); + dsType = ds.type ?? getDefaultDatasourceType(); + const gb: GroupByVariableKind = { kind: 'GroupByVariable', + group: dsType, spec: { ...commonProperties, - datasource: v.datasource || getDefaultDatasource(), options: v.options, current: { value: v.current.value, @@ -701,6 +729,13 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables'] multi: v.multi, }, }; + + if (ds.uid) { + gb.datasource = { + name: ds.uid, + }; + } + variables.push(gb); break; default: @@ -723,7 +758,12 @@ function getAnnotations(annotations: AnnotationQuery[]): DashboardV2Spec['annota iconColor: a.iconColor, builtIn: Boolean(a.builtIn), query: { - kind: a.datasource?.type || getDefaultDatasourceType(), + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: a.datasource?.type || getDefaultDatasourceType(), + datasource: { + name: a.datasource?.uid, + }, spec: { ...a.target, }, @@ -758,7 +798,10 @@ function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] { 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, + datasource: { + type: v.spec.query?.spec.group, + uid: v.spec.query?.spec.datasource?.name, + }, sort: transformSortVariableToEnumV1(v.spec.sort), refresh: transformVariableRefreshToEnumV1(v.spec.refresh), regex: v.spec.regex, @@ -850,7 +893,10 @@ function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] { case 'GroupByVariable': const gv: VariableModel = { ...commonProperties, - datasource: v.spec.datasource, + datasource: { + uid: v.datasource?.name, + type: v.group, + }, current: v.spec.current, options: v.spec.options, }; @@ -859,7 +905,10 @@ function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] { case 'AdhocVariable': const av: VariableModel = { ...commonProperties, - datasource: v.spec.datasource, + datasource: { + uid: v.datasource?.name, + type: v.group, + }, // @ts-expect-error baseFilters: v.spec.baseFilters, filters: v.spec.filters, @@ -875,22 +924,6 @@ function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] { return variables; } -function getAnnotationsV1(annotations: DashboardV2Spec['annotations']): AnnotationQuery[] { - // @ts-expect-error - target v2 query is not compatible with v1 target - return annotations.map((a) => { - return { - name: a.spec.name, - datasource: a.spec.datasource, - enable: a.spec.enable, - hide: a.spec.hide, - iconColor: a.spec.iconColor, - builtIn: a.spec.builtIn ? 1 : 0, - target: a.spec.query?.spec, - filter: a.spec.filter, - }; - }); -} - interface LibraryPanelDTO extends Pick {} function getPanelsV1( @@ -934,12 +967,12 @@ function transformV2PanelToV1Panel( const panel = p.spec; return { id: panel.id, - type: panel.vizConfig.kind, + type: panel.vizConfig.group, title: panel.title, description: panel.description, fieldConfig: transformMappingsToV1(panel.vizConfig.spec.fieldConfig), options: panel.vizConfig.spec.options, - pluginVersion: panel.vizConfig.spec.pluginVersion, + pluginVersion: panel.vizConfig.version, links: // @ts-expect-error - Panel link is wrongly typed as DashboardLink panel.links?.map((l) => ({ @@ -951,7 +984,10 @@ function transformV2PanelToV1Panel( return { refId: q.spec.refId, hide: q.spec.hidden, - datasource: q.spec.datasource, + datasource: { + uid: q.spec.query.spec.datasource?.uid, + type: q.spec.query.spec.group, + }, ...q.spec.query.spec, }; }), @@ -1135,7 +1171,8 @@ function transformToV1VariableTypes(variable: TypedVariableModelV2): VariableTyp } export function transformDashboardV2SpecToV1(spec: DashboardV2Spec, metadata: ObjectMeta): DashboardDataDTO { - const annotations = getAnnotationsV1(spec.annotations); + const annotations = spec.annotations.map(transformV2ToV1AnnotationQuery); + const variables = getVariablesV1(spec.variables); const panels = getPanelsV1(spec.elements, spec.layout); return { diff --git a/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts b/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts index 8b9e3cd2f47..737fda747ab 100644 --- a/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts +++ b/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts @@ -2,7 +2,7 @@ import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec, defaultSpec as defaultDashboardV2Spec, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { ResourceList } from 'app/features/apiserver/types'; import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; @@ -64,7 +64,7 @@ describe('UnifiedDashboardAPI', () => { it('should fallback to v2 if v1 throws DashboardVersionError', async () => { const mockV2Response = { spec: { title: 'test' } }; - v1Client.getDashboardDTO.mockRejectedValue(new DashboardVersionError('v2alpha1', 'Dashboard is V1 format')); + v1Client.getDashboardDTO.mockRejectedValue(new DashboardVersionError('v2beta1', 'Dashboard is V1 format')); v2Client.getDashboardDTO.mockResolvedValue(mockV2Response as DashboardWithAccessInfo); const result = await api.getDashboardDTO('123'); @@ -88,7 +88,7 @@ describe('UnifiedDashboardAPI', () => { }, }; - v1Client.getDashboardDTO.mockRejectedValue(new DashboardVersionError('v2alpha1', 'Dashboard is V1 format')); + v1Client.getDashboardDTO.mockRejectedValue(new DashboardVersionError('v2beta1', 'Dashboard is V1 format')); v2Client.getDashboardDTO.mockImplementation((params) => { const actualClient = jest.requireActual('./v2').K8sDashboardV2API; const client = new actualClient(); @@ -152,7 +152,7 @@ describe('UnifiedDashboardAPI', () => { describe('deleteDashboard', () => { it('should not try other version if fails', async () => { - v1Client.deleteDashboard.mockRejectedValue(new DashboardVersionError('v2alpha1', 'Dashboard is V1 format')); + v1Client.deleteDashboard.mockRejectedValue(new DashboardVersionError('v2beta1', 'Dashboard is V1 format')); try { await api.deleteDashboard('123', true); @@ -188,7 +188,7 @@ describe('UnifiedDashboardAPI', () => { { metadata: { name: 'v2-dash', resourceVersion: '123', creationTimestamp: '2023-01-01T00:00:00Z' }, spec: null, - status: { conversion: { failed: true, storedVersion: 'v2alpha1', error: 'conversion failed' } }, + status: { conversion: { failed: true, storedVersion: 'v2beta1', error: 'conversion failed' } }, }, { kind: 'Dashboard', @@ -200,13 +200,13 @@ describe('UnifiedDashboardAPI', () => { ], }; const mockV2Response = { - apiVersion: 'dashboard.grafana.app/v2alpha1', + apiVersion: 'dashboard.grafana.app/v2beta1', kind: 'DashboardList', metadata: { resourceVersion: '456' }, items: [ { kind: 'Dashboard', - apiVersion: 'dashboard.grafana.app/v2alpha1', + apiVersion: 'dashboard.grafana.app/v2beta1', metadata: { name: 'v2-dash', resourceVersion: '456', creationTimestamp: '2023-01-01T00:00:00Z' }, spec: { title: 'v2', elements: {} }, status: {}, @@ -273,7 +273,7 @@ describe('UnifiedDashboardAPI', () => { it('should use v2 client for v2 dashboard resource', async () => { const mockV2Dashboard = { - apiVersion: 'dashboard.grafana.app/v2alpha1', + apiVersion: 'dashboard.grafana.app/v2beta1', kind: 'Dashboard', metadata: { name: 'dash-1', diff --git a/public/app/features/dashboard/api/UnifiedDashboardAPI.ts b/public/app/features/dashboard/api/UnifiedDashboardAPI.ts index 6851a6c0f76..a76049421d2 100644 --- a/public/app/features/dashboard/api/UnifiedDashboardAPI.ts +++ b/public/app/features/dashboard/api/UnifiedDashboardAPI.ts @@ -1,5 +1,5 @@ import { Dashboard } from '@grafana/schema'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { isResource } from 'app/features/apiserver/guards'; import { Resource, ResourceList } from 'app/features/apiserver/types'; import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; @@ -7,7 +7,13 @@ import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; import { DashboardAPI, DashboardVersionError, DashboardWithAccessInfo, ListDeletedDashboardsOptions } from './types'; -import { isDashboardV2Spec, isV1DashboardCommand, isV2DashboardCommand, failedFromVersion } from './utils'; +import { + isDashboardV2Spec, + isV1DashboardCommand, + isV2DashboardCommand, + failedFromVersion, + isV2StoredVersion, +} from './utils'; import { K8sDashboardAPI } from './v1'; import { K8sDashboardV2API } from './v2'; @@ -27,7 +33,7 @@ export class UnifiedDashboardAPI try { return await this.v1Client.getDashboardDTO(uid); } catch (error) { - if (error instanceof DashboardVersionError && error.data.storedVersion === 'v2alpha1') { + if (error instanceof DashboardVersionError && isV2StoredVersion(error.data.storedVersion)) { return await this.v2Client.getDashboardDTO(uid); } throw error; diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts index c4a73deccb7..966de322f62 100644 --- a/public/app/features/dashboard/api/dashboard_api.ts +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -1,5 +1,5 @@ import { Dashboard } from '@grafana/schema'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { DashboardDTO } from 'app/types/dashboard'; import { UnifiedDashboardAPI } from './UnifiedDashboardAPI'; diff --git a/public/app/features/dashboard/api/types.ts b/public/app/features/dashboard/api/types.ts index 90c16d527dc..ff4037da318 100644 --- a/public/app/features/dashboard/api/types.ts +++ b/public/app/features/dashboard/api/types.ts @@ -1,5 +1,5 @@ import { UrlQueryMap } from '@grafana/data'; -import { Status } from '@grafana/schema/src/schema/dashboard/v2alpha1/types.status.gen'; +import { Status } from '@grafana/schema/src/schema/dashboard/v2'; import { ListOptions, Resource, ResourceList } from 'app/features/apiserver/types'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; import { AnnotationsPermissions, SaveDashboardResponseDTO } from 'app/types/dashboard'; @@ -40,7 +40,7 @@ export interface DashboardVersionError extends Error { status: number; data: { // The version which was stored when the dashboard was created / updated. - // Currently known versions are: 'v2alpha1' | 'v1beta1' | 'v0alpha1' + // Currently known versions are: 'v2beta1' | 'v1beta1' | 'v0alpha1' storedVersion: string; message: string; }; diff --git a/public/app/features/dashboard/api/utils.ts b/public/app/features/dashboard/api/utils.ts index d027960bce7..b79cfbacf9a 100644 --- a/public/app/features/dashboard/api/utils.ts +++ b/public/app/features/dashboard/api/utils.ts @@ -1,7 +1,7 @@ import { config, locationService } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; -import { Status } from '@grafana/schema/src/schema/dashboard/v2alpha1/types.status.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; +import { Status } from '@grafana/schema/src/schema/dashboard/v2'; import { Resource } from 'app/features/apiserver/types'; import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; @@ -9,6 +9,10 @@ import { SaveDashboardCommand } from '../components/SaveDashboard/types'; import { DashboardWithAccessInfo } from './types'; +export function isV2StoredVersion(version: string): boolean { + return version === 'v2alpha1' || version === 'v2beta1'; +} + export function getDashboardsApiVersion(responseFormat?: 'v1' | 'v2') { const isDashboardSceneEnabled = config.featureToggles.dashboardScene; const isKubernetesDashboardsEnabled = config.featureToggles.kubernetesDashboards; diff --git a/public/app/features/dashboard/api/v1.test.ts b/public/app/features/dashboard/api/v1.test.ts index 7cdadd0995a..675010235ef 100644 --- a/public/app/features/dashboard/api/v1.test.ts +++ b/public/app/features/dashboard/api/v1.test.ts @@ -281,14 +281,14 @@ describe('v1 dashboard API', () => { }); describe('version error handling', () => { - it('should throw DashboardVersionError for v2alpha1 conversion error', async () => { + it('should throw DashboardVersionError for v2beta1 conversion error', async () => { const mockDashboardWithError = { ...mockDashboardDto, status: { conversion: { failed: true, error: 'backend conversion not yet implemented', - storedVersion: 'v2alpha1', + storedVersion: 'v2beta1', }, }, }; diff --git a/public/app/features/dashboard/api/v1.ts b/public/app/features/dashboard/api/v1.ts index f196e9d71e2..7b6423adc3e 100644 --- a/public/app/features/dashboard/api/v1.ts +++ b/public/app/features/dashboard/api/v1.ts @@ -1,7 +1,7 @@ import { locationUtil } from '@grafana/data'; import { t } from '@grafana/i18n'; import { Dashboard } from '@grafana/schema'; -import { Status } from '@grafana/schema/src/schema/dashboard/v2alpha1/types.status.gen'; +import { Status } from '@grafana/schema/src/schema/dashboard/v2'; import { backendSrv } from 'app/core/services/backend_srv'; import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors'; import kbn from 'app/core/utils/kbn'; @@ -26,6 +26,7 @@ import { DashboardDataDTO, DashboardDTO, SaveDashboardResponseDTO } from 'app/ty import { SaveDashboardCommand } from '../components/SaveDashboard/types'; import { DashboardAPI, DashboardVersionError, DashboardWithAccessInfo, ListDeletedDashboardsOptions } from './types'; +import { isV2StoredVersion } from './utils'; export const K8S_V1_DASHBOARD_API_CONFIG = { group: 'dashboard.grafana.app', @@ -116,7 +117,7 @@ export class K8sDashboardAPI implements DashboardAPI { const dash = await this.client.subresource>(uid, 'dto'); // This could come as conversion error from v0 or v2 to V1. - if (dash.status?.conversion?.failed && dash.status.conversion.storedVersion === 'v2alpha1') { + if (dash.status?.conversion?.failed && isV2StoredVersion(dash.status.conversion.storedVersion)) { throw new DashboardVersionError(dash.status.conversion.storedVersion, dash.status.conversion.error); } diff --git a/public/app/features/dashboard/api/v2.test.ts b/public/app/features/dashboard/api/v2.test.ts index a3b540ae6b6..cb86f4d7467 100644 --- a/public/app/features/dashboard/api/v2.test.ts +++ b/public/app/features/dashboard/api/v2.test.ts @@ -1,7 +1,7 @@ import { Spec as DashboardV2Spec, defaultSpec as defaultDashboardV2Spec, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { backendSrv } from 'app/core/services/backend_srv'; import { AnnoKeyFolder, @@ -37,7 +37,7 @@ const mockGet = jest.fn().mockResolvedValue(mockDashboardDto); const mockPut = jest.fn().mockImplementation((url, data) => { return { - apiVersion: 'dashboard.grafana.app/v2alpha1', + apiVersion: 'dashboard.grafana.app/v2beta1', kind: 'Dashboard', metadata: { name: data.metadata?.name, @@ -53,7 +53,7 @@ const mockPut = jest.fn().mockImplementation((url, data) => { const mockPost = jest.fn().mockImplementation((url, data) => { return { - apiVersion: 'dashboard.grafana.app/v2alpha1', + apiVersion: 'dashboard.grafana.app/v2beta1', kind: 'Dashboard', metadata: { name: data.metadata?.name || 'restored-dash', @@ -220,7 +220,7 @@ describe('v2 dashboard API', () => { }); expect(mockPut).toHaveBeenCalledTimes(1); expect(mockPut).toHaveBeenCalledWith( - '/apis/dashboard.grafana.app/v2alpha1/namespaces/default/dashboards/existing-dash', + '/apis/dashboard.grafana.app/v2beta1/namespaces/default/dashboards/existing-dash', { metadata: { name: 'existing-dash', @@ -329,7 +329,7 @@ describe('v2 dashboard API', () => { conversion: { failed: true, error: 'other-error', - storedVersion: 'v2alpha1', + storedVersion: 'v2beta1', }, }, }; @@ -381,7 +381,7 @@ describe('v2 dashboard API', () => { expect(dashboardToRestore.metadata.resourceVersion).toBe(''); expect(mockPost).toHaveBeenCalledWith( - expect.stringContaining('/apis/dashboard.grafana.app/v2alpha1/'), + expect.stringContaining('/apis/dashboard.grafana.app/v2beta1/'), expect.objectContaining({ metadata: expect.objectContaining({ resourceVersion: '', diff --git a/public/app/features/dashboard/api/v2.ts b/public/app/features/dashboard/api/v2.ts index 676f7b26e27..3cb7dc51468 100644 --- a/public/app/features/dashboard/api/v2.ts +++ b/public/app/features/dashboard/api/v2.ts @@ -1,7 +1,7 @@ import { locationUtil } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; -import { Status } from '@grafana/schema/src/schema/dashboard/v2alpha1/types.status.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; +import { Status } from '@grafana/schema/src/schema/dashboard/v2'; import { backendSrv } from 'app/core/services/backend_srv'; import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors'; import kbn from 'app/core/utils/kbn'; @@ -28,7 +28,7 @@ import { isDashboardV2Spec } from './utils'; export const K8S_V2_DASHBOARD_API_CONFIG = { group: 'dashboard.grafana.app', - version: 'v2alpha1', + version: 'v2beta1', resource: 'dashboards', }; diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index d28f575ce86..ef3ec5fb279 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -4,7 +4,7 @@ import moment from 'moment'; // eslint-disable-line no-restricted-imports import { AppEvents, dateMath, UrlQueryMap, UrlQueryValue } from '@grafana/data'; import { getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; -import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { backendSrv } from 'app/core/services/backend_srv'; import impressionSrv from 'app/core/services/impression_srv'; import kbn from 'app/core/utils/kbn'; diff --git a/public/app/features/dashboard/utils/tracking.ts b/public/app/features/dashboard/utils/tracking.ts index 1bd574edb38..0d5e1ecc30c 100644 --- a/public/app/features/dashboard/utils/tracking.ts +++ b/public/app/features/dashboard/utils/tracking.ts @@ -1,5 +1,5 @@ import { VariableModel } from '@grafana/schema/dist/esm/index'; -import { VariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { VariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; diff --git a/public/app/features/manage-dashboards/state/actions.test.ts b/public/app/features/manage-dashboards/state/actions.test.ts index c4aebee06de..527e175463b 100644 --- a/public/app/features/manage-dashboards/state/actions.test.ts +++ b/public/app/features/manage-dashboards/state/actions.test.ts @@ -7,7 +7,8 @@ import { defaultSpec as defaultDashboardV2Spec, defaultPanelSpec, defaultQueryVariableSpec, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; + defaultDataQueryKind, +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { getLibraryPanel } from 'app/features/library-panels/state/api'; @@ -836,7 +837,9 @@ describe('processV2Datasources', () => { refId: 'A', hidden: false, query: { - kind: 'prometheus', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', spec: { expr: 'access_evaluation_duration_count', range: true, @@ -862,7 +865,9 @@ describe('processV2Datasources', () => { ...defaultQueryVariableSpec(), name: 'var1', query: { - kind: 'loki', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'loki', spec: { expr: 'access_evaluation_duration_count', range: true, @@ -880,7 +885,9 @@ describe('processV2Datasources', () => { hide: false, iconColor: 'red', query: { - kind: 'loki', + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'loki', spec: { expr: 'access_evaluation_duration_count', range: true, @@ -982,13 +989,17 @@ describe('processV2DatasourceInput', () => { ...defaultQueryVariableSpec(), name: 'var2WithGrafanaDs', query: { - kind: 'grafana', + kind: 'DataQuery' as const, + version: defaultDataQueryKind().version, + group: 'grafana', spec: { - panelId: 2, + expr: 'access_evaluation_duration_count', + range: true, }, }, }, }; + const result = await processV2DatasourceInput(queryVariable.spec, {}); expect(result).toEqual({}); }); @@ -1001,7 +1012,9 @@ describe('processV2DatasourceInput', () => { refId: 'A', hidden: false, query: { - kind: 'datasource', + kind: 'DataQuery' as const, + version: defaultDataQueryKind().version, + group: 'datasource', spec: { panelId: 2, }, diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index 5d07c1d66e6..039c5b5999a 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -5,7 +5,7 @@ import { QueryVariableKind, PanelQueryKind, AnnotationQueryKind, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; import { browseDashboardsAPI, ImportInputs } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; @@ -168,6 +168,7 @@ export function processV2Datasources(dashboard: DashboardV2Spec): ThunkResult 0) { for (const query of element.spec.data.spec.queries) { inputs = await processV2DatasourceInput(query.spec, inputs); @@ -332,43 +333,43 @@ export function getFolderByUid(uid: string): Promise<{ uid: string; title: strin } export async function processV2DatasourceInput( - obj: PanelQueryKind['spec'] | QueryVariableKind['spec'] | AnnotationQueryKind['spec'], + spec: PanelQueryKind['spec'] | QueryVariableKind['spec'] | AnnotationQueryKind['spec'], inputs: Record = {} ) { - const datasourceRef = obj?.datasource; - if (!datasourceRef && obj?.query) { - const dsType = obj.query.kind; + const datasourceRef = spec.query.datasource; + let dataSourceInput: DataSourceInput | undefined; + const dsType = spec.query.group; + + if (!datasourceRef) { // if dsType is grafana, it means we are using a built-in annotation or default grafana datasource, in those // cases we don't need to map it // "datasource" type is what we call "--Dashboard--" datasource <.-.> if (dsType === 'grafana' || dsType === 'datasource') { return inputs; } - const datasource = await getDatasourceSrv().get({ type: dsType }); - let dataSourceInput: DataSourceInput | undefined; - if (datasource) { - dataSourceInput = { - name: datasource.name, - label: datasource.name, - info: `Select a ${datasource.name} data source`, - value: datasource.uid, - type: InputType.DataSource, - pluginId: datasource.meta?.id, - }; + } - inputs[datasource.meta?.id] = dataSourceInput; - } else { - dataSourceInput = { - name: dsType, - label: dsType, - info: `No data sources of type ${dsType} found`, - value: '', - type: InputType.DataSource, - pluginId: dsType, - }; - - inputs[dsType] = dataSourceInput; - } + const datasource = await getDatasourceSrv().get({ type: dsType }); + if (datasource) { + dataSourceInput = { + name: datasource.name, + label: datasource.name, + info: `Select a ${datasource.name} data source`, + value: datasource.uid, + type: InputType.DataSource, + pluginId: datasource.meta?.id, + }; + inputs[datasource.meta?.id] = dataSourceInput; + } else { + dataSourceInput = { + name: dsType, + label: dsType, + info: `No data sources of type ${dsType} found`, + value: '', + type: InputType.DataSource, + pluginId: dsType, + }; + inputs[dsType] = dataSourceInput; } return inputs; } diff --git a/public/app/features/manage-dashboards/utils/validation.test.ts b/public/app/features/manage-dashboards/utils/validation.test.ts index d6fb178bf80..e955f7a732f 100644 --- a/public/app/features/manage-dashboards/utils/validation.test.ts +++ b/public/app/features/manage-dashboards/utils/validation.test.ts @@ -1,7 +1,7 @@ import { Spec as DashboardV2Spec, defaultSpec as defaultDashboardV2Spec, -} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +} from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { AnnoKeyFolder, AnnoKeyFolderTitle } from 'app/features/apiserver/types'; import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; @@ -23,7 +23,7 @@ const legacyDashboard: DashboardDTO = { const v2Dashboard: DashboardWithAccessInfo = { kind: 'DashboardWithAccessInfo', - apiVersion: 'v2alpha1', + apiVersion: 'v2beta1', metadata: { creationTimestamp: '2021-09-29T14:00:00Z', name: 'dashboard-uid', From bb45dc88ac06f26e78daa98f1439cf9bf4d9cd66 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 30 Jul 2025 15:23:48 +0200 Subject: [PATCH 046/126] Dashboard: Tweak interaction performance tracking (#108658) * Dashboard: Tweak interaction tracking * Bump scenes to canary (https://github.com/grafana/scenes/pull/1195) * Update naming * Use latest canary * Update cenes, introduce singleton dashboard profiler, instrument v2 schema dashboard * Fix an oooooqpsiiii * Bmp scenes to 6.28.5 * Add documentation * Add event start/end time stamps, update documentation. Bump scenes * Docs * Move file * Typos --- package.json | 4 +- .../pages/DashboardScenePageStateManager.ts | 2 +- .../transformSaveModelSchemaV2ToScene.ts | 15 +- .../transformSaveModelToScene.ts | 59 ++---- .../dashboard/services/DashboardProfiler.ts | 34 +++ .../dashboard-render-performance-profiling.md | 199 ++++++++++++++++++ yarn.lock | 22 +- 7 files changed, 276 insertions(+), 59 deletions(-) create mode 100644 public/app/features/dashboard/services/DashboardProfiler.ts create mode 100644 public/app/features/dashboard/services/dashboard-render-performance-profiling.md diff --git a/package.json b/package.json index 278ffb6efcc..08f37ed064e 100644 --- a/package.json +++ b/package.json @@ -290,8 +290,8 @@ "@grafana/plugin-ui": "0.10.7", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^6.27.2", - "@grafana/scenes-react": "^6.27.2", + "@grafana/scenes": "6.28.6", + "@grafana/scenes-react": "6.28.6", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index d91cf0d2230..63f1aaa0bcd 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -267,7 +267,7 @@ abstract class DashboardScenePageStateManagerBase const queryController = sceneGraph.getQueryController(dashboard); trackDashboardSceneLoaded(dashboard, measure?.duration); - queryController?.startProfile('DashboardScene'); + queryController?.startProfile('dashboard_view'); if (options.route !== DashboardRoutes.New) { emitDashboardViewEvent({ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 986e0018329..2b6dc7d5864 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -51,6 +51,10 @@ import { DeprecatedInternalId, } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; +import { + getDashboardInteractionCallback, + getDashboardSceneProfiler, +} from 'app/features/dashboard/services/DashboardProfiler'; import { DashboardMeta } from 'app/types/dashboard'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; @@ -157,6 +161,15 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo uid === '*' || uid === metadata.name) !== -1, + onProfileComplete: getDashboardInteractionCallback(metadata.name, dashboard.title), + }, + getDashboardSceneProfiler() + ); + const dashboardScene = new DashboardScene( { description: dashboard.description, @@ -184,7 +197,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo uid === '*' || uid === oldModel.uid) !== -1, + onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title), + }, + getDashboardSceneProfiler() + ); + const behaviorList: SceneObjectState['$behaviors'] = [ new behaviors.CursorSync({ sync: oldModel.graphTooltip, }), - new behaviors.SceneQueryController({ - enableProfiling: - config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1, - onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title), - }), + queryController, registerDashboardMacro, registerPanelInteractionsReporter, new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }), @@ -499,40 +507,3 @@ export const convertOldSnapshotToScenesSnapshot = (panel: PanelModel) => { panel.snapshotData = []; } }; - -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 } - ); - }; -} diff --git a/public/app/features/dashboard/services/DashboardProfiler.ts b/public/app/features/dashboard/services/DashboardProfiler.ts new file mode 100644 index 00000000000..aa4ce4d2caa --- /dev/null +++ b/public/app/features/dashboard/services/DashboardProfiler.ts @@ -0,0 +1,34 @@ +import { logMeasurement, reportInteraction } from '@grafana/runtime'; +import { SceneInteractionProfileEvent, SceneRenderProfiler } from '@grafana/scenes'; + +let dashboardSceneProfiler: SceneRenderProfiler | undefined; + +export function getDashboardSceneProfiler() { + if (!dashboardSceneProfiler) { + dashboardSceneProfiler = new SceneRenderProfiler(); + } + return dashboardSceneProfiler; +} + +export function getDashboardInteractionCallback(uid: string, title: string) { + return (e: SceneInteractionProfileEvent) => { + const payload = { + duration: e.duration, + networkDuration: e.networkDuration, + startTs: e.startTs, + endTs: e.endTs, + totalJSHeapSize: e.totalJSHeapSize, + usedJSHeapSize: e.usedJSHeapSize, + jsHeapSizeLimit: e.jsHeapSizeLimit, + timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration, + }; + + reportInteraction('dashboard_render', { + interactionType: e.origin, + uid, + ...payload, + }); + + logMeasurement(`dashboard_render`, payload, { interactionType: e.origin, dashboard: uid, title: title }); + }; +} diff --git a/public/app/features/dashboard/services/dashboard-render-performance-profiling.md b/public/app/features/dashboard/services/dashboard-render-performance-profiling.md new file mode 100644 index 00000000000..574e4f7eb90 --- /dev/null +++ b/public/app/features/dashboard/services/dashboard-render-performance-profiling.md @@ -0,0 +1,199 @@ +# Grafana Dashboard Render Performance Metrics + +This documentation describes the dashboard render performance metrics exposed from Grafana's frontend. + +## Overview + +The exposed dashboard performance metrics feature provides comprehensive tracking and profiling of dashboard interactions, allowing administrators and developers to analyze dashboard render performance, user interactions, and identify performance bottlenecks. + +## Configuration + +### Enabling Performance Metrics + +Dashboard performance metrics are configured in the Grafana configuration file (`grafana.ini`) under the `[dashboards]` section: + +```ini +[dashboards] +# Dashboards UIDs to report performance metrics for. * can be used to report metrics for all dashboards +dashboard_performance_metrics = * +``` + +**Configuration Options:** + +- **`*`** - Enable profiling on all dashboards +- **``** - Enable profiling on specific dashboards only +- **`""` (empty)** - Disable performance metrics (default) + +**Examples:** + +```ini +# Enable for all dashboards +dashboard_performance_metrics = * + +# Enable for specific dashboards +dashboard_performance_metrics = dashboard-uid-1,dashboard-uid-2,dashboard-uid-3 + +# Disable performance metrics +dashboard_performance_metrics = +``` + +## Tracked Interactions + +The system tracks various dashboard interaction types automatically using the [`@grafana/scenes`](https://github.com/grafana/scenes) library. Each interaction is captured with a specific origin identifier that describes the type of user action performed. In Grafana, these interaction events are then reported as `dashboard_render` events with interaction type information included. + +### Core Performance-Tracked Interactions + +The following dashboard interaction types are tracked for dashboard render performance profiling: + +| Interaction Type | Trigger | When Measured | +| ------------------------ | -------------------------- | -------------------------------------------------------- | +| `dashboard_view` | Dashboard view | When user loads or navigates to a dashboard | +| `refresh` | Manual/Auto refresh | When user clicks refresh button or auto-refresh triggers | +| `time_range_change` | Time picker changes | When user changes time range in time picker | +| `filter_added` | Ad-hoc filter addition | When user adds a new filter to the dashboard | +| `filter_removed` | Ad-hoc filter removal | When user removes a filter from the dashboard | +| `filter_changed` | Ad-hoc filter modification | When user changes filter values or operators | +| `filter_restored` | Ad-hoc filter restoration | When user restores a previously applied filter | +| `variable_value_changed` | Variable value changes | When user changes dashboard variable values | +| `scopes_changed` | Scopes modifications | When user modifies dashboard scopes | + +The interactions mentioned above are reported to Echo service as well as sent to [Faro](https://grafana.com/docs/grafana-cloud/monitor-applications/frontend-observability/) as `dashboard_render` measurements: + +```ts +const payload = { + duration: e.duration, + networkDuration: e.networkDuration, + startTs: e.startTs, + endTs: e.endTs, + totalJSHeapSize: e.totalJSHeapSize, + usedJSHeapSize: e.usedJSHeapSize, + jsHeapSizeLimit: e.jsHeapSizeLimit, + timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration, +}; + +reportInteraction('dashboard_render', { + interactionType: e.origin, + uid, + ...payload, +}); + +logMeasurement(`dashboard_render`, payload, { interactionType: e.origin, dashboard: uid, title: title }); +``` + +### Interaction Origin Mapping + +The profiling system uses profiler event's `origin` directly as the `interactionType`, providing direct mapping between user actions and performance measurements. + +## Profiling Implementation + +### Profile Data Structure + +Each interaction profile event captures: + +```typescript +interface SceneInteractionProfileEvent { + origin: string; // Interaction type + duration: number; // Total interaction duration + networkDuration: number; // Network requests duration + totalJSHeapSize: number; // JavaScript heap size metrics + usedJSHeapSize: number; // Used JavaScript heap size + jsHeapSizeLimit: number; // JavaScript heap size limit + startTs: number; // Profile start timestamp + endTs: number; // Profile end timestamp +} +``` + +### Collected Metrics + +For each tracked interaction, the system collects: + +- **Dashboard Metadata**: UID, title +- **Performance Metrics**: Duration, network duration +- **Memory Metrics**: JavaScript heap usage statistics +- **Timing Information**: Time since boot, profile start and end timestamps +- **Interaction Context**: Type of user interaction + +## Debugging and Development + +### Enable Profiler Debug Logging + +To observe profiling events in the browser console: + +```javascript +localStorage.setItem('grafana.debug.scenes', 'true'); +``` + +#### Console Output + +When debug logging is enabled, you'll see console logs for each profiling event: + +``` +SceneRenderProfiler: Profile started: {origin: , crumbs: Array(0)} +... // intermediate steps adding profile crumbs +SceneRenderProfiler: Stopped recording, total measured time (network included): 2123 +``` + +### Enable Echo Service Debug Logging + +To observe Echo events in the browser console: + +```javascript +_debug.echo.enable(); +``` + +#### Console Output + +When Echo debug logging is enabled, you'll see console logs for each profiling event captured by Echo service: + +``` +[EchoSrv: interaction event]: {interactionName: 'dashboard_render', properties: {…}, meta: {…}} +``` + +### Browser Performance Profiler + +Dashboard interactions can be recorded in the browser's performance profiler, where they appear as: + +``` +Dashboard Interaction +``` + +## Analytics Integration + +### Interaction Reporting + +Performance data is integrated with Grafana's analytics system through: + +- **`reportInteraction`**: Reports interaction events to Echo service with performance data +- **`logMeasurement`**: Records Faro's performance measurements with metadata + +### Data Collection + +The system reports the following data for each interaction: + +```typescript +{ + interactionType: string, // Type of interaction + uid: string, // Dashboard UID + duration: number, // Total duration + networkDuration: number, // Network time + startTs: number, // Profile start timestamp + endTs: number, // Profile end timestamp + totalJSHeapSize: number, // Memory metrics + usedJSHeapSize: number, + jsHeapSizeLimit: number, + timeSinceBoot: number // Time since frontend boot +} +``` + +## Implementation Details + +The profiler is integrated into dashboard creation paths and uses a singleton pattern to share profiler instances across dashboard reloads. The performance tracking is implemented using the `SceneRenderProfiler` from the `@grafana/scenes` library. + +## Related Documentation + +- [PR #858 - Add SceneRenderProfiler to scenes](https://github.com/grafana/scenes/pull/858) +- [PR #99629 - Dashboard render performance metrics](https://github.com/grafana/grafana/pull/99629) +- [PR #108658 - Dashboard: Tweak interaction tracking](https://github.com/grafana/grafana/pull/108658) +- [PR #1195 - Enhance SceneRenderProfiler with additional interaction tracking](https://github.com/grafana/scenes/pull/1195) +- [PR #1198 - Make SceneRenderProfiler optional and injectable](https://github.com/grafana/scenes/pull/1198) +- [PR #1199 - SceneRenderProfiler: add start and end timestamps to profile events](https://github.com/grafana/scenes/pull/1199) diff --git a/yarn.lock b/yarn.lock index 7d72c331da7..d8d007fda69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3600,11 +3600,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:^6.27.2": - version: 6.28.2 - resolution: "@grafana/scenes-react@npm:6.28.2" +"@grafana/scenes-react@npm:6.28.6": + version: 6.28.6 + resolution: "@grafana/scenes-react@npm:6.28.6" dependencies: - "@grafana/scenes": "npm:6.28.2" + "@grafana/scenes": "npm:6.28.6" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3616,13 +3616,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/c00730312828639f8a596c9fd9b0336ec57ca5cec624ac4f09a5fa0be93282d180451e3bcbf83c0209ff04486def25ff98d7b0ea6c370e91b8990112c3e903f2 + checksum: 10/55dfe20a6454f218b7c5ab2dc728678885d48e53656d046c125b555b201891e121e0960e108229372c2d70336cfe3dade9b392ed001fef90aec57cbcc13f76ca languageName: node linkType: hard -"@grafana/scenes@npm:6.28.2, @grafana/scenes@npm:^6.27.2": - version: 6.28.2 - resolution: "@grafana/scenes@npm:6.28.2" +"@grafana/scenes@npm:6.28.6": + version: 6.28.6 + resolution: "@grafana/scenes@npm:6.28.6" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3642,7 +3642,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/53370553ac4ac38d41ab1d782ee14cf05f483e35e5338406d619efb61e4c9881fd361c98ee116ae8bf40ab1a5fa2c82ca859fc519f6d532853c275e71949c654 + checksum: 10/fcfcf663e2eb63ad25fad27ec50af8590b1a6c9e802927e8e457bf2f81d04cf971c2f12f456a04a502c929ec7a1d50ef7118baee095134b133ecc92b269a6907 languageName: node linkType: hard @@ -18187,8 +18187,8 @@ __metadata: "@grafana/plugin-ui": "npm:0.10.7" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:^6.27.2" - "@grafana/scenes-react": "npm:^6.27.2" + "@grafana/scenes": "npm:6.28.6" + "@grafana/scenes-react": "npm:6.28.6" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/test-utils": "workspace:*" From 006ea8e1b94b77988a104d24183628c107fdb3a8 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Wed, 30 Jul 2025 10:31:11 -0300 Subject: [PATCH 047/126] ShortURL: Fix tests and small refactor (#108871) --- pkg/api/api.go | 7 +++-- pkg/api/short_url.go | 8 ++++++ pkg/tests/api/shorturl/short_url_test.go | 34 ++++++++++++++++++++---- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 12de28eaa90..1561c28ea21 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -176,7 +176,6 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/import/dashboard", reqSignedIn, hs.Index) r.Get("/dashboards/", reqSignedIn, hs.Index) r.Get("/dashboards/*", reqSignedIn, hs.Index) - r.Get("/goto/:uid", reqSignedIn, hs.redirectFromShortURL, hs.Index) if hs.Cfg.PublicDashboardsEnabled { // list public dashboards @@ -264,6 +263,9 @@ func (hs *HTTPServer) registerRoutes() { providerParam := ac.Parameter(":provider") r.Get("/admin/authentication/:provider", authorize(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsOAuth(providerParam))), hs.Index) + // ShortURL API + hs.registerShortURLAPI(r) + // authed api r.Group("/api", func(apiRoute routing.RouteRegister) { // user (signed in) @@ -549,9 +551,6 @@ func (hs *HTTPServer) registerRoutes() { // Some channels may have info liveRoute.Get("/info/*", routing.Wrap(hs.Live.HandleInfoHTTP)) }, requestmeta.SetSLOGroup(requestmeta.SLOGroupNone)) - - // short urls - apiRoute.Post("/short-urls", routing.Wrap(hs.createShortURL)) }, reqSignedIn) // admin api diff --git a/pkg/api/short_url.go b/pkg/api/short_url.go index 22cb57a31f8..9be4bdab028 100644 --- a/pkg/api/short_url.go +++ b/pkg/api/short_url.go @@ -7,6 +7,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/middleware" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/setting" @@ -14,6 +16,12 @@ import ( "github.com/grafana/grafana/pkg/web" ) +func (hs *HTTPServer) registerShortURLAPI(apiRoute routing.RouteRegister) { + reqSignedIn := middleware.ReqSignedIn + apiRoute.Post("/api/short-urls", reqSignedIn, hs.createShortURL) + apiRoute.Get("/goto/:uid", reqSignedIn, hs.redirectFromShortURL, hs.Index) +} + // createShortURL handles requests to create short URLs. func (hs *HTTPServer) createShortURL(c *contextmodel.ReqContext) response.Response { cmd := dtos.CreateShortURLCmd{} diff --git a/pkg/tests/api/shorturl/short_url_test.go b/pkg/tests/api/shorturl/short_url_test.go index e598c36ea7a..a938b861281 100644 --- a/pkg/tests/api/shorturl/short_url_test.go +++ b/pkg/tests/api/shorturl/short_url_test.go @@ -7,8 +7,10 @@ import ( "fmt" "io" "net/http" + "net/url" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" @@ -31,10 +33,12 @@ func TestMain(m *testing.M) { func TestShortURL(t *testing.T) { dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ AppModeProduction: true, + DisableAnonymous: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + // Test that the endpoint is accessible with authentication. username, password := "viewer", "viewer" createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ DefaultOrgRole: string(org.RoleEditor), @@ -50,7 +54,7 @@ func TestShortURL(t *testing.T) { defer func() { _ = res.Body.Close() }() - require.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, http.StatusOK, res.StatusCode) bodyRaw, err := io.ReadAll(res.Body) require.NoError(t, err) @@ -67,8 +71,8 @@ func TestShortURL(t *testing.T) { defer func() { _ = res.Body.Close() }() - require.Equal(t, "http://localhost:3000/explore", res.Header.Get("Location")) - require.Equal(t, http.StatusFound, res.StatusCode) + assert.Equal(t, "http://localhost:3000/explore", res.Header.Get("Location")) + assert.Equal(t, http.StatusFound, res.StatusCode) // If the go-to does not exist, it should redirect to the home page and return 308. res, err = c.get("/goto/DoesNotExist") @@ -76,8 +80,28 @@ func TestShortURL(t *testing.T) { defer func() { _ = res.Body.Close() }() - require.Equal(t, "http://localhost:3000/", res.Header.Get("Location")) - require.Equal(t, http.StatusPermanentRedirect, res.StatusCode) + assert.Equal(t, "http://localhost:3000/", res.Header.Get("Location")) + assert.Equal(t, http.StatusPermanentRedirect, res.StatusCode) + + // Create a client that does not have authentication. + notLoggedInClient := client(grafanaListedAddr, "", "") + // Test that the short-urls endpoint is not accessible without authentication. + res, err = notLoggedInClient.post("/api/short-urls", bytes.NewReader([]byte(`{"path":"explore"}`))) + require.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + defer func() { + _ = res.Body.Close() + }() + + // If the user is not logged in, it should redirect to the login page and return 302. + res, err = notLoggedInClient.get(fmt.Sprintf("/goto/%s", resParsed.UID)) + require.NoError(t, err) + defer func() { + _ = res.Body.Close() + }() + expectedRedirect := "/login?redirectTo=" + url.QueryEscape("/goto/"+resParsed.UID) + assert.Equal(t, expectedRedirect, res.Header.Get("Location")) + assert.Equal(t, http.StatusFound, res.StatusCode) } func createUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCommand) int64 { From d790fdf081a7d440eee3aed596af264b9f636fb9 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 30 Jul 2025 15:38:43 +0200 Subject: [PATCH 048/126] Schema V2: Fix apiVersion for conversion to v2beta1 (#108916) SchemaV2: Fix apiVersion for conversion to v2beta1 Co-authored-by: Ivan Ortega --- .../conversion/testdata/output/v2alpha1.complete.v2beta1.json | 2 +- .../testdata/output/v2alpha1.ds-data-query.v2beta1.json | 2 +- .../testdata/output/v2alpha1.groupby-adhoc-vars.v2beta1.json | 2 +- .../conversion/testdata/output/v2alpha1.viz-config.v2beta1.json | 2 +- apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json index 617b33a8108..7662f66d855 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json @@ -1,6 +1,6 @@ { "kind": "Dashboard", - "apiVersion": "v2beta1", + "apiVersion": "dashboard.grafana.app/v2beta1", "metadata": { "name": "test-v2alpha1-complete", "creationTimestamp": null, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json index a7f9231756a..ba6bc946dc0 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json @@ -1,6 +1,6 @@ { "kind": "Dashboard", - "apiVersion": "v2beta1", + "apiVersion": "dashboard.grafana.app/v2beta1", "metadata": { "name": "test-v2alpha1-annotations", "creationTimestamp": null diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v2beta1.json index ff37296efe6..9d34353a32b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v2beta1.json @@ -1,6 +1,6 @@ { "kind": "Dashboard", - "apiVersion": "v2beta1", + "apiVersion": "dashboard.grafana.app/v2beta1", "metadata": { "name": "test-v2alpha1-groupby-adhoc-vars", "creationTimestamp": null diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json index 0fe38667e6e..b867057085e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json @@ -1,6 +1,6 @@ { "kind": "Dashboard", - "apiVersion": "v2beta1", + "apiVersion": "dashboard.grafana.app/v2beta1", "metadata": { "name": "test-v2alpha1-viz-config", "creationTimestamp": null diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go index 05d7aa0ada0..ec673c80137 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -39,7 +39,7 @@ import ( func ConvertDashboard_V2alpha1_to_V2beta1(in *dashv2alpha1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta - out.APIVersion = dashv2beta1.VERSION + out.APIVersion = dashv2beta1.APIVERSION out.Kind = in.Kind return convertDashboardSpec_V2alpha1_to_V2beta1(&in.Spec, &out.Spec, scope) From 788e91c57b57f5434921ef68c8b8c6f2f0347cc6 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Wed, 30 Jul 2025 08:43:47 -0500 Subject: [PATCH 049/126] CI: remove nightly builds in drone (#108913) remove nightly builds in drone --- .drone.yml | 260 +---------------------------------------- scripts/drone/rgm.star | 1 - 2 files changed, 1 insertion(+), 260 deletions(-) diff --git a/.drone.yml b/.drone.yml index 345aae7d4bd..2ca377f28a9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2049,264 +2049,6 @@ image_pull_secrets: - gcr - gar kind: pipeline -name: rgm-nightly-build -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - wget -qO- https://github.com/dagger/dagger/releases/download/v0.18.8/dagger_v0.18.8_linux_amd64.tar.gz - | tar zx -C /bin - - apk add docker - - export GRAFANA_DIR=$$(pwd) - - export GITHUB_TOKEN=$(cat /github-app/token) - - ./pkg/build/daggerbuild/scripts/drone_build_nightly_grafana.sh - environment: - _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: - from_secret: dagger_token - ALPINE_BASE: alpine:3.21.3 - CDN_DESTINATION: - from_secret: rgm_cdn_destination - DESTINATION: - from_secret: destination - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USERNAME: - from_secret: docker_username - DOWNLOADS_DESTINATION: - from_secret: rgm_downloads_destination - GCOM_API_KEY: - from_secret: grafana_api_key - GCP_KEY_BASE64: - from_secret: gcp_key_base64 - GPG_PASSPHRASE: - from_secret: packages_gpg_passphrase - GPG_PRIVATE_KEY: - from_secret: packages_gpg_private_key - GPG_PUBLIC_KEY: - from_secret: packages_gpg_public_key - NPM_TOKEN: - from_secret: npm_token - STORYBOOK_DESTINATION: - from_secret: rgm_storybook_destination - UBUNTU_BASE: ubuntu:22.04 - image: golang:1.24.5-alpine - name: rgm-build - pull: always - volumes: - - name: docker - path: /var/run/docker.sock - - name: github-app - path: /github-app -- commands: - - mkdir -p $${DESTINATION}/$${DRONE_BUILD_EVENT} - - printenv GCP_KEY_BASE64 | base64 -d > /tmp/key.json - - gcloud auth activate-service-account --key-file=/tmp/key.json - - gcloud storage cp -r $${DRONE_WORKSPACE}/dist/* $${DESTINATION}/$${DRONE_BUILD_EVENT} - depends_on: - - rgm-build - environment: - _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: - from_secret: dagger_token - CDN_DESTINATION: - from_secret: rgm_cdn_destination - DESTINATION: - from_secret: destination - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USERNAME: - from_secret: docker_username - DOWNLOADS_DESTINATION: - from_secret: rgm_downloads_destination - GCOM_API_KEY: - from_secret: grafana_api_key - GCP_KEY_BASE64: - from_secret: gcp_key_base64 - GPG_PASSPHRASE: - from_secret: packages_gpg_passphrase - GPG_PRIVATE_KEY: - from_secret: packages_gpg_private_key - GPG_PUBLIC_KEY: - from_secret: packages_gpg_public_key - NPM_TOKEN: - from_secret: npm_token - STORYBOOK_DESTINATION: - from_secret: rgm_storybook_destination - image: google/cloud-sdk:alpine - name: rgm-copy -trigger: - cron: - include: - - nightly-release - event: - include: - - cron -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - retries: 3 -depends_on: -- rgm-nightly-build -image_pull_secrets: -- gcr -- gar -kind: pipeline -name: rgm-nightly-publish -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - mkdir -p $${DRONE_WORKSPACE}/dist - - printenv GCP_KEY_BASE64 | base64 -d > /tmp/key.json - - gcloud auth activate-service-account --key-file=/tmp/key.json - - gcloud storage cp -r $${DESTINATION}/$${DRONE_BUILD_EVENT}/*_$${DRONE_BUILD_NUMBER}_* - $${DRONE_WORKSPACE}/dist - environment: - _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: - from_secret: dagger_token - CDN_DESTINATION: - from_secret: rgm_cdn_destination - DESTINATION: - from_secret: destination - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USERNAME: - from_secret: docker_username - DOWNLOADS_DESTINATION: - from_secret: rgm_downloads_destination - GCOM_API_KEY: - from_secret: grafana_api_key - GCP_KEY_BASE64: - from_secret: gcp_key_base64 - GPG_PASSPHRASE: - from_secret: packages_gpg_passphrase - GPG_PRIVATE_KEY: - from_secret: packages_gpg_private_key - GPG_PUBLIC_KEY: - from_secret: packages_gpg_public_key - NPM_TOKEN: - from_secret: npm_token - STORYBOOK_DESTINATION: - from_secret: rgm_storybook_destination - image: google/cloud-sdk:alpine - name: rgm-copy -- commands: - - wget -qO- https://github.com/dagger/dagger/releases/download/v0.18.8/dagger_v0.18.8_linux_amd64.tar.gz - | tar zx -C /bin - - apk add docker - - export GRAFANA_DIR=$$(pwd) - - export GITHUB_TOKEN=$(cat /github-app/token) - - ./pkg/build/daggerbuild/scripts/drone_publish_nightly_grafana.sh - depends_on: - - rgm-copy - environment: - _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: - from_secret: dagger_token - ALPINE_BASE: alpine:3.21.3 - CDN_DESTINATION: - from_secret: rgm_cdn_destination - DESTINATION: - from_secret: destination - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USERNAME: - from_secret: docker_username - DOWNLOADS_DESTINATION: - from_secret: rgm_downloads_destination - GCOM_API_KEY: - from_secret: grafana_api_key - GCP_KEY_BASE64: - from_secret: gcp_key_base64 - GPG_PASSPHRASE: - from_secret: packages_gpg_passphrase - GPG_PRIVATE_KEY: - from_secret: packages_gpg_private_key - GPG_PUBLIC_KEY: - from_secret: packages_gpg_public_key - NPM_TOKEN: - from_secret: npm_token - STORYBOOK_DESTINATION: - from_secret: rgm_storybook_destination - UBUNTU_BASE: ubuntu:22.04 - image: golang:1.24.5-alpine - name: rgm-publish - pull: always - volumes: - - name: docker - path: /var/run/docker.sock - - name: github-app - path: /github-app -- depends_on: - - rgm-publish - image: us.gcr.io/kubernetes-dev/package-publish:latest - name: publish-deb - privileged: true - settings: - access_key_id: - from_secret: packages_access_key_id - gpg_passphrase: - from_secret: packages_gpg_passphrase - gpg_private_key: - from_secret: packages_gpg_private_key - gpg_public_key: - from_secret: packages_gpg_public_key - package_path: file:///drone/src/dist/*.deb - secret_access_key: - from_secret: packages_secret_access_key - service_account_json: - from_secret: packages_service_account - target_bucket: grafana-packages -- depends_on: - - rgm-publish - image: us.gcr.io/kubernetes-dev/package-publish:latest - name: publish-rpm - privileged: true - settings: - access_key_id: - from_secret: packages_access_key_id - gpg_passphrase: - from_secret: packages_gpg_passphrase - gpg_private_key: - from_secret: packages_gpg_private_key - gpg_public_key: - from_secret: packages_gpg_public_key - package_path: file:///drone/src/dist/*.rpm - secret_access_key: - from_secret: packages_secret_access_key - service_account_json: - from_secret: packages_service_account - target_bucket: grafana-packages -trigger: - cron: - include: - - nightly-release - event: - include: - - cron -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - retries: 3 -depends_on: [] -image_pull_secrets: -- gcr -- gar -kind: pipeline name: rgm-promotion node: type: no-parallel @@ -2625,6 +2367,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: aef043aae7394d3160a7147c8b57599bf1a2f4ba5c596ffb795a0e6a049c73a6 +hmac: bfbdbe4aeedbcdc8778dd67abdfe94a75a20bdb314d1c53418cf2c2ea7a9f0c9 ... diff --git a/scripts/drone/rgm.star b/scripts/drone/rgm.star index abaf82b9127..604df62766e 100644 --- a/scripts/drone/rgm.star +++ b/scripts/drone/rgm.star @@ -378,6 +378,5 @@ def rgm(): rgm_main_pipeline() + rgm_tag_pipeline() + rgm_version_branch_pipeline() + - rgm_nightly_pipeline() + rgm_promotion_pipeline() ) From 87f587507a65538bd4eb3ca1ca20be6a8cbadfb5 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 30 Jul 2025 15:01:35 +0100 Subject: [PATCH 050/126] Frontend service: Ensure we're always loading either the light or dark assets for any theme (#108902) ensure we're always loading either the light or dark assets for other themes --- pkg/services/frontend/index.html | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/pkg/services/frontend/index.html b/pkg/services/frontend/index.html index 2ece6114fa5..f582df4604e 100644 --- a/pkg/services/frontend/index.html +++ b/pkg/services/frontend/index.html @@ -86,7 +86,7 @@ .fs-hidden { display: none; } - + .fs-spinner { animation: spin 1500ms linear infinite; width: 32px; @@ -100,7 +100,7 @@ .fs-spinner-arc { stroke: #F55F3E; } - + .fs-loader-text { opacity: 0; font-size: 16px; @@ -199,7 +199,7 @@ async function fetchBootData() { const resp = await fetch("/bootdata"); const textResponse = await resp.text(); - + let rawBootData; try { rawBootData = JSON.parse(textResponse); @@ -211,7 +211,7 @@ if (resp.status === 503 && rawBootData.code === 'Loading') { return; } - + if (!resp.ok) { throw new Error("Unexpected response body: " + textResponse); } @@ -259,21 +259,20 @@ const cssLink = document.createElement("link"); cssLink.rel = 'stylesheet'; - let theme = window.grafanaBootData.user.theme; + const theme = window.grafanaBootData.user.theme; if (theme === "system") { const darkQuery = window.matchMedia("(prefers-color-scheme: dark)"); - theme = darkQuery.matches ? 'dark' : 'light'; - } - if (theme === "light") { - document.body.classList.add("theme-light"); - cssLink.href = window.grafanaBootData.assets.light; - window.grafanaBootData.user.lightTheme = true; - } else if (theme === "dark") { - document.body.classList.add("theme-dark"); - cssLink.href = window.grafanaBootData.assets.dark; - window.grafanaBootData.user.lightTheme = false; + if (darkQuery.matches) { + document.body.classList.add("theme-dark"); + window.grafanaBootData.user.lightTheme = false; + } else { + document.body.classList.add("theme-light"); + window.grafanaBootData.user.lightTheme = true; + } } + const isLightTheme = window.grafanaBootData.user.lightTheme; + cssLink.href = window.grafanaBootData.assets[isLightTheme ? 'light' : 'dark']; document.head.appendChild(cssLink); } From 04c53678fa3e66da71407f9af1a40714dc156d9f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:03:10 +0100 Subject: [PATCH 051/126] Update React Aria (#108895) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 8 +- packages/grafana-ui/package.json | 8 +- yarn.lock | 210 +++++++++++++++---------------- 3 files changed, 113 insertions(+), 113 deletions(-) diff --git a/package.json b/package.json index 08f37ed064e..6abb79b38ea 100644 --- a/package.json +++ b/package.json @@ -310,10 +310,10 @@ "@opentelemetry/exporter-collector": "0.25.0", "@opentelemetry/semantic-conventions": "1.36.0", "@popperjs/core": "2.11.8", - "@react-aria/dialog": "3.5.27", - "@react-aria/focus": "3.20.5", - "@react-aria/overlays": "3.27.3", - "@react-aria/utils": "3.29.1", + "@react-aria/dialog": "3.5.28", + "@react-aria/focus": "3.21.0", + "@react-aria/overlays": "3.28.0", + "@react-aria/utils": "3.30.0", "@react-awesome-query-builder/ui": "6.6.15", "@reduxjs/toolkit": "2.8.2", "@visx/event": "3.12.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 8738aab4d7c..e1ef60e75b3 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -75,10 +75,10 @@ "@hello-pangea/dnd": "18.0.1", "@monaco-editor/react": "4.7.0", "@popperjs/core": "2.11.8", - "@react-aria/dialog": "3.5.27", - "@react-aria/focus": "3.20.5", - "@react-aria/overlays": "3.27.3", - "@react-aria/utils": "3.29.1", + "@react-aria/dialog": "3.5.28", + "@react-aria/focus": "3.21.0", + "@react-aria/overlays": "3.28.0", + "@react-aria/utils": "3.30.0", "@tanstack/react-virtual": "^3.5.1", "@types/jquery": "3.5.32", "@types/lodash": "4.17.20", diff --git a/yarn.lock b/yarn.lock index d8d007fda69..c98bba2096b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3750,10 +3750,10 @@ __metadata: "@hello-pangea/dnd": "npm:18.0.1" "@monaco-editor/react": "npm:4.7.0" "@popperjs/core": "npm:2.11.8" - "@react-aria/dialog": "npm:3.5.27" - "@react-aria/focus": "npm:3.20.5" - "@react-aria/overlays": "npm:3.27.3" - "@react-aria/utils": "npm:3.29.1" + "@react-aria/dialog": "npm:3.5.28" + "@react-aria/focus": "npm:3.21.0" + "@react-aria/overlays": "npm:3.28.0" + "@react-aria/utils": "npm:3.30.0" "@rollup/plugin-node-resolve": "npm:16.0.1" "@storybook/addon-a11y": "npm:^8.6.2" "@storybook/addon-actions": "npm:^8.6.2" @@ -4049,12 +4049,12 @@ __metadata: languageName: node linkType: hard -"@internationalized/number@npm:^3.6.3": - version: 3.6.3 - resolution: "@internationalized/number@npm:3.6.3" +"@internationalized/number@npm:^3.6.4": + version: 3.6.4 + resolution: "@internationalized/number@npm:3.6.4" dependencies: "@swc/helpers": "npm:^0.5.0" - checksum: 10/b6f1793d12f89aca4c5bd88b60643739c4b02cade50a42c61192a2a5e0a913088ee35e4013b7ef3f675bdc919ebaf9a1631f1c40b1ceffd998f3fc14201a01c3 + checksum: 10/1a7f1e56ec689c56d10a2b8cbbfb8be09126048a624ac338963041cc793789ee3ac0f5fb67d1808cabb5de2d287a3bee0d9a156984d0fb291c1b5552c4139579 languageName: node linkType: hard @@ -6443,136 +6443,136 @@ __metadata: languageName: node linkType: hard -"@react-aria/dialog@npm:3.5.27": - version: 3.5.27 - resolution: "@react-aria/dialog@npm:3.5.27" +"@react-aria/dialog@npm:3.5.28": + version: 3.5.28 + resolution: "@react-aria/dialog@npm:3.5.28" dependencies: - "@react-aria/interactions": "npm:^3.25.3" - "@react-aria/overlays": "npm:^3.27.3" - "@react-aria/utils": "npm:^3.29.1" - "@react-types/dialog": "npm:^3.5.19" - "@react-types/shared": "npm:^3.30.0" + "@react-aria/interactions": "npm:^3.25.4" + "@react-aria/overlays": "npm:^3.28.0" + "@react-aria/utils": "npm:^3.30.0" + "@react-types/dialog": "npm:^3.5.20" + "@react-types/shared": "npm:^3.31.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/21257bb14352ab611e5659ce555cd845d8de51d30eea0cf7882b5119848c2136cd35f796a3031bbbcbc9640bffadeb35eee6f6254b34f2791c5a739c550e60da + checksum: 10/b76b89456d1a5b7a24aa5d41c1828b3167b700477baf3b9f401acf2bbb5ad6c393390086a41b331b192a433efbe0ab194c42f945316637c50b54db302095f982 languageName: node linkType: hard -"@react-aria/focus@npm:3.20.5, @react-aria/focus@npm:^3.20.5": - version: 3.20.5 - resolution: "@react-aria/focus@npm:3.20.5" +"@react-aria/focus@npm:3.21.0, @react-aria/focus@npm:^3.21.0": + version: 3.21.0 + resolution: "@react-aria/focus@npm:3.21.0" dependencies: - "@react-aria/interactions": "npm:^3.25.3" - "@react-aria/utils": "npm:^3.29.1" - "@react-types/shared": "npm:^3.30.0" + "@react-aria/interactions": "npm:^3.25.4" + "@react-aria/utils": "npm:^3.30.0" + "@react-types/shared": "npm:^3.31.0" "@swc/helpers": "npm:^0.5.0" clsx: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/543c2f18c1d4f10940662608ca67947d1bc22630ae272e73136b36c2aefec61243148668201442132825bf4e9c0ab64add86c9870b9a51cb7bde2dfab7453a6e + checksum: 10/e894bcdd6c842326f84e764528a9de76b41c7771a768b041118b7858163713bd75a95abdd4158b5dcd20e72205f3679ba35c7cddf8deb7a73349c2a3fbe938ff languageName: node linkType: hard -"@react-aria/i18n@npm:^3.12.10": - version: 3.12.10 - resolution: "@react-aria/i18n@npm:3.12.10" +"@react-aria/i18n@npm:^3.12.11": + version: 3.12.11 + resolution: "@react-aria/i18n@npm:3.12.11" dependencies: "@internationalized/date": "npm:^3.8.2" "@internationalized/message": "npm:^3.1.8" - "@internationalized/number": "npm:^3.6.3" + "@internationalized/number": "npm:^3.6.4" "@internationalized/string": "npm:^3.2.7" - "@react-aria/ssr": "npm:^3.9.9" - "@react-aria/utils": "npm:^3.29.1" - "@react-types/shared": "npm:^3.30.0" + "@react-aria/ssr": "npm:^3.9.10" + "@react-aria/utils": "npm:^3.30.0" + "@react-types/shared": "npm:^3.31.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/6f331ceb62a1364d32f2336257ed82715f712e27d3e04a173296efc320f6532626561e883c2c3639ef4efdcd03498005e87d7160d8942ea8255d18d57770d3a0 + checksum: 10/2b6a904b032df9c4230c2deb723ae68b3c427468b041288ac80df6347a9a09c521d2d146716ec41db2a4a415298e2c5b6aba8d9aaf0e97f2af19c6babe92a9d2 languageName: node linkType: hard -"@react-aria/interactions@npm:^3.25.3": - version: 3.25.3 - resolution: "@react-aria/interactions@npm:3.25.3" +"@react-aria/interactions@npm:^3.25.4": + version: 3.25.4 + resolution: "@react-aria/interactions@npm:3.25.4" dependencies: - "@react-aria/ssr": "npm:^3.9.9" - "@react-aria/utils": "npm:^3.29.1" + "@react-aria/ssr": "npm:^3.9.10" + "@react-aria/utils": "npm:^3.30.0" "@react-stately/flags": "npm:^3.1.2" - "@react-types/shared": "npm:^3.30.0" + "@react-types/shared": "npm:^3.31.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/21754f3e15c35430262b8e283d00010543c8855bac9b9feeb701c967394940d4063b63380b59637c7da8e9b922f29edb3525d15d6005202a6dc22fd32a0d951f + checksum: 10/3ac422fe44a6d39d718a0b30cee00047de580562b928b2f620b4a641ab4f20aceafc30da1561303502a22666493af4311802ff5b5134946ae462e9181a6dc35c languageName: node linkType: hard -"@react-aria/overlays@npm:3.27.3, @react-aria/overlays@npm:^3.27.3": - version: 3.27.3 - resolution: "@react-aria/overlays@npm:3.27.3" +"@react-aria/overlays@npm:3.28.0, @react-aria/overlays@npm:^3.28.0": + version: 3.28.0 + resolution: "@react-aria/overlays@npm:3.28.0" dependencies: - "@react-aria/focus": "npm:^3.20.5" - "@react-aria/i18n": "npm:^3.12.10" - "@react-aria/interactions": "npm:^3.25.3" - "@react-aria/ssr": "npm:^3.9.9" - "@react-aria/utils": "npm:^3.29.1" - "@react-aria/visually-hidden": "npm:^3.8.25" - "@react-stately/overlays": "npm:^3.6.17" - "@react-types/button": "npm:^3.12.2" - "@react-types/overlays": "npm:^3.8.16" - "@react-types/shared": "npm:^3.30.0" + "@react-aria/focus": "npm:^3.21.0" + "@react-aria/i18n": "npm:^3.12.11" + "@react-aria/interactions": "npm:^3.25.4" + "@react-aria/ssr": "npm:^3.9.10" + "@react-aria/utils": "npm:^3.30.0" + "@react-aria/visually-hidden": "npm:^3.8.26" + "@react-stately/overlays": "npm:^3.6.18" + "@react-types/button": "npm:^3.13.0" + "@react-types/overlays": "npm:^3.9.0" + "@react-types/shared": "npm:^3.31.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/8cee1a99bc9ff4ecf0577b76f54cccaf684dde2278d0c270e82c683ef6f42bb8100de4596ae411e775643a3765abbd87fdb1c5d46bf05481ed9df9ed9b82276a + checksum: 10/4b4a682a72e7eabced1ae2cfb81b7715b23c915bc844d8c83f852924c432be430779463b76f533093cba4c3a9affc33b189094874d4fe6c4990d1e6fda8c5773 languageName: node linkType: hard -"@react-aria/ssr@npm:^3.9.9": - version: 3.9.9 - resolution: "@react-aria/ssr@npm:3.9.9" +"@react-aria/ssr@npm:^3.9.10": + version: 3.9.10 + resolution: "@react-aria/ssr@npm:3.9.10" dependencies: "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/d0675357b51784e9dbe3add97aa0e6acc810ab0ef01d1c7a317ff8ead5eae64d66c60cc751ea3d10f874bb381e445099bb31cb7f1955801848ce6dce91b588a2 + checksum: 10/3b414b5b174769e874014604749d9beaf2556f360f61d3df3223bca6150c16dd37fbf16800e669a2b0045bd221df70212756991a426a7a472c56aac6c0dabd1b languageName: node linkType: hard -"@react-aria/utils@npm:3.29.1, @react-aria/utils@npm:^3.29.1": - version: 3.29.1 - resolution: "@react-aria/utils@npm:3.29.1" +"@react-aria/utils@npm:3.30.0, @react-aria/utils@npm:^3.30.0": + version: 3.30.0 + resolution: "@react-aria/utils@npm:3.30.0" dependencies: - "@react-aria/ssr": "npm:^3.9.9" + "@react-aria/ssr": "npm:^3.9.10" "@react-stately/flags": "npm:^3.1.2" - "@react-stately/utils": "npm:^3.10.7" - "@react-types/shared": "npm:^3.30.0" + "@react-stately/utils": "npm:^3.10.8" + "@react-types/shared": "npm:^3.31.0" "@swc/helpers": "npm:^0.5.0" clsx: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/90af5ee5b9c7db063ba66a28cfecb8594f356c2b0fc4cce2fcee94543f3b160214e4a9c280e83355af89452b88b1def8856493705c770874da7121ba1d3d0cc0 + checksum: 10/62870808fe53277eadef3d758759a18e16746eb00332db3dc74f50dd56ea7a2a73fbe1297f138cfb605a80a7e71faf8f5b158e79e35690d82fd301ff21a05ad1 languageName: node linkType: hard -"@react-aria/visually-hidden@npm:^3.8.25": - version: 3.8.25 - resolution: "@react-aria/visually-hidden@npm:3.8.25" +"@react-aria/visually-hidden@npm:^3.8.26": + version: 3.8.26 + resolution: "@react-aria/visually-hidden@npm:3.8.26" dependencies: - "@react-aria/interactions": "npm:^3.25.3" - "@react-aria/utils": "npm:^3.29.1" - "@react-types/shared": "npm:^3.30.0" + "@react-aria/interactions": "npm:^3.25.4" + "@react-aria/utils": "npm:^3.30.0" + "@react-types/shared": "npm:^3.31.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/362a793beac35fde101e598f4cac9cd98394b9a0171309842e2428b25074ee696358c45d2ded8244bc40a9db8471440994be59b0f347d2c643092c6445aaca61 + checksum: 10/2056c0c3e2ced5d7c73364dac36f6122e62cac794313a4935e87e6c776c1f5219a64c9343499b808343b09112d086e150be9d97b67491743993742e8e60d6ed3 languageName: node linkType: hard @@ -6620,27 +6620,27 @@ __metadata: languageName: node linkType: hard -"@react-stately/overlays@npm:^3.6.17": - version: 3.6.17 - resolution: "@react-stately/overlays@npm:3.6.17" +"@react-stately/overlays@npm:^3.6.18": + version: 3.6.18 + resolution: "@react-stately/overlays@npm:3.6.18" dependencies: - "@react-stately/utils": "npm:^3.10.7" - "@react-types/overlays": "npm:^3.8.16" + "@react-stately/utils": "npm:^3.10.8" + "@react-types/overlays": "npm:^3.9.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/9c44f181cd8f0b99a40142aebf3d7e1d1df1e96a789b636a5ee877529e60fddece4ebcf32d9812d9848ce0298bf5cb6202df3b680552b32c89f1276bce3f8a7b + checksum: 10/dfba96e1a1ddac6b94a30536f657972cec9dda17a139c739f7eddc294a413b7012abfe6230136ad688077d21daf10165009ad2fc952b593ddbf9b948cb39596e languageName: node linkType: hard -"@react-stately/utils@npm:^3.10.7": - version: 3.10.7 - resolution: "@react-stately/utils@npm:3.10.7" +"@react-stately/utils@npm:^3.10.8": + version: 3.10.8 + resolution: "@react-stately/utils@npm:3.10.8" dependencies: "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/b9c1a62d4f179e252d08a2f18120a02ffc8704424f257e81e5e74da1222e3acd1e821ce84220d87d9d7b4b6d4dc2fd58d5bb380546d198bf434b700cda8ac3d8 + checksum: 10/7878ec47b132075566708bae630cb86d8237dde976eb3793bba43695abbb29fcaea9d00ea3f4f7244fcda253368f5b2b85263c37665c24e390500cdcc978c6fe languageName: node linkType: hard @@ -6655,26 +6655,26 @@ __metadata: languageName: node linkType: hard -"@react-types/button@npm:^3.12.2": - version: 3.12.2 - resolution: "@react-types/button@npm:3.12.2" +"@react-types/button@npm:^3.13.0": + version: 3.13.0 + resolution: "@react-types/button@npm:3.13.0" dependencies: - "@react-types/shared": "npm:^3.30.0" + "@react-types/shared": "npm:^3.31.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/d00f7e23fd6cfa863c2d9f0f91ec47fc017666b55775842af3c001fabcf949dfa55b116674bcb841de568eacaf677c3a25e079b4810dcf857547bd9cf6d60917 + checksum: 10/11af29a2a5cb777629aa959a05dcd3d1c5a1ad396017e0d81be8837479114c4e7fd0aec283b878f8721883c86a59f4b6497f5e4a892c52b97bd8b9d9c7929ae9 languageName: node linkType: hard -"@react-types/dialog@npm:^3.5.19": - version: 3.5.19 - resolution: "@react-types/dialog@npm:3.5.19" +"@react-types/dialog@npm:^3.5.20": + version: 3.5.20 + resolution: "@react-types/dialog@npm:3.5.20" dependencies: - "@react-types/overlays": "npm:^3.8.16" - "@react-types/shared": "npm:^3.30.0" + "@react-types/overlays": "npm:^3.9.0" + "@react-types/shared": "npm:^3.31.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/07bd55d214d8162f1a700f1ac80cd51468d0defc0d9753309d5e58a0fffbc1eb65a588b6c0709593a41fa8d72cc1f8f8dba0a65b2a336968fdc2ff92afec3596 + checksum: 10/467ea8dbcd3ba4fc1557e42109172d01fda941d35b8e5534da62173a1936645e58b1e4240190da2a792621389c9f2e2cd2bd938a03e0f641a101427ca2e590c1 languageName: node linkType: hard @@ -6701,14 +6701,14 @@ __metadata: languageName: node linkType: hard -"@react-types/overlays@npm:^3.8.12, @react-types/overlays@npm:^3.8.16": - version: 3.8.16 - resolution: "@react-types/overlays@npm:3.8.16" +"@react-types/overlays@npm:^3.8.12, @react-types/overlays@npm:^3.9.0": + version: 3.9.0 + resolution: "@react-types/overlays@npm:3.9.0" dependencies: - "@react-types/shared": "npm:^3.30.0" + "@react-types/shared": "npm:^3.31.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/12bedf8e79d31827cd9868dcb115d07b9fee7954af5e00c1a46fe87b9f2b08e971af4b1b238faa2ce7ed173600786a83c7b9cfcdee5c7464a42bc581d7dee3a6 + checksum: 10/96dbc368f2bba4698d89dd9f7f5c51307686fb7cf0b2d89a51755ca552a9525f5e7f0fda7710d7a1a48e827f1d232024e937aa185deb4127a7da67277d9e580e languageName: node linkType: hard @@ -6721,12 +6721,12 @@ __metadata: languageName: node linkType: hard -"@react-types/shared@npm:^3.27.0, @react-types/shared@npm:^3.30.0": - version: 3.30.0 - resolution: "@react-types/shared@npm:3.30.0" +"@react-types/shared@npm:^3.27.0, @react-types/shared@npm:^3.31.0": + version: 3.31.0 + resolution: "@react-types/shared@npm:3.31.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/58af4134275a347bf18ac5d15e3dcfd07dcf623e1d494f140303e06f2149b7aad86408979e163393346df613f6390a3a387f77a2937f0bf1908c6672a44f7b69 + checksum: 10/906f9f05563b9698485b93168ddd693aef39569753967de832f4fdabc1895c8da0e5add06b8fd23403a4feac42f2b81bd2401454255860144fdf871fbf92fa3f languageName: node linkType: hard @@ -18213,10 +18213,10 @@ __metadata: "@playwright/test": "npm:1.54.1" "@pmmmwh/react-refresh-webpack-plugin": "npm:0.6.1" "@popperjs/core": "npm:2.11.8" - "@react-aria/dialog": "npm:3.5.27" - "@react-aria/focus": "npm:3.20.5" - "@react-aria/overlays": "npm:3.27.3" - "@react-aria/utils": "npm:3.29.1" + "@react-aria/dialog": "npm:3.5.28" + "@react-aria/focus": "npm:3.21.0" + "@react-aria/overlays": "npm:3.28.0" + "@react-aria/utils": "npm:3.30.0" "@react-awesome-query-builder/ui": "npm:6.6.15" "@react-types/button": "npm:3.10.2" "@react-types/menu": "npm:3.9.14" From f7f8a52bda6528d0b25d7264b3694bddc6fcb651 Mon Sep 17 00:00:00 2001 From: Usman Ahmad Date: Wed, 30 Jul 2025 16:03:44 +0200 Subject: [PATCH 052/126] MySQL documentation major improvements (#107690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added keywords and fixed description * Update (dateColumn) description Signed-off-by: Sheikh-Abubaker * Mount docs from the current repository Signed-off-by: Jack Baldry * Clean up variables Signed-off-by: Jack Baldry * Improve timeFilter and timeGroup macro description Signed-off-by: Sheikh-Abubaker * Fix timefilter macro example formatting Signed-off-by: Sheikh-Abubaker * Fix time macro example formatting Signed-off-by: Sheikh-Abubaker * Remove extra spaces from timefilter macro example Signed-off-by: Sheikh-Abubaker * Introduce new table and Add examples for time, timeFilter and timeGroup Macros Signed-off-by: Sheikh-Abubaker * Improve existing examples of timeGroupAlias Macro Signed-off-by: Sheikh-Abubaker * Remove data frame description Signed-off-by: Sheikh-Abubaker * fixed the homepage with valid links Signed-off-by: Usman Ahmad * Add Table Panel results for Macros Signed-off-by: Sheikh-Abubaker * fixed and define the configuration part for grafana mysql user Signed-off-by: Usman Ahmad * fixed port typo Signed-off-by: Usman Ahmad * remove the sqlDatasourceDatabaseSelection based https://github.com/grafana/grafana/issues/105232 Signed-off-by: Usman Ahmad * added annotation single tag example Signed-off-by: Usman Ahmad * Change format type Signed-off-by: Sheikh-Abubaker * fix minor typos and run prettier Signed-off-by: Usman Ahmad * Address Macros specific changes Signed-off-by: Sheikh-Abubaker * Revert column name and replace URL with version substitution Signed-off-by: Sheikh-Abubaker * re-added the text for the feature flag Signed-off-by: Usman Ahmad * revert back the changes for the grafana mysql usage Signed-off-by: Usman Ahmad * fixed links as per writers toolkit guide * run prettier Signed-off-by: Usman Ahmad * Update docs/sources/datasources/mysql/query-editor/_index.md thanks Co-authored-by: Zoltán Bedi --------- Signed-off-by: Sheikh-Abubaker Signed-off-by: Jack Baldry Signed-off-by: Usman Ahmad Co-authored-by: Sheikh-Abubaker Co-authored-by: Jack Baldry Co-authored-by: Zoltán Bedi --- docs/sources/datasources/mysql/_index.md | 5 +- .../datasources/mysql/configuration/_index.md | 12 +- .../datasources/mysql/query-editor/_index.md | 183 +++++++++++------- docs/variables.mk | 16 +- 4 files changed, 128 insertions(+), 88 deletions(-) diff --git a/docs/sources/datasources/mysql/_index.md b/docs/sources/datasources/mysql/_index.md index 33067e51983..f1152a4a762 100644 --- a/docs/sources/datasources/mysql/_index.md +++ b/docs/sources/datasources/mysql/_index.md @@ -2,10 +2,11 @@ aliases: - ../data-sources/mysql/ - ../features/datasources/mysql/ -description: introduction to the MySQL data source in Grafana +description: Introduction to the MySQL data source in Grafana keywords: - grafana - mysql + - data source - guide labels: products: @@ -45,7 +46,7 @@ refs: # MySQL data source -Grafana ships with a built-in MySQL data source plugin that allows you to query and visualize data from a MySQL-compatible database like MariaDB or Percona Server. You don't need to install a plugin in order to add the MySQL data source to your Grafana instance. +Grafana ships with a built-in MySQL data source plugin that allows you to query and visualize data from a MySQL-compatible database like [MariaDB](https://mariadb.org/) or [Percona Server](https://www.percona.com/). You don't need to install a plugin in order to add the MySQL data source to your Grafana instance. Grafana offers several configuration options for this data source as well as a visual and code-based query editor. diff --git a/docs/sources/datasources/mysql/configuration/_index.md b/docs/sources/datasources/mysql/configuration/_index.md index d98aff95019..424345476ad 100644 --- a/docs/sources/datasources/mysql/configuration/_index.md +++ b/docs/sources/datasources/mysql/configuration/_index.md @@ -45,10 +45,14 @@ This document provides instructions for configuring the MySQL data source and ex You must have the `Organization administrator` role in order to configure the MySQL data source. Administrators can also [configure the data source via YAML](#provision-the-data-source) with Grafana's provisioning system. -Grafana ships with the MySQL plugin, so no additional installation is required. - {{< admonition type="note" >}} -When adding a data source, ensure the database user you specify has only `SELECT` permissions on the relevant database and tables. Grafana does not validate the safety of queries, which means they can include potentially harmful SQL statements, such as `USE otherdb;` or `DROP TABLE user;`, which could get executed. To minimize this risk, Grafana strongly recommends creating a dedicated MySQL user with restricted permissions. +Grafana ships with the MySQL data source by default, so no additional installation is required. +{{< /admonition >}} + +{{< admonition type="caution" >}} +When adding a data source, ensure the database user you specify has only `SELECT` permissions on the relevant database and tables. Grafana does not validate the safety of queries, which means they can include potentially harmful SQL statements, such as `USE otherdb;` or `DROP TABLE user;`, which could get executed. + +To minimize this risk, Grafana strongly recommends creating a dedicated MySQL user with restricted permissions. {{< /admonition >}} Example: @@ -81,7 +85,7 @@ Following is a list of MySQL configuration options: **Connection:** -- **Host URL** - Enter the IP address/hostname and optional port of your MySQL instance. If the port is omitted the default 3306 port will be used. +- **Host URL** - Enter the IP address/hostname and optional port of your MySQL instance. If the port is omitted the default `3306` port will be used. - **Database** - Enter the name of your MySQL database. **Authentication:** diff --git a/docs/sources/datasources/mysql/query-editor/_index.md b/docs/sources/datasources/mysql/query-editor/_index.md index 8ec12f2789e..b34bcf65935 100644 --- a/docs/sources/datasources/mysql/query-editor/_index.md +++ b/docs/sources/datasources/mysql/query-editor/_index.md @@ -58,6 +58,9 @@ refs: destination: /docs/grafana//alerting/alerting-rules/templates/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/templates/ + configure-standard-options: + - pattern: /docs/grafana/ + - destination: /docs/grafana//panels-visualizations/configure-standard-options/ --- # MySQL query editor @@ -125,26 +128,26 @@ Changes made to a query in Code mode will not transfer to Builder mode and will You can add macros to your queries to simplify the syntax and enable dynamic elements, such as date range filters. -| Macro example | Description | -| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `$__time(dateColumn)` | Replaces the value with an expression to convert to a UNIX timestamp and renames the column to `time_sec`. Example: _UNIX_TIMESTAMP(dateColumn) AS time_sec_. | -| `$__timeEpoch(dateColumn)` | Replaces the value with an expression to convert to a UNIX Epoch timestamp and renames the column to `time_sec`. Example: _UNIX_TIMESTAMP(dateColumn) AS time_sec_. | -| `$__timeFilter(dateColumn)` | Replaces the value a time range filter using the specified column name. Example: _dateColumn BETWEEN FROM_UNIXTIME(1494410783) AND FROM_UNIXTIME(1494410983)_ | -| `$__timeFrom()` | Replaces the value with the start of the currently active time selection. Example: _FROM_UNIXTIME(1494410783)_ | -| `$__timeTo()` | Replaces the value with the end of the currently active time selection. Example: _FROM_UNIXTIME(1494410983)_ | -| `$__timeGroup(dateColumn,'5m')` | Replaces the value with an expression suitable for use in a GROUP BY clause. Example: *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),\* | -| `$__timeGroup(dateColumn,'5m', 0)` | Same as the `$__timeGroup(dateColumn,'5m')` macro, but includes a fill parameter to ensure missing points in the series are added by Grafana, using 0 as the default value. **This applies only to time series queries.** | -| `$__timeGroup(dateColumn,'5m', NULL)` | Same as the `$__timeGroup(dateColumn,'5m', 0)` but NULL is used as the value for missing points. **This applies only to time series queries.** | -| `$__timeGroup(dateColumn,'5m', previous)` | Same as the `$__timeGroup(dateColumn,'5m', previous)` macro, but uses the previous value in the series as the fill value. If no previous value exists,`NULL` will be used. **This applies only to time series queries.** | -| `$__timeGroupAlias(dateColumn,'5m')` | Replaces the value identical to $\_\_timeGroup but with an added column alias. | -| `$__unixEpochFilter(dateColumn)` | Replaces the value by a time range filter using the specified column name with times represented as a UNIX timestamp. Example: _dateColumn > 1494410783 AND dateColumn < 1494497183_ | -| `$__unixEpochFrom()` | Replaces the value with the start of the currently active time selection as a UNIX timestamp. Example: _1494410783_ | -| `$__unixEpochTo()` | Replaces the value with the end of the currently active time selection as UNIX timestamp. Example: _1494497183_ | -| `$__unixEpochNanoFilter(dateColumn)` | Replaces the value with a time range filter using the specified column name with time represented as a nanosecond timestamp. Example: _dateColumn > 1494410783152415214 AND dateColumn < 1494497183142514872_ | -| `$__unixEpochNanoFrom()` | Replaces the value with the start of the currently active time selection as nanosecond timestamp. Example: _1494410783152415214_ | -| `$__unixEpochNanoTo()` | Replaces the value with the end of the currently active time selection as nanosecond timestamp. Example: _1494497183142514872_ | -| `$__unixEpochGroup(dateColumn,'5m', [fillmode])` | Same as $\_\_timeGroup but for times stored as Unix timestamp. **Note that `fillMode` only works with time series queries.** | -| `$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])` | Same as $\_\_timeGroup but also adds a column alias. **Note that `fillMode` only works with time series queries.** | +| Macro example | Description | +| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `$__time(dateColumn)` | Replaces the value with an expression to convert to a UNIX timestamp and renames the column to `time_sec`. It also helps to recognize the `time` column, as required in Time Series format. Example: _UNIX_TIMESTAMP(dateColumn) AS time_sec_. | +| `$__timeEpoch(dateColumn)` | Replaces the value with an expression to convert to a UNIX Epoch timestamp and renames the column to `time_sec`. Example: _UNIX_TIMESTAMP(dateColumn) AS time_sec_. | +| `$__timeFilter(dateColumn)` | Applies a time range filter using the specified column name and fetches only the data that falls within that range. Example: _dateColumn BETWEEN FROM_UNIXTIME(1494410783) AND FROM_UNIXTIME(1494410983)_ | +| `$__timeFrom()` | Replaces the value with the start of the currently active time selection. Example: _FROM_UNIXTIME(1494410783)_ | +| `$__timeTo()` | Replaces the value with the end of the currently active time selection. Example: _FROM_UNIXTIME(1494410983)_ | +| `$__timeGroup(dateColumn,'5m')` | Replaces the value with an expression suitable for use in a GROUP BY clause and creates the bucket timestamps at a fixed interval. Example: *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),\* | +| `$__timeGroup(dateColumn,'5m', 0)` | Same as the `$__timeGroup(dateColumn,'5m')` macro, but includes a fill parameter to ensure missing points in the series are added by Grafana, using 0 as the default value. **This applies only to time series queries.** | +| `$__timeGroup(dateColumn,'5m', NULL)` | Same as the `$__timeGroup(dateColumn,'5m', 0)` but NULL is used as the value for missing points. **This applies only to time series queries.** | +| `$__timeGroup(dateColumn,'5m', previous)` | Same as the `$__timeGroup(dateColumn,'5m', previous)` macro, but uses the previous value in the series as the fill value. If no previous value exists,`NULL` will be used. **This applies only to time series queries.** | +| `$__timeGroupAlias(dateColumn,'5m')` | Replaces the value identical to $\_\_timeGroup but with an added column alias. | +| `$__unixEpochFilter(dateColumn)` | Replaces the value by a time range filter using the specified column name with times represented as a UNIX timestamp. Example: _dateColumn > 1494410783 AND dateColumn < 1494497183_ | +| `$__unixEpochFrom()` | Replaces the value with the start of the currently active time selection as a UNIX timestamp. Example: _1494410783_ | +| `$__unixEpochTo()` | Replaces the value with the end of the currently active time selection as UNIX timestamp. Example: _1494497183_ | +| `$__unixEpochNanoFilter(dateColumn)` | Replaces the value with a time range filter using the specified column name with time represented as a nanosecond timestamp. Example: _dateColumn > 1494410783152415214 AND dateColumn < 1494497183142514872_ | +| `$__unixEpochNanoFrom()` | Replaces the value with the start of the currently active time selection as nanosecond timestamp. Example: _1494410783152415214_ | +| `$__unixEpochNanoTo()` | Replaces the value with the end of the currently active time selection as nanosecond timestamp. Example: _1494497183142514872_ | +| `$__unixEpochGroup(dateColumn,'5m', [fillmode])` | Same as $\_\_timeGroup but for times stored as Unix timestamp. **Note that `fillMode` only works with time series queries.** | +| `$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])` | Same as $\_\_timeGroup but also adds a column alias. **Note that `fillMode` only works with time series queries.** | ## Table SQL queries @@ -180,106 +183,127 @@ The examples in this section refer to the data in the following table: +---------------------+--------------+---------------------+----------+ | time_date_time | value_double | CreatedAt | hostname | +---------------------+--------------+---------------------+----------+ -| 2020-01-02 03:05:00 | 3.0 | 2020-01-02 03:05:00 | 10.0.1.1 | -| 2020-01-02 03:06:00 | 4.0 | 2020-01-02 03:06:00 | 10.0.1.2 | -| 2020-01-02 03:10:00 | 6.0 | 2020-01-02 03:10:00 | 10.0.1.1 | -| 2020-01-02 03:11:00 | 7.0 | 2020-01-02 03:11:00 | 10.0.1.2 | -| 2020-01-02 03:20:00 | 5.0 | 2020-01-02 03:20:00 | 10.0.1.2 | +| 2025-01-02 03:05:00 | 3.0 | 2025-01-02 03:05:00 | 10.0.1.1 | +| 2025-01-02 03:06:00 | 4.0 | 2025-01-02 03:06:00 | 10.0.1.2 | +| 2025-01-02 03:10:00 | 6.0 | 2025-01-02 03:10:00 | 10.0.1.1 | +| 2025-01-02 03:11:00 | 7.0 | 2025-01-02 03:11:00 | 10.0.1.2 | +| 2025-01-02 03:20:00 | 5.0 | 2025-01-02 03:20:00 | 10.0.1.2 | +---------------------+--------------+---------------------+----------+ ``` -A time series query result is returned in a [wide data frame format](https://grafana.com/developers/plugin-tools/key-concepts/data-frames#wide-format). Any column except time or of type string transforms into value fields in the data frame query result. Any string column transforms into field labels in the data frame query result. - {{< admonition type="note" >}} For backward compatibility, an exception to the aforementioned rule applies to queries returning three columns, including a string column named `metric`. Instead of converting the metric column into field labels, it is used as the field name, and the series name is set to the value of the metric column. Refer to the following example with a metric column. {{< /admonition >}} -**Example with `metric` column:** +**Example with `$__time(dateColumn)` Macro:** + +```sql +SELECT + $__time(time_date_time), + value_double +FROM my_data +ORDER BY time_date_time +``` + +Table panel result: + +{{< figure alt="output of time macro" src="/media/docs/grafana/data-sources/mysql/screenshot-time-and-timefilter-macro.png" >}} + +In the following example, the result includes two columns, `Time` and `value_double`, which represent the data associated with fixed timestamps. This query does not apply a time range filter and returns all rows from the table. + +**Example with `$__timeFilter(dateColumn)` Macro:** + +```sql +SELECT + $__time(time_date_time), + value_double +FROM my_data +WHERE $__timeFilter(time_date_time) +ORDER BY time_date_time +``` + +Table panel result: + +{{< figure alt="output of time filter macro" src="/media/docs/grafana/data-sources/mysql/screenshot-time-and-timefilter-macro.png" >}} + +This example returns the same result as the previous one, but adds support for filtering data using the Grafana time picker. + +**Example with `$__timeGroup(dateColumn,'5m')` Macro:** + +```sql +SELECT + $__timeGroup(time_date_time, '5m') AS time, + sum(value_double) AS sum_value +FROM my_data +WHERE $__timeFilter(time_date_time) +GROUP BY time +ORDER BY time +``` + +Table panel result: + +{{< figure alt="output of time group macro" src="/media/docs/grafana/data-sources/mysql/screenshot-timegroup-macro.png" >}} + +Given the result in the following example, the data is grouped and aggregated within buckets with timestamps of fixed interval i.e. 5 mins. To customize the default series name formatting (optional), refer to [Standard options definitions](ref:configure-standard-options). + +**Example with `$__timeGroupAlias(dateColumn,'5m')` Macro:** ```sql SELECT $__timeGroupAlias(time_date_time,'5m'), min(value_double), 'min' as metric -FROM test_data +FROM my_data WHERE $__timeFilter(time_date_time) GROUP BY time ORDER BY time ``` -Data frame result: +Table panel result: -```text -+---------------------+-----------------+ -| Name: time | Name: min | -| Labels: | Labels: | -| Type: []time.Time | Type: []float64 | -+---------------------+-----------------+ -| 2020-01-02 03:05:00 | 3 | -| 2020-01-02 03:10:00 | 6 | -| 2020-01-02 03:20:00 | 5 | -+---------------------+-----------------+ -``` +{{< figure alt="output of time group alias macro" src="/media/docs/grafana/data-sources/mysql/screenshot-timeGroupAlias-macro.png" >}} -To customize the default series name formatting (optional), refer to [Standard options definitions](ref:configure-standard-options-display-name). +The following result is similar to the result of the `$__timeGroup(dateColumn,'5m')` macro, except it uses a built-in alias for the time column. +To customize the default series name formatting (optional), refer to [Standard options definitions](ref:configure-standard-options). -**Example using the fill parameter in the $\_\_timeGroupAlias macro to convert null values to be zero instead:** +**Example with `$__timeGroupAlias` Macro to convert null values to zero instead:** ```sql SELECT $__timeGroupAlias(createdAt,'5m',0), sum(value_double) as value, hostname -FROM test_data +FROM my_data WHERE $__timeFilter(createdAt) GROUP BY time, hostname ORDER BY time ``` -Given the data frame result in the following example and using the graph panel, you will get two series named _value 10.0.1.1_ and _value 10.0.1.2_. To render the series with a name of _10.0.1.1_ and _10.0.1.2_ , use a [Standard options definitions](ref:configure-standard-options-display-name) display value of `${__field.labels.hostname}`. +Table panel result: -Data frame result: +{{< figure alt="output of null values to zero case, for time group alias macro" src="/media/docs/grafana/data-sources/mysql/screenshot-timeGroupAlias-macro-conv-null-to-zero.png" >}} -```text -+---------------------+---------------------------+---------------------------+ -| Name: time | Name: value | Name: value | -| Labels: | Labels: hostname=10.0.1.1 | Labels: hostname=10.0.1.2 | -| Type: []time.Time | Type: []float64 | Type: []float64 | -+---------------------+---------------------------+---------------------------+ -| 2020-01-02 03:05:00 | 3 | 4 | -| 2020-01-02 03:10:00 | 6 | 7 | -| 2020-01-02 03:15:00 | 0 | 0 | -| 2020-01-02 03:20:00 | 0 | 5 | -+---------------------+---------------------------+---------------------------+ -``` +Given the result in the following example, null values within bucket timestamps are replaced by zero and also add the `Time` column alias by default. To customize the default series name formatting (optional), refer to [Standard options definitions](ref:configure-standard-options) to display the value of `${__field.labels.hostname}`. -**Example with multiple columns:** +**Example with multiple columns for `$__timeGroupAlias(dateColumn,'5m')` Macro:** ```sql SELECT $__timeGroupAlias(time_date_time,'5m'), min(value_double) as min_value, max(value_double) as max_value -FROM test_data +FROM my_data WHERE $__timeFilter(time_date_time) GROUP BY time ORDER BY time ``` -Data frame result: +Table panel result: -```text -+---------------------+-----------------+-----------------+ -| Name: time | Name: min_value | Name: max_value | -| Labels: | Labels: | Labels: | -| Type: []time.Time | Type: []float64 | Type: []float64 | -+---------------------+-----------------+-----------------+ -| 2020-01-02 03:05:00 | 3 | 4 | -| 2020-01-02 03:10:00 | 6 | 7 | -| 2020-01-02 03:20:00 | 5 | 5 | -+---------------------+-----------------+-----------------+ -``` +{{< figure alt="output with multiple colummns for time group alias macro" src="/media/docs/grafana/data-sources/mysql/screenshot-timeGroupAlias-macro-multiple-columns.png" >}} + +The query returns multiple columns representing minimum and maximum values within the defined range. ## Templating @@ -392,6 +416,21 @@ WHERE $__unixEpochFilter(epoch_time) ``` +You may use one or more tags to show them as annotations in a common-separate string. + +**Example query using a `time` column with epoch values for a single tag:** + +```sql +SELECT + epoch_time as time, + metric1 as text, + tag1 as tag +FROM + my_data +WHERE + $__unixEpochFilter(epoch_time) +``` + **Example region query using `time` and `timeend` columns with epoch values:** ```sql diff --git a/docs/variables.mk b/docs/variables.mk index a3302041ce5..1a8c9cb6691 100644 --- a/docs/variables.mk +++ b/docs/variables.mk @@ -1,11 +1,7 @@ # List of projects to provide to the make-docs script. -PROJECTS := grafana - -# Use the doc-validator image defined in CI by default. -export DOC_VALIDATOR_IMAGE := $(shell sed -En 's, *image: "(grafana/doc-validator[^"]+)",\1,p' "$(shell git rev-parse --show-toplevel)/.github/workflows/doc-validator.yml") - -# Skip some doc-validator checks. -export DOC_VALIDATOR_SKIP_CHECKS := $(shell sed -En "s, *'--skip-checks=(.+)',\1,p" "$(shell git rev-parse --show-toplevel)/.github/workflows/doc-validator.yml") - -# Only run on sections that have been enabled in CI. -export DOC_VALIDATOR_INCLUDE := $(shell sed -En "s, *'--include=\\^docs/sources/(.+)',/hugo/content/docs/grafana/latest/\1,p" "$(shell git rev-parse --show-toplevel)/.github/workflows/doc-validator.yml") +# Format is PROJECT[:[VERSION][:[REPOSITORY][:[DIRECTORY]]]] +# The following PROJECTS value mounts content into the "grafana" project, at the "latest" version, which is the default if not explicitly set. +# This results in the content being served at /docs/grafana/latest/. +# The source of the content is the current repository which is determined by the name of the parent directory of the git root. +# This overrides the default behavior of assuming the repository directory is the same as the project name. +PROJECTS := grafana::$(notdir $(basename $(shell git rev-parse --show-toplevel))) From 4a34a2313c08607dfe1b38649df633ce8ccf9b76 Mon Sep 17 00:00:00 2001 From: Adam Yeats <16296989+adamyeats@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:14:44 +0100 Subject: [PATCH 053/126] InfluxDB: Fix backend queries sharing time ranges (#108873) Fix backend queries sharing time ranges --- pkg/tsdb/influxdb/flux/flux.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/tsdb/influxdb/flux/flux.go b/pkg/tsdb/influxdb/flux/flux.go index cab5c0d1067..6643f064910 100644 --- a/pkg/tsdb/influxdb/flux/flux.go +++ b/pkg/tsdb/influxdb/flux/flux.go @@ -27,9 +27,8 @@ func Query(ctx context.Context, dsInfo *models.DatasourceInfo, tsdbQuery backend } defer r.client.Close() - timeRange := tsdbQuery.Queries[0].TimeRange for _, query := range tsdbQuery.Queries { - qm, err := getQueryModel(query, timeRange, dsInfo) + qm, err := getQueryModel(query, query.TimeRange, dsInfo) if err != nil { tRes.Responses[query.RefID] = backend.DataResponse{ Error: err, From 98e37f2ca9b4fa1a6cac0c3a2ae355a5ff60dbd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Sencer=20=C3=96zcan?= <32759850+mustafasencer@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:18:38 +0200 Subject: [PATCH 054/126] fix: special handling of unmarshalling for invalid json dashboards (#108519) * implement special dashboard fallback logic when dashboard data has invalid json behind feature flag --------- Co-authored-by: Will Assis --- .../src/types/featureToggles.gen.ts | 4 + .../datamigrations/to_unified_storage.go | 16 ++-- pkg/registry/apis/dashboard/legacy/migrate.go | 7 +- .../dashboard/legacy/query_dashboards.sql | 1 + .../apis/dashboard/legacy/sql_dashboards.go | 83 ++++++++++++++--- .../dashboard/legacy/sql_dashboards_test.go | 89 ++++++++++++++++--- .../mysql--query_dashboards-dashboard.sql | 1 + ...--query_dashboards-dashboard_next_page.sql | 1 + ...--query_dashboards-export_with_history.sql | 1 + .../mysql--query_dashboards-folders.sql | 1 + .../mysql--query_dashboards-history_uid.sql | 1 + ...uery_dashboards-history_uid_at_version.sql | 1 + ...ery_dashboards-history_uid_second_page.sql | 1 + ...ery_dashboards-migration_with_fallback.sql | 1 + .../postgres--query_dashboards-dashboard.sql | 1 + ...--query_dashboards-dashboard_next_page.sql | 1 + ...--query_dashboards-export_with_history.sql | 1 + .../postgres--query_dashboards-folders.sql | 1 + ...postgres--query_dashboards-history_uid.sql | 1 + ...uery_dashboards-history_uid_at_version.sql | 1 + ...ery_dashboards-history_uid_second_page.sql | 1 + ...ery_dashboards-migration_with_fallback.sql | 1 + .../sqlite--query_dashboards-dashboard.sql | 1 + ...--query_dashboards-dashboard_next_page.sql | 1 + ...--query_dashboards-export_with_history.sql | 1 + .../sqlite--query_dashboards-folders.sql | 1 + .../sqlite--query_dashboards-history_uid.sql | 1 + ...uery_dashboards-history_uid_at_version.sql | 1 + ...ery_dashboards-history_uid_second_page.sql | 1 + ...ery_dashboards-migration_with_fallback.sql | 1 + pkg/registry/apis/dashboard/register.go | 2 +- pkg/server/wire_gen.go | 4 +- 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 +++ 36 files changed, 218 insertions(+), 35 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 90565ef3eb0..138e05f187e 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -302,6 +302,10 @@ export interface FeatureToggles { */ dashboardSchemaValidationLogging?: boolean; /** + * Enable fallback parsing behavior when scan row encounters invalid dashboard JSON + */ + scanRowInvalidDashboardParseFallbackEnabled?: boolean; + /** * Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) */ datasourceQueryTypes?: boolean; 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 8e46423be87..89053e6b932 100644 --- a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go +++ b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go @@ -63,6 +63,12 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err }, } + featureManager, err := featuremgmt.ProvideManagerService(cfg) + if err != nil { + return err + } + featureToggles := featuremgmt.ProvideToggles(featureManager) + provisioning, err := newStubProvisioning(cfg.ProvisioningPath) if err != nil { return err @@ -76,9 +82,10 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err nil, // no librarypanels.Service sort.ProvideService(), acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), + featureToggles, ) - client, err := newUnifiedClient(cfg, sqlStore) + client, err := newUnifiedClient(cfg, sqlStore, featureToggles) if err != nil { return err } @@ -215,12 +222,7 @@ func promptYesNo(prompt string) (bool, error) { } } -func newUnifiedClient(cfg *setting.Cfg, sqlStore db.DB) (resource.ResourceClient, error) { - featureManager, err := featuremgmt.ProvideManagerService(cfg) - if err != nil { - return nil, err - } - featureToggles := featuremgmt.ProvideToggles(featureManager) +func newUnifiedClient(cfg *setting.Cfg, sqlStore db.DB, featureToggles featuremgmt.FeatureToggles) (resource.ResourceClient, error) { return unified.ProvideUnifiedStorageClient(&unified.Options{ Cfg: cfg, Features: featureToggles, diff --git a/pkg/registry/apis/dashboard/legacy/migrate.go b/pkg/registry/apis/dashboard/legacy/migrate.go index 3da3a01aab8..73d01caaade 100644 --- a/pkg/registry/apis/dashboard/legacy/migrate.go +++ b/pkg/registry/apis/dashboard/legacy/migrate.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/search/sort" @@ -51,9 +52,10 @@ func ProvideLegacyMigrator( provisioning provisioning.ProvisioningService, // only needed for dashboard settings libraryPanelSvc librarypanels.Service, accessControl accesscontrol.AccessControl, + features featuremgmt.FeatureToggles, ) LegacyMigrator { dbp := legacysql.NewDatabaseProvider(sql) - return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, libraryPanelSvc, sort.ProvideService(), accessControl) + return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, libraryPanelSvc, sort.ProvideService(), accessControl, features) } type BlobStoreInfo struct { @@ -309,11 +311,12 @@ func (a *dashboardSqlAccess) migrateDashboards(ctx context.Context, orgId int64, for _, row := range rows.rejected { id := row.Dash.Labels[utils.LabelKeyDeprecatedInternalID] a.log.Warn("rejected dashboard", + "namespace", opts.Namespace, "dashboard", row.Dash.Name, "uid", row.Dash.UID, "id", id, + "version", row.Dash.Generation, "stackId", opts.StackID, - "namespace", opts.Namespace, ) opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name)) } diff --git a/pkg/registry/apis/dashboard/legacy/query_dashboards.sql b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql index 75439c60d03..e8503170a2c 100644 --- a/pkg/registry/apis/dashboard/legacy/query_dashboards.sql +++ b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 0771f0fc3ee..f019a39a799 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -29,6 +29,7 @@ import ( "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/featuremgmt" "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/provisioning" @@ -62,6 +63,8 @@ type dashboardSqlAccess struct { namespacer request.NamespaceMapper provisioning provisioning.ProvisioningService + invalidDashboardParseFallbackEnabled bool + // Use for writing (not reading) dashStore dashboards.Store dashboardSearchClient legacysearcher.DashboardSearchClient @@ -82,17 +85,19 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, libraryPanelSvc librarypanels.Service, sorter sort.Service, accessControl accesscontrol.AccessControl, + features featuremgmt.FeatureToggles, ) DashboardAccess { dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter) return &dashboardSqlAccess{ - sql: sql, - namespacer: namespacer, - dashStore: dashStore, - provisioning: provisioning, - dashboardSearchClient: *dashboardSearchClient, - libraryPanelSvc: libraryPanelSvc, - accessControl: accessControl, - log: log.New("dashboard.legacysql"), + sql: sql, + namespacer: namespacer, + dashStore: dashStore, + provisioning: provisioning, + dashboardSearchClient: *dashboardSearchClient, + libraryPanelSvc: libraryPanelSvc, + accessControl: accessControl, + log: log.New("dashboard.legacysql"), + invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled), } } @@ -176,7 +181,7 @@ func (r *rowsWrapper) Next() bool { r.row, err = r.a.scanRow(r.rows, r.history) if err != nil { r.a.log.Error("error scanning dashboard", "error", err) - if len(r.rejected) > 0 || r.row == nil { + if len(r.rejected) > 100 || r.row == nil { r.err = fmt.Errorf("too many rejected rows (%d) %w", len(r.rejected), err) return false } @@ -228,6 +233,51 @@ func (r *rowsWrapper) Value() []byte { return b } +func generateFallbackDashboard(data []byte, title, uid string) ([]byte, error) { + generatedDashboard := map[string]interface{}{ + "editable": true, + "id": 1, + "panels": []map[string]interface{}{ + { + "description": "The JSON is invalid. You can import it again after fixing it.", + "gridPos": map[string]interface{}{"h": 8, "w": 24, "x": 0, "y": 0}, + "id": 1, + "options": map[string]interface{}{ + "code": map[string]interface{}{"language": "plaintext", "showLineNumbers": false, "showMiniMap": false}, + "content": string(data), + "mode": "code", + }, + "title": "Invalid dashboard", + "type": "text", + }, + }, + "schemaVersion": 41, + "title": title, + "uid": uid, + "version": 3, + } + return json.Marshal(generatedDashboard) +} + +func (a *dashboardSqlAccess) parseDashboard(dash *dashboardV1.Dashboard, data []byte, id int64, title string) error { + if err := dash.Spec.UnmarshalJSON(data); err != nil { + a.log.Warn("error unmarshalling dashboard spec. Generating fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name) + dash.Spec = *dashboardV0.NewDashboardSpec() + + dashboardData, err := generateFallbackDashboard(data, title, string(dash.UID)) + if err != nil { + a.log.Warn("error generating fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name) + return err + } + + if err = dash.Spec.UnmarshalJSON(dashboardData); err != nil { + a.log.Warn("error unmarshalling fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name) + return err + } + } + return nil +} + func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRow, error) { dash := &dashboardV1.Dashboard{ TypeMeta: dashboardV1.DashboardResourceInfo.TypeMeta(), @@ -238,6 +288,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo var dashboard_id int64 var orgId int64 var folder_uid sql.NullString + var title string var updated time.Time var updatedBy sql.NullString var updatedByID sql.NullInt64 @@ -257,7 +308,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo var data []byte // the dashboard JSON var version int64 - err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &folder_uid, + err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &title, &folder_uid, &deleted, &plugin_id, &origin_name, &origin_path, &origin_hash, &origin_ts, &created, &createdBy, &createdByID, @@ -286,6 +337,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo dash.SetCreationTimestamp(metav1.NewTime(created)) meta, err := utils.MetaAccessor(dash) if err != nil { + a.log.Debug("failed to get meta accessor for dashboard", "error", err, "uid", dash.UID, "name", dash.Name, "version", version) return nil, err } meta.SetUpdatedTimestamp(&updated) @@ -331,9 +383,14 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo } if len(data) > 0 { - err = dash.Spec.UnmarshalJSON(data) - if err != nil { - return row, fmt.Errorf("JSON unmarshal error for: %s // %w", dash.Name, err) + if a.invalidDashboardParseFallbackEnabled { + if err := a.parseDashboard(dash, data, dashboard_id, title); err != nil { + return row, err + } + } else { + if err := dash.Spec.UnmarshalJSON(data); err != nil { + return row, fmt.Errorf("JSON unmarshal error for: %s // %w", dash.Name, err) + } } } // Ignore any saved values for id/version/uid diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go index 41da615b6ff..da8d2bfe25d 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go @@ -32,13 +32,15 @@ func TestScanRow(t *testing.T) { provisioner := provisioning.NewProvisioningServiceMock(context.Background()) provisioner.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "provisioner" } store := &dashboardSqlAccess{ - namespacer: func(_ int64) string { return "default" }, - provisioning: provisioner, - log: log.New("test"), + namespacer: func(_ int64) string { return "default" }, + provisioning: provisioner, + log: log.New("test"), + invalidDashboardParseFallbackEnabled: false, } - columns := []string{"orgId", "dashboard_id", "name", "folder_uid", "deleted", "plugin_id", "origin_name", "origin_path", "origin_hash", "origin_ts", "created", "createdBy", "createdByID", "updated", "updatedBy", "updatedByID", "version", "message", "data", "api_version"} + columns := []string{"orgId", "dashboard_id", "name", "title", "folder_uid", "deleted", "plugin_id", "origin_name", "origin_path", "origin_hash", "origin_ts", "created", "createdBy", "createdByID", "updated", "updatedBy", "updatedByID", "version", "message", "data", "api_version"} id := int64(100) + uid := "someuid" title := "Test Dashboard" folderUID := "folder123" timestamp := time.Now() @@ -49,7 +51,7 @@ func TestScanRow(t *testing.T) { updatedUser := "updator" t.Run("Should scan a valid row correctly", func(t *testing.T) { - rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz") + rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz") mock.ExpectQuery("SELECT *").WillReturnRows(rows) resultRows, err := mockDB.Query("SELECT *") require.NoError(t, err) @@ -59,7 +61,7 @@ func TestScanRow(t *testing.T) { row, err := store.scanRow(resultRows, false) require.NoError(t, err) require.NotNil(t, row) - require.Equal(t, "Test Dashboard", row.Dash.Name) + require.Equal(t, uid, row.Dash.Name) require.Equal(t, version, row.RV) // rv should be the dashboard version require.Equal(t, common.Unstructured{ Object: map[string]interface{}{"key": "value"}, @@ -80,7 +82,7 @@ func TestScanRow(t *testing.T) { }) t.Run("File provisioned dashboard should have annotations", func(t *testing.T) { - rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "", "provisioner", pathToFile, "hashing", 100000, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz") + rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "provisioner", pathToFile, "hashing", 100000, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz") mock.ExpectQuery("SELECT *").WillReturnRows(rows) resultRows, err := mockDB.Query("SELECT *") require.NoError(t, err) @@ -108,7 +110,7 @@ func TestScanRow(t *testing.T) { }) t.Run("Plugin provisioned dashboard should have annotations", func(t *testing.T) { - rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "slo", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz") + rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "slo", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz") mock.ExpectQuery("SELECT *").WillReturnRows(rows) resultRows, err := mockDB.Query("SELECT *") require.NoError(t, err) @@ -144,7 +146,7 @@ func TestScanRow(t *testing.T) { // In migration scenario, COALESCE functions return dashboard table values // when dashboard_version values are NULL, ensuring all dashboards are migrated rows := sqlmock.NewRows(columns).AddRow( - 1, id, title, folderUID, nil, "", // basic dashboard fields + 1, id, uid, title, folderUID, nil, "", // basic dashboard fields "", "", "", 0, // origin fields timestamp, createdUser, 0, // created fields // These represent COALESCED values from dashboard table (not version table) @@ -163,7 +165,8 @@ func TestScanRow(t *testing.T) { require.NotNil(t, row) // Verify migration scenario works correctly with fallback data - require.Equal(t, title, row.Dash.Name) + require.Equal(t, uid, row.Dash.Name) + require.Equal(t, "Migrated Dashboard", row.Dash.Spec.Object["title"]) require.Equal(t, migrationVersion, row.RV) // Should use COALESCEd dashboard table version require.Equal(t, common.Unstructured{ Object: map[string]interface{}{ @@ -187,6 +190,72 @@ func TestScanRow(t *testing.T) { require.Equal(t, folderUID, meta.GetFolder()) require.Equal(t, "dashboard.grafana.app/"+migrationAPIVersion, row.Dash.APIVersion) }) + + t.Run("should follow dashboard template when failing to unmarshal dashboard if feature flag X is enabled", func(t *testing.T) { + // row with bad data + badData := []byte(`{"rows":[{"panels":[{"targets":[{"refId":"A","target":"aliasSub(alias, '^(.{27}).+', '\1...')"}]}]}]}`) + rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, badData, "vXyz") + mock.ExpectQuery("SELECT *").WillReturnRows(rows) + resultRows, err := mockDB.Query("SELECT *") + require.NoError(t, err) + defer resultRows.Close() // nolint:errcheck + resultRows.Next() + + row, err := store.scanRow(resultRows, false) + require.Error(t, err, "JSON unmarshal error for: Test Dashboard // invalid character '1' in string escape code") + require.NotNil(t, row) + // correctly scans these + require.Equal(t, uid, row.Dash.Name) + require.Equal(t, version, row.RV) + require.Equal(t, "default", row.Dash.Namespace) + require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token) + + // failure case: does NOT parse the dashboard itself + require.Equal(t, common.Unstructured{ + Object: nil, + }, row.Dash.Spec) + + // store with feature flag enabled + store = &dashboardSqlAccess{ + namespacer: func(_ int64) string { return "default" }, + provisioning: provisioner, + log: log.New("test"), + invalidDashboardParseFallbackEnabled: true, + } + + row, err = store.scanRow(resultRows, false) + require.NoError(t, err) + require.NotNil(t, row) + require.Equal(t, uid, row.Dash.Name) + require.Equal(t, version, row.RV) + require.Equal(t, "default", row.Dash.Namespace) + require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token) + + // instead of failing, create dummy dashboard with broken json inlined in text panel + require.Equal(t, title, row.Dash.Spec.Object["title"]) + panels, exists := row.Dash.Spec.Object["panels"] + require.True(t, exists, "panels property should exist") + + panelsSlice, ok := panels.([]interface{}) + require.True(t, ok, "panels should be a slice") + require.Len(t, panelsSlice, 1, "panels should have exactly one element") + + panel, ok := panelsSlice[0].(map[string]interface{}) + require.True(t, ok, "panel should be a map") + + options, exists := panel["options"] + require.True(t, exists, "panel should have options property") + + optionsMap, ok := options.(map[string]interface{}) + require.True(t, ok, "options should be a map") + + content, exists := optionsMap["content"] + require.True(t, exists, "options should have content property") + + contentStr, ok := content.(string) + require.True(t, ok, "content should be a string") + require.Equal(t, string(badData), contentStr, "content should match bad json data") + }) } func TestBuildSaveDashboardCommand(t *testing.T) { diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard.sql index e3a945a53db..63f1231205e 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_next_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_next_page.sql index 862725168b0..0992bbd3ce2 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_next_page.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_next_page.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-export_with_history.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-export_with_history.sql index fd54e6dc949..1d57a86d450 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-export_with_history.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-export_with_history.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-folders.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-folders.sql index 5935eb3422c..38cf8f56bea 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-folders.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-folders.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid.sql index ec0a40a5934..5c1a6974590 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, 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 e04b1ff7430..bf85c6ba3d5 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 @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_second_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_second_page.sql index ec0a40a5934..5c1a6974590 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_second_page.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_second_page.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-migration_with_fallback.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-migration_with_fallback.sql index ba38c048136..8c3801ba771 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-migration_with_fallback.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-migration_with_fallback.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard.sql index 552a486f8cd..c04123f90ab 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_next_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_next_page.sql index fb09d57d08d..5916a9b315f 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_next_page.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_next_page.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-export_with_history.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-export_with_history.sql index 4d0affb337f..5eec54770c7 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-export_with_history.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-export_with_history.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-folders.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-folders.sql index b994d617708..5c9cca1e30e 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-folders.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-folders.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid.sql index 9bcbb168149..876fca02fe3 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, 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 61311c5c99b..0fa23f2db08 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 @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_second_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_second_page.sql index 9bcbb168149..876fca02fe3 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_second_page.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_second_page.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-migration_with_fallback.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-migration_with_fallback.sql index 364fbebf417..fee2c28d525 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-migration_with_fallback.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-migration_with_fallback.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard.sql index 552a486f8cd..c04123f90ab 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_next_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_next_page.sql index fb09d57d08d..5916a9b315f 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_next_page.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_next_page.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-export_with_history.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-export_with_history.sql index 4d0affb337f..5eec54770c7 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-export_with_history.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-export_with_history.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-folders.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-folders.sql index b994d617708..5c9cca1e30e 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-folders.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-folders.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid.sql index 9bcbb168149..876fca02fe3 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, 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 61311c5c99b..0fa23f2db08 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 @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_second_page.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_second_page.sql index 9bcbb168149..876fca02fe3 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_second_page.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_second_page.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-migration_with_fallback.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-migration_with_fallback.sql index 364fbebf417..fee2c28d525 100755 --- a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-migration_with_fallback.sql +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-migration_with_fallback.sql @@ -2,6 +2,7 @@ SELECT dashboard.org_id, dashboard.id, dashboard.uid, + dashboard.title, dashboard.folder_uid, dashboard.deleted, plugin_id, diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 19108edadf8..75e2ed5b518 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -143,7 +143,7 @@ func RegisterAPIService( folderClient: folderClient, legacy: &DashboardStorage{ - Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, accessControl), + Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, accessControl, features), DashboardService: dashboardService, }, reg: reg, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index b398b85d5a9..aeff3aa1924 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -746,7 +746,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser } userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) factory := github.ProvideFactory() - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl) + legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl, featureToggles) databaseDatabase := database5.ProvideDatabase(sqlStore, tracer) secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, registerer) if err != nil { @@ -1307,7 +1307,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { } userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) factory := github.ProvideFactory() - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl) + legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl, featureToggles) databaseDatabase := database5.ProvideDatabase(sqlStore, tracer) secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, registerer) if err != nil { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 0ac3c4d9ce2..f50b7854ed6 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -501,6 +501,12 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaAppPlatformSquad, }, + { + Name: "scanRowInvalidDashboardParseFallbackEnabled", + Description: "Enable fallback parsing behavior when scan row encounters invalid dashboard JSON", + 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 7b5c27e5123..6c21f852785 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -65,6 +65,7 @@ kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,fals dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false dashboardDisableSchemaValidationV2,experimental,@grafana/grafana-app-platform-squad,false,false,false dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squad,false,false,false +scanRowInvalidDashboardParseFallbackEnabled,experimental,@grafana/search-and-storage,false,false,false datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false queryService,experimental,@grafana/grafana-datasources-core-services,false,true,false queryServiceRewrite,experimental,@grafana/grafana-datasources-core-services,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b1cc2fb7f13..52552a0b8d9 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -271,6 +271,10 @@ const ( // Log schema validation errors so they can be analyzed later FlagDashboardSchemaValidationLogging = "dashboardSchemaValidationLogging" + // FlagScanRowInvalidDashboardParseFallbackEnabled + // Enable fallback parsing behavior when scan row encounters invalid dashboard JSON + FlagScanRowInvalidDashboardParseFallbackEnabled = "scanRowInvalidDashboardParseFallbackEnabled" + // 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 048f5512581..980f805d56d 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2760,6 +2760,18 @@ "codeowner": "@grafana/identity-access-team" } }, + { + "metadata": { + "name": "scanRowInvalidDashboardParseFallbackEnabled", + "resourceVersion": "1753730899886", + "creationTimestamp": "2025-07-28T19:28:19Z" + }, + "spec": { + "description": "Enable fallback parsing behavior when scan row encounters invalid dashboard JSON", + "stage": "experimental", + "codeowner": "@grafana/search-and-storage" + } + }, { "metadata": { "name": "scopeApi", From c28b2215e06b37b6c468429617477af4da9a8c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Wed, 30 Jul 2025 16:34:15 +0200 Subject: [PATCH 055/126] Improve indexing observability (#108901) * Add details to tracing spans when creating index. * Log reason for building index. * Log reason for building index. * Remove initialization of labels to avoid unnecessary metrics. * Track succcessful, failed and skiped index builds. Track index build time for individual index, not all indexes. * Revert removal of labels initialization. --- .../unified/resource/bleve_index_metrics.go | 30 +++++++--- pkg/storage/unified/resource/bulk.go | 2 +- pkg/storage/unified/resource/search.go | 57 +++++++++++-------- pkg/storage/unified/resource/search_test.go | 10 ++-- pkg/storage/unified/search/bleve.go | 28 ++++++++- .../unified/search/bleve_search_test.go | 2 +- pkg/storage/unified/search/bleve_test.go | 28 ++++----- pkg/storage/unified/testing/benchmark.go | 2 +- pkg/storage/unified/testing/search_backend.go | 6 +- 9 files changed, 108 insertions(+), 57 deletions(-) diff --git a/pkg/storage/unified/resource/bleve_index_metrics.go b/pkg/storage/unified/resource/bleve_index_metrics.go index 73a18d4fabe..3f130eb0b1b 100644 --- a/pkg/storage/unified/resource/bleve_index_metrics.go +++ b/pkg/storage/unified/resource/bleve_index_metrics.go @@ -9,11 +9,14 @@ import ( ) type BleveIndexMetrics struct { - IndexLatency *prometheus.HistogramVec - IndexSize prometheus.Gauge - IndexedKinds *prometheus.GaugeVec - IndexCreationTime *prometheus.HistogramVec - OpenIndexes *prometheus.GaugeVec + IndexLatency *prometheus.HistogramVec + IndexSize prometheus.Gauge + IndexedKinds *prometheus.GaugeVec + IndexCreationTime *prometheus.HistogramVec + OpenIndexes *prometheus.GaugeVec + IndexBuilds *prometheus.CounterVec + IndexBuildFailures prometheus.Counter + IndexBuildSkipped prometheus.Counter } var IndexCreationBuckets = []float64{1, 5, 10, 25, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} @@ -37,8 +40,8 @@ func ProvideIndexMetrics(reg prometheus.Registerer) *BleveIndexMetrics { Help: "Number of indexed documents by kind", }, []string{"kind"}), IndexCreationTime: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ - Name: "index_server_index_creation_time_seconds", - Help: "Time (in seconds) it takes until index is created", + Name: "index_server_index_build_time_seconds", + Help: "Time it takes to successfully build an index. Failed or skipped builds are not counted.", Buckets: IndexCreationBuckets, NativeHistogramBucketFactor: 1.1, // enable native histograms NativeHistogramMaxBucketNumber: 160, @@ -48,11 +51,22 @@ func ProvideIndexMetrics(reg prometheus.Registerer) *BleveIndexMetrics { Name: "index_server_open_indexes", Help: "Number of open indexes per storage type. An open index corresponds to single resource group.", }, []string{"index_storage"}), // index_storage is either "file" or "memory" + IndexBuilds: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{ + Name: "index_server_index_build_total", + Help: "Number of times index build was attempted due to specific reason", + }, []string{"reason"}), + IndexBuildFailures: promauto.With(reg).NewCounter(prometheus.CounterOpts{ + Name: "index_server_index_build_failures_total", + Help: "Number of times index build failed", + }), + IndexBuildSkipped: promauto.With(reg).NewCounter(prometheus.CounterOpts{ + Name: "index_server_index_build_skipped_total", + Help: "Number of times index build has been skipped due to existing valid index being found on disk", + }), } // Initialize labels. m.OpenIndexes.WithLabelValues("file").Set(0) m.OpenIndexes.WithLabelValues("memory").Set(0) - return m } diff --git a/pkg/storage/unified/resource/bulk.go b/pkg/storage/unified/resource/bulk.go index f1dbb3131c2..c3f2e61e290 100644 --- a/pkg/storage/unified/resource/bulk.go +++ b/pkg/storage/unified/resource/bulk.go @@ -244,7 +244,7 @@ func (s *server) BulkProcess(stream resourcepb.BulkStore_BulkProcessServer) erro Namespace: summary.Namespace, Group: summary.Group, Resource: summary.Resource, - }, summary.Count, summary.ResourceVersion) + }, summary.Count, summary.ResourceVersion, "rebuildAfterBatchLoad") if err != nil { s.log.Warn("error building search index after batch load", "err", err) rsp.Error = &resourcepb.ErrorResult{ diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index bda857993ba..721f7105b30 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -90,7 +90,7 @@ type SearchBackend interface { // Depending on the size, the backend may choose different options (eg: memory vs disk). // The last known resource version can be used to detect that nothing has changed, and existing on-disk index can be reused. // The builder will write all documents before returning. - BuildIndex(ctx context.Context, key NamespacedResource, size int64, resourceVersion int64, nonStandardFields SearchableDocumentFields, builder func(index ResourceIndex) (int64, error)) (ResourceIndex, error) + BuildIndex(ctx context.Context, key NamespacedResource, size int64, resourceVersion int64, nonStandardFields SearchableDocumentFields, indexBuildReason string, builder func(index ResourceIndex) (int64, error)) (ResourceIndex, error) // TotalDocs returns the total number of documents across all indexes. TotalDocs() int64 @@ -196,7 +196,7 @@ func (s *searchSupport) ListManagedObjects(ctx context.Context, req *resourcepb. Namespace: req.Namespace, Group: info.Group, Resource: info.Resource, - }) + }, "listManagedObjects") if err != nil { rsp.Error = AsErrorResult(err) return rsp, nil @@ -237,7 +237,7 @@ func (s *searchSupport) CountManagedObjects(ctx context.Context, req *resourcepb Namespace: req.Namespace, Group: info.Group, Resource: info.Resource, - }) + }, "countManagedObjects") if err != nil { rsp.Error = AsErrorResult(err) return rsp, nil @@ -282,7 +282,7 @@ func (s *searchSupport) Search(ctx context.Context, req *resourcepb.ResourceSear Namespace: req.Options.Key.Namespace, Resource: req.Options.Key.Resource, } - idx, err := s.getOrCreateIndex(ctx, nsr) + idx, err := s.getOrCreateIndex(ctx, nsr, "search") if err != nil { return &resourcepb.ResourceSearchResponse{ Error: AsErrorResult(err), @@ -294,7 +294,7 @@ func (s *searchSupport) Search(ctx context.Context, req *resourcepb.ResourceSear for i, f := range req.Federated { nsr.Group = f.Group nsr.Resource = f.Resource - federate[i], err = s.getOrCreateIndex(ctx, nsr) + federate[i], err = s.getOrCreateIndex(ctx, nsr, "federatedSearch") if err != nil { return &resourcepb.ResourceSearchResponse{ Error: AsErrorResult(err), @@ -323,7 +323,7 @@ func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceSt Namespace: req.Namespace, Group: parts[0], Resource: parts[1], - }) + }, "getStats") if err != nil { rsp.Error = AsErrorResult(err) return rsp, nil @@ -367,7 +367,7 @@ func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceSt Namespace: req.Namespace, Group: stat.Group, Resource: stat.Resource, - }) + }, "getStats") if err != nil { rsp.Error = AsErrorResult(err) return rsp, nil @@ -449,8 +449,12 @@ func (s *searchSupport) buildIndexes(ctx context.Context, rebuild bool) (int, er return err } - s.log.Debug("building index", "namespace", info.Namespace, "group", info.Group, "resource", info.Resource) - _, _, err := s.build(ctx, info.NamespacedResource, info.Count, info.ResourceVersion) + s.log.Debug("building index", "namespace", info.Namespace, "group", info.Group, "resource", info.Resource, "rebuild", rebuild) + reason := "init" + if rebuild { + reason = "rebuild" + } + _, _, err := s.build(ctx, info.NamespacedResource, info.Count, info.ResourceVersion, reason) return err }) } @@ -504,9 +508,6 @@ func (s *searchSupport) init(ctx context.Context) error { end := time.Now().Unix() s.log.Info("search index initialized", "duration_secs", end-start, "total_docs", s.search.TotalDocs()) - if s.indexMetrics != nil { - s.indexMetrics.IndexCreationTime.WithLabelValues().Observe(float64(end - start)) - } return nil } @@ -537,7 +538,7 @@ func (s *searchSupport) dispatchEvent(ctx context.Context, evt *WrittenEvent) { Group: evt.Key.Group, Resource: evt.Key.Resource, } - index, err := s.getOrCreateIndex(ctx, nsr) + index, err := s.getOrCreateIndex(ctx, nsr, "dispatchEvent") if err != nil { s.log.Warn("error getting index for watch event", "error", err) span.RecordError(err) @@ -622,15 +623,10 @@ func (s *searchSupport) rebuildDashboardIndexes(ctx context.Context) error { "duration", duration, "rebuilt_indexes", totalBatchesIndexed, "total_docs", s.search.TotalDocs()) - - if s.indexMetrics != nil { - s.indexMetrics.IndexCreationTime.WithLabelValues().Observe(duration.Seconds()) - } - return nil } -func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedResource) (ResourceIndex, error) { +func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedResource, reason string) (ResourceIndex, error) { if s == nil || s.search == nil { return nil, fmt.Errorf("search is not configured properly (missing unifiedStorageSearch feature toggle?)") } @@ -672,7 +668,7 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso } } - idx, _, err = s.build(ctx, key, size, rv) + idx, _, err = s.build(ctx, key, size, rv, reason) if err != nil { return nil, fmt.Errorf("error building search index, %w", err) } @@ -693,10 +689,18 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso } } -func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size int64, rv int64) (ResourceIndex, int64, error) { +func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size int64, rv int64, indexBuildReason string) (ResourceIndex, int64, error) { ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"Build") defer span.End() + span.SetAttributes( + attribute.String("namespace", nsr.Namespace), + attribute.String("group", nsr.Group), + attribute.String("resource", nsr.Resource), + attribute.Int64("size", size), + attribute.Int64("rv", rv), + ) + logger := s.log.With("namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource) builder, err := s.builders.get(ctx, nsr) @@ -705,7 +709,10 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size } fields := s.builders.GetFields(nsr) - index, err := s.search.BuildIndex(ctx, nsr, size, rv, fields, func(index ResourceIndex) (int64, error) { + index, err := s.search.BuildIndex(ctx, nsr, size, rv, fields, indexBuildReason, func(index ResourceIndex) (int64, error) { + span := trace.SpanFromContext(ctx) + span.AddEvent("building index", trace.WithAttributes(attribute.Int64("size", size), attribute.Int64("rv", rv), attribute.String("reason", indexBuildReason))) + rv, err = s.storage.ListIterator(ctx, &resourcepb.ListRequest{ Limit: 1000000000000, // big number Options: &resourcepb.ListOptions{ @@ -734,9 +741,11 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size Name: iter.Name(), } + span.AddEvent("building document", trace.WithAttributes(attribute.String("name", iter.Name()))) // Convert it to an indexable document doc, err := builder.BuildDocument(ctx, key, iter.ResourceVersion(), iter.Value()) if err != nil { + span.RecordError(err) logger.Error("error building search document", "key", SearchID(key), "err", err) continue } @@ -749,6 +758,7 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size // When we reach the batch size, perform bulk index and reset the batch. if len(items) >= maxBatchSize { + span.AddEvent("bulk indexing", trace.WithAttributes(attribute.Int("count", len(items)))) if err = index.BulkIndex(&BulkIndexRequest{ Items: items, }); err != nil { @@ -762,6 +772,7 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size // Index any remaining items in the final batch. if len(items) > 0 { + span.AddEvent("bulk indexing", trace.WithAttributes(attribute.Int("count", len(items)))) if err = index.BulkIndex(&BulkIndexRequest{ Items: items, }); err != nil { @@ -799,7 +810,7 @@ func (s *searchSupport) buildEmptyIndex(ctx context.Context, nsr NamespacedResou s.log.Debug("Building empty index", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "rv", rv) // Build an empty index by passing a builder function that doesn't add any documents - return s.search.BuildIndex(ctx, nsr, 0, rv, fields, func(index ResourceIndex) (int64, error) { + return s.search.BuildIndex(ctx, nsr, 0, rv, fields, "empty", func(index ResourceIndex) (int64, error) { // Return the resource version without adding any documents to the index return rv, nil }) diff --git a/pkg/storage/unified/resource/search_test.go b/pkg/storage/unified/resource/search_test.go index 07a9a464ae1..a05c74c8a4b 100644 --- a/pkg/storage/unified/resource/search_test.go +++ b/pkg/storage/unified/resource/search_test.go @@ -121,7 +121,7 @@ func (m *mockSearchBackend) GetIndex(ctx context.Context, key NamespacedResource return nil, nil } -func (m *mockSearchBackend) BuildIndex(ctx context.Context, key NamespacedResource, size int64, resourceVersion int64, fields SearchableDocumentFields, builder func(index ResourceIndex) (int64, error)) (ResourceIndex, error) { +func (m *mockSearchBackend) BuildIndex(ctx context.Context, key NamespacedResource, size int64, resourceVersion int64, fields SearchableDocumentFields, reason string, builder func(index ResourceIndex) (int64, error)) (ResourceIndex, error) { index := &MockResourceIndex{} index.On("BulkIndex", mock.Anything).Return(nil).Maybe() index.On("DocCount", mock.Anything, mock.Anything).Return(int64(0), nil).Maybe() @@ -317,7 +317,7 @@ func TestSearchGetOrCreateIndex(t *testing.T) { go func() { defer wg.Done() <-start - _, _ = support.getOrCreateIndex(context.Background(), NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}) + _, _ = support.getOrCreateIndex(context.Background(), NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, "test") }() } @@ -365,7 +365,7 @@ func TestSearchGetOrCreateIndexWithCancellation(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() - _, err = support.getOrCreateIndex(ctx, NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}) + _, err = support.getOrCreateIndex(ctx, NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, "test") // Make sure we get context deadline error require.ErrorIs(t, err, context.DeadlineExceeded) @@ -380,9 +380,9 @@ type slowSearchBackend struct { wg sync.WaitGroup } -func (m *slowSearchBackend) BuildIndex(ctx context.Context, key NamespacedResource, size int64, resourceVersion int64, fields SearchableDocumentFields, builder func(index ResourceIndex) (int64, error)) (ResourceIndex, error) { +func (m *slowSearchBackend) BuildIndex(ctx context.Context, key NamespacedResource, size int64, resourceVersion int64, fields SearchableDocumentFields, reason string, builder func(index ResourceIndex) (int64, error)) (ResourceIndex, error) { m.wg.Add(1) defer m.wg.Done() time.Sleep(1 * time.Second) - return m.mockSearchBackend.BuildIndex(ctx, key, size, resourceVersion, fields, builder) + return m.mockSearchBackend.BuildIndex(ctx, key, size, resourceVersion, fields, reason, builder) } diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 141e02b4030..2626ab36f1d 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -23,6 +23,7 @@ import ( "github.com/blevesearch/bleve/v2/search/query" bleveSearch "github.com/blevesearch/bleve/v2/search/searcher" index "github.com/blevesearch/bleve_index_api" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/selection" @@ -197,11 +198,21 @@ func (b *bleveBackend) BuildIndex( size int64, resourceVersion int64, fields resource.SearchableDocumentFields, + indexBuildReason string, builder func(index resource.ResourceIndex) (int64, error), ) (resource.ResourceIndex, error) { _, span := b.tracer.Start(ctx, tracingPrexfixBleve+"BuildIndex") defer span.End() + span.SetAttributes( + attribute.String("namespace", key.Namespace), + attribute.String("group", key.Group), + attribute.String("resource", key.Resource), + attribute.Int64("size", size), + attribute.Int64("rv", resourceVersion), + attribute.String("reason", indexBuildReason), + ) + mapper, err := GetBleveMappings(fields) if err != nil { return nil, err @@ -214,7 +225,7 @@ func (b *bleveBackend) BuildIndex( return nil, err } - logWithDetails := b.log.With("namespace", key.Namespace, "group", key.Group, "resource", key.Resource, "size", size, "rv", resourceVersion) + logWithDetails := b.log.With("namespace", key.Namespace, "group", key.Group, "resource", key.Resource, "size", size, "rv", resourceVersion, "reason", indexBuildReason) // Close the newly created/opened index by default. closeIndex := true @@ -306,14 +317,29 @@ func (b *bleveBackend) BuildIndex( } if build { + if b.indexMetrics != nil { + b.indexMetrics.IndexBuilds.WithLabelValues(indexBuildReason).Inc() + } + start := time.Now() _, err = builder(idx) if err != nil { logWithDetails.Error("Failed to build index", "err", err) + if b.indexMetrics != nil { + b.indexMetrics.IndexBuildFailures.Inc() + } return nil, fmt.Errorf("failed to build index: %w", err) } elapsed := time.Since(start) logWithDetails.Info("Finished building index", "elapsed", elapsed) + if b.indexMetrics != nil { + b.indexMetrics.IndexCreationTime.WithLabelValues().Observe(elapsed.Seconds()) + } + } else { + logWithDetails.Info("Skipping index build, using existing index") + if b.indexMetrics != nil { + b.indexMetrics.IndexBuildSkipped.Inc() + } } // Set expiration after building the index. Only expire in-memory indexes. diff --git a/pkg/storage/unified/search/bleve_search_test.go b/pkg/storage/unified/search/bleve_search_test.go index a795b4c0f8d..0680026e2af 100644 --- a/pkg/storage/unified/search/bleve_search_test.go +++ b/pkg/storage/unified/search/bleve_search_test.go @@ -553,7 +553,7 @@ func newTestDashboardsIndex(t TB, threshold int64, size int64, batchSize int64, Namespace: key.Namespace, Group: key.Group, Resource: key.Resource, - }, size, rv, info.Fields, writer) + }, size, rv, info.Fields, "test", writer) require.NoError(t, err) return index, tmpdir diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index 5a4c16be2b3..821ce6de0c0 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -71,7 +71,7 @@ func TestBleveBackend(t *testing.T) { Namespace: key.Namespace, Group: key.Group, Resource: key.Resource, - }, 2, rv, info.Fields, func(index resource.ResourceIndex) (int64, error) { + }, 2, rv, info.Fields, "test", func(index resource.ResourceIndex) (int64, error) { err := index.BulkIndex(&resource.BulkIndexRequest{ Items: []*resource.BulkIndexItem{ { @@ -352,7 +352,7 @@ func TestBleveBackend(t *testing.T) { Namespace: key.Namespace, Group: key.Group, Resource: key.Resource, - }, 2, rv, fields, func(index resource.ResourceIndex) (int64, error) { + }, 2, rv, fields, "test", func(index resource.ResourceIndex) (int64, error) { err := index.BulkIndex(&resource.BulkIndexRequest{ Items: []*resource.BulkIndexItem{ { @@ -766,7 +766,7 @@ func TestBleveInMemoryIndexExpiration(t *testing.T) { Resource: "resource", } - builtIndex, err := backend.BuildIndex(context.Background(), ns, 1 /* below FileThreshold */, 100, nil, indexTestDocs(ns, 1)) + builtIndex, err := backend.BuildIndex(context.Background(), ns, 1 /* below FileThreshold */, 100, nil, "test", indexTestDocs(ns, 1)) require.NoError(t, err) // Wait for index expiration, which is 1ns @@ -798,7 +798,7 @@ func TestBleveFileIndexExpiration(t *testing.T) { } // size=100 is above FileThreshold, this will be file-based index - builtIndex, err := backend.BuildIndex(context.Background(), ns, 100, 100, nil, indexTestDocs(ns, 1)) + builtIndex, err := backend.BuildIndex(context.Background(), ns, 100, 100, nil, "test", indexTestDocs(ns, 1)) require.NoError(t, err) // Wait for index expiration, which is 1ns @@ -830,7 +830,7 @@ func TestFileIndexIsReusedOnSameSizeAndRV(t *testing.T) { tmpDir := t.TempDir() backend1, reg1 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) - _, err := backend1.BuildIndex(context.Background(), ns, 10 /* file based */, 100, nil, indexTestDocs(ns, 10)) + _, err := backend1.BuildIndex(context.Background(), ns, 10 /* file based */, 100, nil, "test", indexTestDocs(ns, 10)) require.NoError(t, err) // Verify one open index. @@ -853,7 +853,7 @@ func TestFileIndexIsReusedOnSameSizeAndRV(t *testing.T) { // We open new backend using same directory, and run indexing with same size (10) and RV (100). This should reuse existing index, and skip indexing. backend2, reg2 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) - idx, err := backend2.BuildIndex(context.Background(), ns, 10 /* file based */, 100, nil, indexTestDocs(ns, 1000)) + idx, err := backend2.BuildIndex(context.Background(), ns, 10 /* file based */, 100, nil, "test", indexTestDocs(ns, 1000)) require.NoError(t, err) // Verify that we're reusing existing index and there is only 10 documents in it, not 1000. @@ -879,13 +879,13 @@ func TestFileIndexIsNotReusedOnDifferentSize(t *testing.T) { tmpDir := t.TempDir() backend1, _ := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) - _, err := backend1.BuildIndex(context.Background(), ns, 10, 100, nil, indexTestDocs(ns, 10)) + _, err := backend1.BuildIndex(context.Background(), ns, 10, 100, nil, "test", indexTestDocs(ns, 10)) require.NoError(t, err) backend1.closeAllIndexes() // We open new backend using same directory, but with different size. Index should be rebuilt. backend2, _ := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) - idx, err := backend2.BuildIndex(context.Background(), ns, 100, 100, nil, indexTestDocs(ns, 100)) + idx, err := backend2.BuildIndex(context.Background(), ns, 100, 100, nil, "test", indexTestDocs(ns, 100)) require.NoError(t, err) // Verify that index has updated number of documents. @@ -904,13 +904,13 @@ func TestFileIndexIsNotReusedOnDifferentRV(t *testing.T) { tmpDir := t.TempDir() backend1, _ := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) - _, err := backend1.BuildIndex(context.Background(), ns, 10, 100, nil, indexTestDocs(ns, 10)) + _, err := backend1.BuildIndex(context.Background(), ns, 10, 100, nil, "test", indexTestDocs(ns, 10)) require.NoError(t, err) backend1.closeAllIndexes() // We open new backend using same directory, but with different RV. Index should be rebuilt. backend2, _ := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) - idx, err := backend2.BuildIndex(context.Background(), ns, 10 /* file based */, 999999, nil, indexTestDocs(ns, 100)) + idx, err := backend2.BuildIndex(context.Background(), ns, 10 /* file based */, 999999, nil, "test", indexTestDocs(ns, 100)) require.NoError(t, err) // Verify that index has updated number of documents. @@ -942,7 +942,7 @@ func TestRebuildingIndexClosesPreviousCachedIndex(t *testing.T) { if testCase.firstInMemory { firstSize = 1 } - firstIndex, err := backend.BuildIndex(context.Background(), ns, int64(firstSize), 100, nil, indexTestDocs(ns, firstSize)) + firstIndex, err := backend.BuildIndex(context.Background(), ns, int64(firstSize), 100, nil, "test", indexTestDocs(ns, firstSize)) require.NoError(t, err) openInMemoryIndexes := 0 @@ -952,7 +952,7 @@ func TestRebuildingIndexClosesPreviousCachedIndex(t *testing.T) { secondSize = 1 openInMemoryIndexes = 1 } - secondIndex, err := backend.BuildIndex(context.Background(), ns, int64(secondSize), 100, nil, indexTestDocs(ns, secondSize)) + secondIndex, err := backend.BuildIndex(context.Background(), ns, int64(secondSize), 100, nil, "test", indexTestDocs(ns, secondSize)) require.NoError(t, err) // Verify that first and second index are different, and first one is now closed. @@ -1050,12 +1050,12 @@ func testBleveIndexWithFailures(t *testing.T, fileBased bool) { // size=100 is above FileThreshold (5), make it a file-based index. size = 100 } - _, err := backend.BuildIndex(context.Background(), ns, size, 100, nil, func(index resource.ResourceIndex) (int64, error) { + _, err := backend.BuildIndex(context.Background(), ns, size, 100, nil, "test", func(index resource.ResourceIndex) (int64, error) { return 0, fmt.Errorf("fail") }) require.Error(t, err) // Even though previous build of the index failed, new building of the index should work. - _, err = backend.BuildIndex(context.Background(), ns, size, 100, nil, indexTestDocs(ns, int(size))) + _, err = backend.BuildIndex(context.Background(), ns, size, 100, nil, "test", indexTestDocs(ns, int(size))) require.NoError(t, err) } diff --git a/pkg/storage/unified/testing/benchmark.go b/pkg/storage/unified/testing/benchmark.go index 8503b1dd2bd..dcdf113c212 100644 --- a/pkg/storage/unified/testing/benchmark.go +++ b/pkg/storage/unified/testing/benchmark.go @@ -215,7 +215,7 @@ func runSearchBackendBenchmarkWriteThroughput(ctx context.Context, backend resou // Build initial index size := int64(10000) // force the index to be on disk - index, err := backend.BuildIndex(ctx, nr, size, 0, nil, func(index resource.ResourceIndex) (int64, error) { + index, err := backend.BuildIndex(ctx, nr, size, 0, nil, "benchmark", func(index resource.ResourceIndex) (int64, error) { return 0, nil }) if err != nil { diff --git a/pkg/storage/unified/testing/search_backend.go b/pkg/storage/unified/testing/search_backend.go index 57d887682c6..52af572b01a 100644 --- a/pkg/storage/unified/testing/search_backend.go +++ b/pkg/storage/unified/testing/search_backend.go @@ -64,7 +64,7 @@ func runTestSearchBackendBuildIndex(t *testing.T, backend resource.SearchBackend require.Nil(t, index) // Build the index - index, err = backend.BuildIndex(ctx, ns, 0, 0, nil, func(index resource.ResourceIndex) (int64, error) { + index, err = backend.BuildIndex(ctx, ns, 0, 0, nil, "test", func(index resource.ResourceIndex) (int64, error) { // Write a test document err := index.BulkIndex(&resource.BulkIndexRequest{ Items: []*resource.BulkIndexItem{ @@ -111,7 +111,7 @@ func runTestResourceIndex(t *testing.T, backend resource.SearchBackend, nsPrefix } // Build initial index with some test documents - index, err := backend.BuildIndex(ctx, ns, 3, 0, nil, func(index resource.ResourceIndex) (int64, error) { + index, err := backend.BuildIndex(ctx, ns, 3, 0, nil, "test", func(index resource.ResourceIndex) (int64, error) { err := index.BulkIndex(&resource.BulkIndexRequest{ Items: []*resource.BulkIndexItem{ { @@ -235,7 +235,7 @@ func runTestResourceIndex(t *testing.T, backend resource.SearchBackend, nsPrefix t.Run("Search by LibraryPanel reference", func(t *testing.T) { // Build index with dashboards that have LibraryPanel references - index, err := backend.BuildIndex(ctx, ns, 3, 0, nil, func(index resource.ResourceIndex) (int64, error) { + index, err := backend.BuildIndex(ctx, ns, 3, 0, nil, "test", func(index resource.ResourceIndex) (int64, error) { err := index.BulkIndex(&resource.BulkIndexRequest{ Items: []*resource.BulkIndexItem{ { From 4fae330f9be2d4de1d0fbcc3fd7420c15baea0c6 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 30 Jul 2025 15:36:57 +0100 Subject: [PATCH 056/126] Chore: update nginx config to match hosted-grafana (#108898) update nginx config to match hosted-grafana --- devenv/frontend-service/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devenv/frontend-service/nginx.conf b/devenv/frontend-service/nginx.conf index 26d6c1f9968..1cf21295a36 100644 --- a/devenv/frontend-service/nginx.conf +++ b/devenv/frontend-service/nginx.conf @@ -60,7 +60,7 @@ server { # API calls go to the backend # Cheat with app plugin paths and route them to the backend. These should come from # the Plugin CDN - location ~ ^/(api|apis|bootdata|logout|public\/plugins\/grafana\-\w+\-app) { + location ~ ^/(api|apis|avatar|bootdata|render|logout|public\/plugins) { if ($cookie_fs_unavailable) { add_header Content-Type application/json always; return 503 '{"code":"Loading", "message": "Soon!"}'; From 9ff44d17f268f950a24f8e8be4214656975d7083 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:52:56 +0100 Subject: [PATCH 057/126] Update dependency @floating-ui/react to v0.27.15 (#108922) 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 | 44 ++++++++++++------------ 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 6abb79b38ea..ac882e05234 100644 --- a/package.json +++ b/package.json @@ -268,7 +268,7 @@ "@emotion/css": "11.13.5", "@emotion/react": "11.14.0", "@fingerprintjs/fingerprintjs": "^3.4.2", - "@floating-ui/react": "0.27.14", + "@floating-ui/react": "0.27.15", "@formatjs/intl-durationformat": "^0.7.0", "@glideapps/glide-data-grid": "^6.0.0", "@grafana/alerting": "workspace:*", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index efd2b239574..0e59a3b7760 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@emotion/css": "11.13.5", - "@floating-ui/react": "0.27.14", + "@floating-ui/react": "0.27.15", "@grafana/data": "12.2.0-pre", "@grafana/e2e-selectors": "12.2.0-pre", "@grafana/i18n": "12.2.0-pre", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index e1ef60e75b3..ff855abfffb 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -66,7 +66,7 @@ "@emotion/css": "11.13.5", "@emotion/react": "11.14.0", "@emotion/serialize": "1.3.3", - "@floating-ui/react": "0.27.14", + "@floating-ui/react": "0.27.15", "@grafana/data": "12.2.0-pre", "@grafana/e2e-selectors": "12.2.0-pre", "@grafana/faro-web-sdk": "^1.13.2", diff --git a/yarn.lock b/yarn.lock index c98bba2096b..6ad34a673f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2362,48 +2362,48 @@ __metadata: languageName: node linkType: hard -"@floating-ui/core@npm:^1.7.2": - version: 1.7.2 - resolution: "@floating-ui/core@npm:1.7.2" +"@floating-ui/core@npm:^1.7.3": + version: 1.7.3 + resolution: "@floating-ui/core@npm:1.7.3" dependencies: "@floating-ui/utils": "npm:^0.2.10" - checksum: 10/2885a4c824f8d148222714cf2650c29bf3b30b70f465b990734766aafb1366ba23b4a285918c4f3ee67161096e7b9e4fbe0b3ca31e0f78800338cb4e6292912d + checksum: 10/a8952ff2673ddf28f12feeb86d90c54949e45bcb1af5758b7672850ac0dadb36d4bd61aa45dad1b6a35ba40d4756d3573afac6610b90502639d7266b91e0864e languageName: node linkType: hard -"@floating-ui/dom@npm:^1.0.1, @floating-ui/dom@npm:^1.7.2": - version: 1.7.2 - resolution: "@floating-ui/dom@npm:1.7.2" +"@floating-ui/dom@npm:^1.0.1, @floating-ui/dom@npm:^1.7.3": + version: 1.7.3 + resolution: "@floating-ui/dom@npm:1.7.3" dependencies: - "@floating-ui/core": "npm:^1.7.2" + "@floating-ui/core": "npm:^1.7.3" "@floating-ui/utils": "npm:^0.2.10" - checksum: 10/2d581459973b66024c47f039cabdc059b39c72abca93a4a0a8110e0a90b79d3fb84e49099afb73e13b3e7f17096070422220d996b206a5f0120c92bd7c48b98b + checksum: 10/0e91b67df31d30247a9516216b2d610a0b52f572c11d60a9875c68e5d65db0fe8819d096b823f123b45ed3ac7ef38d7462e268bdfc8b338c511c724acf098e78 languageName: node linkType: hard -"@floating-ui/react-dom@npm:^2.1.2, @floating-ui/react-dom@npm:^2.1.4": - version: 2.1.4 - resolution: "@floating-ui/react-dom@npm:2.1.4" +"@floating-ui/react-dom@npm:^2.1.2, @floating-ui/react-dom@npm:^2.1.5": + version: 2.1.5 + resolution: "@floating-ui/react-dom@npm:2.1.5" dependencies: - "@floating-ui/dom": "npm:^1.7.2" + "@floating-ui/dom": "npm:^1.7.3" peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 10/8fad23bd7a94b50bbd715109a272528978511b793b8954098c8e99dd0b63557a8641b315ce09301ef014eada560fd8638e4dea4c21cf7ce8d93f0038a59cd442 + checksum: 10/45d7bf558cce2045f0a62454f7675e1d139e3e24fe5c14cf8e4f718c911cc378c687ad428c1c6f1b3a7925a3210f1a4a924980be29f90fbd4ee4d9a7d5bb2f7f languageName: node linkType: hard -"@floating-ui/react@npm:0.27.14": - version: 0.27.14 - resolution: "@floating-ui/react@npm:0.27.14" +"@floating-ui/react@npm:0.27.15": + version: 0.27.15 + resolution: "@floating-ui/react@npm:0.27.15" dependencies: - "@floating-ui/react-dom": "npm:^2.1.4" + "@floating-ui/react-dom": "npm:^2.1.5" "@floating-ui/utils": "npm:^0.2.10" tabbable: "npm:^6.0.0" peerDependencies: react: ">=17.0.0" react-dom: ">=17.0.0" - checksum: 10/5466938bdd0db8f7939a640da459bfe8417388d95b1547c69a236f87e4b218992f4a2d7cee14642a6b64c88fbb8df7fced46a01e41b7feb12785e4181498669b + checksum: 10/1b48c0956f04eb00e8d992ef27234fbf6c204ad5c266538109332a1b469ef1360b3bf98c6ebfb23b05aa29e8284a67bd01a26ba0484a9f34af4a087d67d65fcc languageName: node linkType: hard @@ -3491,7 +3491,7 @@ __metadata: resolution: "@grafana/prometheus@workspace:packages/grafana-prometheus" dependencies: "@emotion/css": "npm:11.13.5" - "@floating-ui/react": "npm:0.27.14" + "@floating-ui/react": "npm:0.27.15" "@grafana/data": "npm:12.2.0-pre" "@grafana/e2e-selectors": "npm:12.2.0-pre" "@grafana/i18n": "npm:12.2.0-pre" @@ -3740,7 +3740,7 @@ __metadata: "@emotion/react": "npm:11.14.0" "@emotion/serialize": "npm:1.3.3" "@faker-js/faker": "npm:^9.0.0" - "@floating-ui/react": "npm:0.27.14" + "@floating-ui/react": "npm:0.27.15" "@grafana/data": "npm:12.2.0-pre" "@grafana/e2e-selectors": "npm:12.2.0-pre" "@grafana/faro-web-sdk": "npm:^1.13.2" @@ -18162,7 +18162,7 @@ __metadata: "@emotion/eslint-plugin": "npm:11.12.0" "@emotion/react": "npm:11.14.0" "@fingerprintjs/fingerprintjs": "npm:^3.4.2" - "@floating-ui/react": "npm:0.27.14" + "@floating-ui/react": "npm:0.27.15" "@formatjs/intl-durationformat": "npm:^0.7.0" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/alerting": "workspace:*" From ae15ba562e71d52cfbd7879df9e43d044d580405 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:47:15 +0100 Subject: [PATCH 058/126] Update dependency @types/node to v22.17.0 (#108936) 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 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-test-utils/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/loki/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 | 50 +++++++++---------- 22 files changed, 46 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index ac882e05234..5d6fcd944e4 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,7 @@ "@types/lodash": "4.17.20", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.3.0", "@types/pluralize": "^0.0.33", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 99cf6ee0f52..10a5d2e046f 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -88,7 +88,7 @@ "@rollup/plugin-node-resolve": "16.0.1", "@types/history": "4.7.11", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/papaparse": "5.3.16", "@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 d647d0fb88d..2e5dd9f65a4 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.1", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/semver": "7.7.0", "esbuild": "0.25.8", "rimraf": "6.0.1", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index f8597f17c32..cfb6e77e230 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.20", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/react": "18.3.18", "@types/react-virtualized-auto-sizer": "1.0.8", "@types/tinycolor2": "1.4.6", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 1d56eb47a0e..87b73a02a08 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.3.0", "@testing-library/user-event": "14.6.1", "@types/jest": "^29.5.4", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/react": "18.3.18", "@types/systemjs": "6.15.3", "jest": "^29.6.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 0e59a3b7760..b75ebc36f99 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -87,7 +87,7 @@ "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", "esbuild": "0.25.8", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 574d940a658..a3e9c784261 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -43,7 +43,7 @@ "@testing-library/user-event": "14.6.1", "@types/jest": "^29.5.4", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-virtualized-auto-sizer": "1.0.8", diff --git a/packages/grafana-test-utils/package.json b/packages/grafana-test-utils/package.json index 9a9f5998621..c20fad715d2 100644 --- a/packages/grafana-test-utils/package.json +++ b/packages/grafana-test-utils/package.json @@ -60,7 +60,7 @@ "@swc/core": "1.10.12", "@swc/jest": "^0.2.26", "@types/jest": "29.5.14", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "jest": "29.7.0", "typescript": "5.8.3" } diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index ff855abfffb..48bd1e11c66 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -166,7 +166,7 @@ "@types/is-hotkey": "0.1.10", "@types/jest": "29.5.14", "@types/mock-raf": "1.0.6", - "@types/node": "22.16.5", + "@types/node": "22.17.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 e0828ae1816..a7b771654d3 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -34,7 +34,7 @@ "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.17.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 c27670248a3..6063cb8f0a1 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.20", - "@types/node": "22.16.5", + "@types/node": "22.17.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 07af56ca8d6..bb91cdc6cc6 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.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/react": "18.3.18", "jest": "29.7.0", "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 595dbef2981..5ed1bd369a5 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.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.17.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 5118426ef3b..d8a2c573280 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.20", - "@types/node": "22.16.5", + "@types/node": "22.17.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 34998789a2f..b4c8fefdc08 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.20", "@types/logfmt": "^1.2.3", - "@types/node": "22.16.5", + "@types/node": "22.17.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/loki/package.json b/public/app/plugins/datasource/loki/package.json index 096eedc45f7..69bf66c12a9 100644 --- a/public/app/plugins/datasource/loki/package.json +++ b/public/app/plugins/datasource/loki/package.json @@ -33,7 +33,7 @@ "@types/d3-random": "^3.0.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/uuid": "10.0.0", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index f6701f152b0..46084541319 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -24,7 +24,7 @@ "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/react": "18.3.18", "i18next-parser": "9.3.0", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index a54da9e6b23..cb07deb595a 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.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/react": "18.3.18", "jest": "29.7.0", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index d411208a322..999694c2dac 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.3.0", "@testing-library/user-event": "14.6.1", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "jest": "29.7.0", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 115ba26cad4..e889d8c30df 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -45,7 +45,7 @@ "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.17.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 126bcc8d923..4e1a7530854 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.3.0", "@types/jest": "29.5.14", "@types/lodash": "4.17.20", - "@types/node": "22.16.5", + "@types/node": "22.17.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "jest": "29.7.0", diff --git a/yarn.lock b/yarn.lock index 6ad34a673f8..a4a40fb40e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2562,7 +2562,7 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2606,7 +2606,7 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/react": "npm:18.3.18" jest: "npm:29.7.0" lodash: "npm:4.17.21" @@ -2637,7 +2637,7 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2679,7 +2679,7 @@ __metadata: "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/uuid": "npm:10.0.0" @@ -2721,7 +2721,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" "@types/logfmt": "npm:^1.2.3" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/react-window": "npm:1.8.8" @@ -2766,7 +2766,7 @@ __metadata: "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/uuid": "npm:10.0.0" @@ -2806,7 +2806,7 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/react": "npm:18.3.18" i18next-parser: "npm:9.3.0" lodash: "npm:4.17.21" @@ -2838,7 +2838,7 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/react": "npm:18.3.18" jest: "npm:29.7.0" lodash: "npm:4.17.21" @@ -2867,7 +2867,7 @@ __metadata: "@testing-library/react": "npm:16.3.0" "@testing-library/user-event": "npm:14.6.1" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" jest: "npm:29.7.0" @@ -2906,7 +2906,7 @@ __metadata: "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2959,7 +2959,7 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3010,7 +3010,7 @@ __metadata: "@testing-library/react": "npm:16.3.0" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" jest: "npm:29.7.0" @@ -3121,7 +3121,7 @@ __metadata: "@types/d3-interpolate": "npm:^3.0.0" "@types/history": "npm:4.7.11" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/papaparse": "npm:5.3.16" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3168,7 +3168,7 @@ __metadata: dependencies: "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:16.0.1" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/semver": "npm:7.7.0" esbuild: "npm:0.25.8" rimraf: "npm:6.0.1" @@ -3275,7 +3275,7 @@ __metadata: "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/react": "npm:18.3.18" "@types/react-virtualized-auto-sizer": "npm:1.0.8" "@types/tinycolor2": "npm:1.4.6" @@ -3397,7 +3397,7 @@ __metadata: "@testing-library/react": "npm:16.3.0" "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:^29.5.4" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/react": "npm:18.3.18" "@types/systemjs": "npm:6.15.3" jest: "npm:^29.6.4" @@ -3517,7 +3517,7 @@ __metadata: "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -3682,7 +3682,7 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.20" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/react-virtualized-auto-sizer": "npm:1.0.8" @@ -3717,7 +3717,7 @@ __metadata: "@swc/core": "npm:1.10.12" "@swc/jest": "npm:^0.2.26" "@types/jest": "npm:29.5.14" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" jest: "npm:29.7.0" msw: "npm:2.10.4" typescript: "npm:5.8.3" @@ -3786,7 +3786,7 @@ __metadata: "@types/jquery": "npm:3.5.32" "@types/lodash": "npm:4.17.20" "@types/mock-raf": "npm:1.0.6" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-color": "npm:3.0.13" @@ -9817,12 +9817,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:22.16.5, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4": - version: 22.16.5 - resolution: "@types/node@npm:22.16.5" +"@types/node@npm:*, @types/node@npm:22.17.0, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4": + version: 22.17.0 + resolution: "@types/node@npm:22.17.0" dependencies: undici-types: "npm:~6.21.0" - checksum: 10/ba45b5c9113cbc5edb12960fcfe7e80db2c998af5c1931264240695b27d756570d92462150b95781bd67a03aa82111cc970ab0f4504eb99213edff8bf425354e + checksum: 10/f77b0e1c3c00e438b56c726d6b1170d4969c600cc8d4ecf2c2aa7692243a8ff455a3d530760da95e0b6aab059c4605a384b43d18f96646c745ff133c00b84875 languageName: node linkType: hard @@ -18257,7 +18257,7 @@ __metadata: "@types/lodash": "npm:4.17.20" "@types/logfmt": "npm:^1.2.3" "@types/lucene": "npm:^2" - "@types/node": "npm:22.16.5" + "@types/node": "npm:22.17.0" "@types/node-forge": "npm:^1" "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.3.0" "@types/pluralize": "npm:^0.0.33" From af6ac6f1d61307f5de5728fc8822217f6ddd8edd Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Wed, 30 Jul 2025 08:59:39 -0700 Subject: [PATCH 059/126] Sharing: Export dashboard as image match user's window width (#108827) --- .../dashboard-export-image.spec.ts | 52 +++++++++++++++++++ .../sharing/ExportButton/ExportMenu.tsx | 1 + .../sharing/ExportButton/utils.test.ts | 50 +++++++++++++++++- .../sharing/ExportButton/utils.ts | 4 +- 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts diff --git a/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts b/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts new file mode 100644 index 00000000000..5a72d94f9d6 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'ZqZnVvFZz'; + +test.use({ + featureToggles: { + scenes: true, + newDashboardSharingComponent: true, + sharingDashboardImage: true, // Enable the export image feature + }, +}); + +test.describe( + 'Export as Image', + { + tag: ['@dashboards'], + }, + () => { + test('Show renderer not available message when plugin not installed', async ({ + gotoDashboardPage, + page, + selectors, + }) => { + // Navigate to a dashboard + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + // Open the export dropdown + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.arrowMenu).click(); + + // Click export as image option + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.Menu.exportAsImage) + .click(); + + // Verify we're on the export image view + await expect(page).toHaveURL(/.*shareView=image/); + + // Verify the "renderer not available" alert is displayed + const rendererAlert = page.getByRole('status'); + await expect(rendererAlert).toBeVisible(); + await expect(rendererAlert).toContainText(/Image renderer plugin not installed/i); + await expect(rendererAlert).toContainText( + /To render an image, you must install the Grafana image renderer plugin/i + ); + + // Verify the generate button is NOT present when renderer is unavailable + await expect(page.getByRole('button', { name: /Generate image/i })).not.toBeVisible(); + }); + } +); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx index d5524108b07..c0099c17f2f 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx @@ -52,6 +52,7 @@ export default function ExportMenu({ dashboard }: { dashboard: DashboardScene }) menuItems.push({ shareId: shareDashboardType.image, + testId: newExportButtonSelector.exportAsImage, icon: 'camera', label: t('share-dashboard.menu.export-image-title', 'Export as image'), renderCondition: Boolean(config.featureToggles.sharingDashboardImage), diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/utils.test.ts b/public/app/features/dashboard-scene/sharing/ExportButton/utils.test.ts index 1c4ad371d0a..fa13471055a 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/utils.test.ts +++ b/public/app/features/dashboard-scene/sharing/ExportButton/utils.test.ts @@ -96,6 +96,13 @@ describe('Dashboard Export Image Utils', () => { const fetchMock = jest.fn().mockReturnValue(of({ ok: true, data: mockBlob })); (getBackendSrv as jest.Mock).mockReturnValue({ fetch: fetchMock }); + // Mock window.innerWidth + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 1280, + }); + const dashboard = { state: { uid: 'test-uid', @@ -119,7 +126,7 @@ describe('Dashboard Export Image Utils', () => { absolute: true, updateQuery: { height: -1, - width: 1000, + width: 1280, scale: 2, kiosk: true, hideNav: true, @@ -128,5 +135,46 @@ describe('Dashboard Export Image Utils', () => { }, }); }); + + it('should fallback to config width when window.innerWidth is not available', async () => { + config.rendererAvailable = true; + config.rendererDefaultImageWidth = 1500; + const mockBlob = new Blob(['test'], { type: 'image/png' }); + const fetchMock = jest.fn().mockReturnValue(of({ ok: true, data: mockBlob })); + (getBackendSrv as jest.Mock).mockReturnValue({ fetch: fetchMock }); + + // Ensure window.innerWidth is undefined + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: undefined, + }); + + const dashboard = { + state: { + uid: 'test-uid', + }, + } as DashboardScene; + + const result = await generateDashboardImage({ dashboard, scale: 1 }); + + expect(result.error).toBeUndefined(); + expect(result.blob).toBe(mockBlob); + expect(getDashboardUrl).toHaveBeenCalledWith({ + uid: 'test-uid', + currentQueryParams: '', + render: true, + absolute: true, + updateQuery: { + height: -1, + width: 1500, // Should use config value + scale: 1, + kiosk: true, + hideNav: true, + orgId: '1', + fullPageImage: true, + }, + }); + }); }); }); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/utils.ts b/public/app/features/dashboard-scene/sharing/ExportButton/utils.ts index 64d84893b45..f9fb9e5f04b 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/utils.ts +++ b/public/app/features/dashboard-scene/sharing/ExportButton/utils.ts @@ -28,7 +28,7 @@ export interface ImageGenerationResult { */ export async function generateDashboardImage({ dashboard, - scale = config.rendererDefaultImageScale || 1, + scale = config.rendererDefaultImageScale || 2, }: ImageGenerationOptions): Promise { try { // Check if renderer plugin is available @@ -46,7 +46,7 @@ export async function generateDashboardImage({ absolute: true, updateQuery: { height: -1, // image renderer will scroll through the dashboard and set the appropriate height - width: config.rendererDefaultImageWidth || 1000, + width: window.innerWidth || config.rendererDefaultImageWidth || 1000, scale, kiosk: true, hideNav: true, From c8c017a6f586e9fb98f56766b0b09b023ee284b0 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 30 Jul 2025 18:37:33 +0200 Subject: [PATCH 060/126] Chore: make update-workspace (#108942) --- apps/advisor/go.sum | 4 ++-- apps/investigations/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index b83026226ad..b52f4e5a7ab 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -673,8 +673,8 @@ github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAc github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0/go.mod h1:H9sVh9A4yg5egMGZeh0mifxT1Q/uqwKe1LBjBJU6pN8= github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho= github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250716132114-6fd75ebc5441 h1:+TSbaxCXBZrKkdROWBzdWna8uStE1f9LYd7GiqjVfz8= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250716132114-6fd75ebc5441/go.mod h1:1XWiRSVuDQiayapHhQiDc4S4e9GzEZgg/3GeNCuDgn4= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71 h1:7AVAgb0CalPYtUC9SJKYAkByOBGwL6r50P4QMEiJi44= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71/go.mod h1:/iuseD/cEpXDiy7MpL+4qBFZ3H6esnUJTYzpoJMw9dw= github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b h1:31MwoIKKT9Ay0ZjbT4lkfcPijiWogUWzXs2EjrCgodI= github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:dLtYBp1pza5HYalezNvzlP8JDeKrZ5BKTonDgEOE0NY= github.com/grafana/grafana/apps/secret v0.0.0-20250711114246-c9b2126c4ad5 h1:+fMhUoqwGdY8ntH0GL2icJa3uk+bTiIMicawDG2r9Uc= diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 88dfe75aef3..65357085011 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -351,8 +351,8 @@ github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAc github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0/go.mod h1:H9sVh9A4yg5egMGZeh0mifxT1Q/uqwKe1LBjBJU6pN8= github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho= github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250716132114-6fd75ebc5441 h1:+TSbaxCXBZrKkdROWBzdWna8uStE1f9LYd7GiqjVfz8= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250716132114-6fd75ebc5441/go.mod h1:1XWiRSVuDQiayapHhQiDc4S4e9GzEZgg/3GeNCuDgn4= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71 h1:7AVAgb0CalPYtUC9SJKYAkByOBGwL6r50P4QMEiJi44= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71/go.mod h1:/iuseD/cEpXDiy7MpL+4qBFZ3H6esnUJTYzpoJMw9dw= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250725152715-69d3b9023cec h1:cg1GbDVZ7goqDrqoMzqeN4AeAcD271MGYjOvdVTDwfw= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250725152715-69d3b9023cec/go.mod h1:3ZgUe0E3rIhI026xF4DKFptOst/jpDHJ/Sn+bRODzI4= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250627191313-2f1a6ae1712b h1:QyJLJn3xwFTIXu9KPZujsrIUN0X8DdiR9b2h75L0AfI= diff --git a/go.mod b/go.mod index dc72d4b349b..6d9b4e0d65b 100644 --- a/go.mod +++ b/go.mod @@ -230,7 +230,7 @@ require ( require ( github.com/grafana/grafana/apps/advisor v0.0.0-20250627191313-2f1a6ae1712b // @grafana/plugins-platform-backend github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250627191313-2f1a6ae1712b // @grafana/alerting-backend - github.com/grafana/grafana/apps/dashboard v0.0.0-20250716132114-6fd75ebc5441 // @grafana/grafana-app-platform-squad @grafana/dashboards-squad + github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71 // @grafana/grafana-app-platform-squad @grafana/dashboards-squad github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b // @grafana/grafana-search-and-storage github.com/grafana/grafana/apps/iam v0.0.0-20250627191313-2f1a6ae1712b // @grafana/identity-access-team github.com/grafana/grafana/apps/investigations v0.0.0-20250627191313-2f1a6ae1712b // @fcjack @matryer diff --git a/go.sum b/go.sum index 46799064e05..2009fce4070 100644 --- a/go.sum +++ b/go.sum @@ -1620,8 +1620,8 @@ github.com/grafana/grafana/apps/advisor v0.0.0-20250627191313-2f1a6ae1712b h1:8o github.com/grafana/grafana/apps/advisor v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:q+h3HbmqU/PposW6lq8cMle1v8vuyX1LCMrGzbabHxc= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250627191313-2f1a6ae1712b h1:jr+C3epmjhd5Yyob4P1Z/dPaW4LRTkU5UJLXsI4eaeM= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:WpI7TCck4P2wKTO2WJLBRcfOWvUGvTdxYu3QqS3z7jM= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250716132114-6fd75ebc5441 h1:+TSbaxCXBZrKkdROWBzdWna8uStE1f9LYd7GiqjVfz8= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250716132114-6fd75ebc5441/go.mod h1:1XWiRSVuDQiayapHhQiDc4S4e9GzEZgg/3GeNCuDgn4= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71 h1:7AVAgb0CalPYtUC9SJKYAkByOBGwL6r50P4QMEiJi44= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71/go.mod h1:/iuseD/cEpXDiy7MpL+4qBFZ3H6esnUJTYzpoJMw9dw= github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b h1:31MwoIKKT9Ay0ZjbT4lkfcPijiWogUWzXs2EjrCgodI= github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:dLtYBp1pza5HYalezNvzlP8JDeKrZ5BKTonDgEOE0NY= github.com/grafana/grafana/apps/iam v0.0.0-20250627191313-2f1a6ae1712b h1:NV8v9xdM/pzjjy+1cLqUseia3bYcvQGh88vZdMW/jA0= From 34019e5ec017df63124a7355a02b73cf880ad5e7 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Wed, 30 Jul 2025 11:46:19 -0500 Subject: [PATCH 061/126] Library Panels: Remove k8s connections feature flag (#108881) --- .../src/types/featureToggles.gen.ts | 4 -- .../dashboards/service/dashboard_service.go | 56 +++++++++---------- .../service/dashboard_service_test.go | 2 +- 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 ----- 7 files changed, 27 insertions(+), 60 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 138e05f187e..e45d19454a7 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -282,10 +282,6 @@ export interface FeatureToggles { */ kubernetesLibraryPanels?: boolean; /** - * Routes library panel connections requests from /api to using search - */ - kubernetesLibraryPanelConnections?: boolean; - /** * Use the kubernetes API in the frontend for dashboards */ kubernetesDashboards?: boolean; diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 043438db828..1eb611fa670 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -474,40 +474,36 @@ func (dr *DashboardServiceImpl) Count(ctx context.Context, scopeParams *quota.Sc } func (dr *DashboardServiceImpl) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*dashboards.DashboardRef, error) { - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesLibraryPanelConnections) { - res, err := dr.k8sclient.Search(ctx, orgID, &resourcepb.ResourceSearchRequest{ - Options: &resourcepb.ListOptions{ - Fields: []*resourcepb.Requirement{ - { - Key: search.DASHBOARD_LIBRARY_PANEL_REFERENCE, - Operator: string(selection.Equals), - Values: []string{libraryPanelUID}, - }, + res, err := dr.k8sclient.Search(ctx, orgID, &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Fields: []*resourcepb.Requirement{ + { + Key: search.DASHBOARD_LIBRARY_PANEL_REFERENCE, + Operator: string(selection.Equals), + Values: []string{libraryPanelUID}, }, }, - Limit: listAllDashboardsLimit, - }) - if err != nil { - return nil, err - } - - results, err := dashboardsearch.ParseResults(res, 0) - if err != nil { - return nil, err - } - - dashes := make([]*dashboards.DashboardRef, 0, len(results.Hits)) - for _, row := range results.Hits { - dashes = append(dashes, &dashboards.DashboardRef{ - UID: row.Name, - FolderUID: row.Folder, - ID: row.Field.GetNestedInt64(resource.SEARCH_FIELD_LEGACY_ID), // nolint:staticcheck - }) - } - return dashes, nil + }, + Limit: listAllDashboardsLimit, + }) + if err != nil { + return nil, err } - return dr.dashboardStore.GetDashboardsByLibraryPanelUID(ctx, libraryPanelUID, orgID) + results, err := dashboardsearch.ParseResults(res, 0) + if err != nil { + return nil, err + } + + dashes := make([]*dashboards.DashboardRef, 0, len(results.Hits)) + for _, row := range results.Hits { + dashes = append(dashes, &dashboards.DashboardRef{ + UID: row.Name, + FolderUID: row.Folder, + ID: row.Field.GetNestedInt64(resource.SEARCH_FIELD_LEGACY_ID), // nolint:staticcheck + }) + } + return dashes, nil } func (dr *DashboardServiceImpl) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) { diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 95e5a0a9023..1cda81c5288 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -2493,7 +2493,7 @@ func TestGetDashboardsByLibraryPanelUID(t *testing.T) { dashboardStore: &fakeStore, folderService: folderSvc, ac: actest.FakeAccessControl{ExpectedEvaluate: true}, - features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesLibraryPanelConnections), + features: featuremgmt.WithFeatures(), publicDashboardService: fakePublicDashboardService, k8sclient: k8sCliMock, } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f50b7854ed6..07bc3cdcc6e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -469,13 +469,6 @@ var ( Owner: grafanaAppPlatformSquad, RequiresRestart: true, // changes the API routing }, - { - Name: "kubernetesLibraryPanelConnections", - Description: "Routes library panel connections requests from /api to using search", - Stage: FeatureStageExperimental, - Owner: grafanaAppPlatformSquad, - RequiresRestart: true, // changes the API routing - }, { Name: "kubernetesDashboards", Description: "Use the kubernetes API in the frontend for dashboards", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 6c21f852785..67f899018d8 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -60,7 +60,6 @@ disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad formatString,GA,@grafana/dataviz-squad,false,false,true kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesLibraryPanels,experimental,@grafana/grafana-app-platform-squad,false,true,false -kubernetesLibraryPanelConnections,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false dashboardDisableSchemaValidationV2,experimental,@grafana/grafana-app-platform-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 52552a0b8d9..236516152f5 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -251,10 +251,6 @@ const ( // Routes library panel requests from /api to the /apis endpoint FlagKubernetesLibraryPanels = "kubernetesLibraryPanels" - // FlagKubernetesLibraryPanelConnections - // Routes library panel connections requests from /api to using search - FlagKubernetesLibraryPanelConnections = "kubernetesLibraryPanelConnections" - // FlagKubernetesDashboards // Use the kubernetes API in the frontend for dashboards FlagKubernetesDashboards = "kubernetesDashboards" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 980f805d56d..2ee75811760 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1756,19 +1756,6 @@ "hideFromAdminPage": true } }, - { - "metadata": { - "name": "kubernetesLibraryPanelConnections", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-07-25T13:06:00Z" - }, - "spec": { - "description": "Routes library panel connections requests from /api to using search", - "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresRestart": true - } - }, { "metadata": { "name": "kubernetesLibraryPanels", From 60ce4ad1fa62c6acbb6cda08bc7a738998bb9778 Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Wed, 30 Jul 2025 13:46:38 -0400 Subject: [PATCH 062/126] Ds-querier: add slug to logs (#108822) --- pkg/registry/apis/query/query.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index 66a1db38eae..6e73e9dd63d 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -108,6 +108,7 @@ func (r *queryREST) NewConnectOptions() (runtime.Object, bool, string) { return nil, false, "" // true means you can use the trailing path as a variable } +// called by mt query service and also when queryServiceFromUI is enabled, can be both mt and st func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.Object, incomingResponder rest.Responder) (http.Handler, error) { // See: /pkg/services/apiserver/builder/helper.go#L34 // The name is set with a rewriter hack @@ -175,7 +176,7 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O return } - qdr, err := handleQuery(ctx, *raw, *b, httpreq, *responder) + qdr, err := handleQuery(ctx, *raw, *b, httpreq, *responder, connectLogger) if err != nil { b.log.Error("execute error", "http code", query.GetResponseCode(qdr), "err", err) @@ -199,7 +200,7 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O }), nil } -func handleQuery(ctx context.Context, raw query.QueryDataRequest, b QueryAPIBuilder, httpreq *http.Request, responder responderWrapper) (*backend.QueryDataResponse, error) { +func handleQuery(ctx context.Context, raw query.QueryDataRequest, b QueryAPIBuilder, httpreq *http.Request, responder responderWrapper, connectLogger log.Logger) (*backend.QueryDataResponse, error) { var jsonQueries = make([]*simplejson.Json, 0, len(raw.Queries)) for _, query := range raw.Queries { jsonBytes, err := json.Marshal(query) @@ -228,17 +229,19 @@ func handleQuery(ctx context.Context, raw query.QueryDataRequest, b QueryAPIBuil instanceConfig, err := b.clientSupplier.GetInstanceConfigurationSettings(ctx) if err != nil { - b.log.Error("failed to get instance configuration settings", "err", err) + connectLogger.Error("failed to get instance configuration settings", "err", err) responder.Error(err) return nil, err } + dsQuerierLoggerWithSlug := connectLogger.New("slug", instanceConfig.Options["slug"]) + mtDsClientBuilder := mtdsclient.NewMtDatasourceClientBuilderWithClientSupplier( b.clientSupplier, ctx, headers, instanceConfig, - b.log, + dsQuerierLoggerWithSlug, ) exprService := expr.ProvideService( @@ -256,7 +259,7 @@ func handleQuery(ctx context.Context, raw query.QueryDataRequest, b QueryAPIBuil mtDsClientBuilder, ) - qdr, err := service.QueryData(ctx, b.log, cache, exprService, mReq, mtDsClientBuilder, headers) + qdr, err := service.QueryData(ctx, dsQuerierLoggerWithSlug, cache, exprService, mReq, mtDsClientBuilder, headers) if err != nil { return qdr, err From 68ed7c129d23c94e6dcea86aea686c41677fcef7 Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Wed, 30 Jul 2025 15:12:35 -0300 Subject: [PATCH 063/126] QueryLibrary: Add `CoreApp` for `QueryEditor` rendering (#108867) --- .../panel-edit/PanelDataPane/PanelDataQueriesTab.tsx | 5 ++++- .../explore/QueryLibrary/QueryLibraryContext.tsx | 9 ++++++--- public/app/features/explore/QueryLibrary/mocks.tsx | 1 + .../explore/RichHistory/RichHistoryAddToLibrary.tsx | 2 +- public/app/features/explore/SecondaryActions.tsx | 8 ++++++-- .../app/features/query/components/QueryEditorRow.tsx | 11 +++++++---- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index ce2784758be..668b8612851 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -376,6 +376,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps @@ -393,7 +394,9 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps - openQueryLibraryDrawer(getDatasourceNames(datasource, queries), onSelectQueryFromLibrary) + openQueryLibraryDrawer(getDatasourceNames(datasource, queries), onSelectQueryFromLibrary, { + context: CoreApp.PanelEditor, + }) } variant="secondary" data-testid={selectors.components.QueryTab.addQueryFromLibrary} diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx index 90640203d48..9037182c156 100644 --- a/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx +++ b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx @@ -1,5 +1,6 @@ import { createContext, ReactNode, useContext } from 'react'; +import { CoreApp } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; import { OnSelectQueryType } from './types'; @@ -17,7 +18,7 @@ export type QueryLibraryContextType = { * @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. - * @param options.context Used for tracking. Should identify the context this is called from, like 'explore' or + * @param options.context Used for QueryEditor. Should identify the context this is called from, like 'explore' or * 'dashboard'. */ openDrawer: ( @@ -32,7 +33,7 @@ export type QueryLibraryContextType = { * Opens a modal for adding a query to the library. * @param query Query to be saved * @param options.onSave Callback that will be called after the query is saved. - * @param options.context Used for tracking. Should identify the context this is called from, like 'explore' or + * @param options.context Used for rendering QueryEditor. Should identify the context this is called from, like 'explore' or * 'dashboard'. * @param options.title Default title for the modal, can be overridden by the query title. */ @@ -46,8 +47,9 @@ export type QueryLibraryContextType = { * Returns a predefined small button that can be used to save a query to the library. * @param query */ - renderSaveQueryButton: (query: DataQuery) => ReactNode; + renderSaveQueryButton: (query: DataQuery, app?: CoreApp) => ReactNode; queryLibraryEnabled: boolean; + context: string; }; export const QueryLibraryContext = createContext({ @@ -63,6 +65,7 @@ export const QueryLibraryContext = createContext({ }, queryLibraryEnabled: false, + context: 'unknown', }); export function useQueryLibraryContext() { diff --git a/public/app/features/explore/QueryLibrary/mocks.tsx b/public/app/features/explore/QueryLibrary/mocks.tsx index 9126fdabe42..e59732870a9 100644 --- a/public/app/features/explore/QueryLibrary/mocks.tsx +++ b/public/app/features/explore/QueryLibrary/mocks.tsx @@ -17,6 +17,7 @@ export function QueryLibraryContextProviderMock(props: PropsWithChildren) closeAddQueryModal: jest.fn(), renderSaveQueryButton: jest.fn(), queryLibraryEnabled: Boolean(props.queryLibraryEnabled), + context: 'explore', }} > {props.children} diff --git a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx index 6c70ac1bf89..e99c0e6cebc 100644 --- a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx @@ -22,7 +22,7 @@ export const RichHistoryAddToLibrary = ({ query }: Props) => { variant="secondary" aria-label={buttonLabel} onClick={() => { - openAddQueryModal(query, { onSave: () => setHasBeenSaved(true), context: 'richHistory' }); + openAddQueryModal(query, { onSave: () => setHasBeenSaved(true), context: 'rich-history' }); }} > {buttonLabel} diff --git a/public/app/features/explore/SecondaryActions.tsx b/public/app/features/explore/SecondaryActions.tsx index cd9d25b860b..b5b30aaae1d 100644 --- a/public/app/features/explore/SecondaryActions.tsx +++ b/public/app/features/explore/SecondaryActions.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; -import { GrafanaTheme2 } from '@grafana/data'; +import { CoreApp, GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { ToolbarButton, useTheme2 } from '@grafana/ui'; @@ -76,7 +76,11 @@ export function SecondaryActions({ data-testid={selectors.pages.Explore.General.addFromQueryLibrary} aria-label={t('explore.secondary-actions.add-from-query-library', 'Add query from library')} variant="canvas" - onClick={() => openQueryLibraryDrawer(activeDatasources, onSelectQueryFromLibrary)} + onClick={() => + openQueryLibraryDrawer(activeDatasources, onSelectQueryFromLibrary, { + context: CoreApp.Explore, + }) + } icon="plus" > Add query from library diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 352451481d0..b711b76d8c8 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -380,7 +380,7 @@ export class QueryEditorRow extends PureComponent )} {this.renderExtraActions()} - + extends PureComponent {!hideHideQueryButton ? ( { datasourceFilters: string[]; onSelectQuery: (query: DataQuery) => void; + app?: CoreApp; } function ReplaceQueryFromLibrary({ datasourceFilters, onSelectQuery, + app, }: ReplaceQueryFromLibraryProps) { const { openDrawer, queryLibraryEnabled } = useQueryLibraryContext(); const onReplaceQueryFromLibrary = () => { - openDrawer(datasourceFilters, onSelectQuery, { isReplacingQuery: true }); + openDrawer(datasourceFilters, onSelectQuery, { isReplacingQuery: true, context: app }); }; return queryLibraryEnabled ? ( From 8fb1483a9f8dd8126d129418ac87b3aba11ee6ef Mon Sep 17 00:00:00 2001 From: Adam Yeats <16296989+adamyeats@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:12:59 +0100 Subject: [PATCH 064/126] Elasticsearch: Remove link to Elasticsearch Templated Dashboard from docs (#108555) Remove link to Elasticsearch Templated Dashboard from docs --- .../datasources/elasticsearch/template-variables/index.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/sources/datasources/elasticsearch/template-variables/index.md b/docs/sources/datasources/elasticsearch/template-variables/index.md index b27861f0796..66ca17a93bd 100644 --- a/docs/sources/datasources/elasticsearch/template-variables/index.md +++ b/docs/sources/datasources/elasticsearch/template-variables/index.md @@ -86,8 +86,6 @@ You can alternatively use other sorting criteria, such as **Alphabetical**, to r In the above example, a Lucene query filters documents based on the `hostname` property using a variable named `$hostname`. The example also uses a variable in the _Terms_ group by field input box, which you can use to quickly change how data is grouped. -To view an example dashboard on Grafana Play, see the [Elasticsearch Templated Dashboard](https://play.grafana.org/d/z8OZC66nk/elasticsearch-8-2-0-sample-flight-data?orgId=1). - ## Create a query Write the query using a custom JSON string, with the field mapped as a [keyword](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword) in the Elasticsearch index mapping. From 6bf542889afb8e5e63bb09a82175782ddbf5dabc Mon Sep 17 00:00:00 2001 From: "lean.dev" <34773040+leandro-deveikis@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:00:30 +0100 Subject: [PATCH 065/126] SecretsManager: Refactor and clean metrics (#108908) --- pkg/registry/apis/secret/contracts/decrypt.go | 2 +- .../apis/secret/service/metrics/metrics.go | 128 ++++++++++++++++ .../apis/secret/service/secure_value.go | 143 ++++++++++++++++-- .../apis/secret/testutils/testutils.go | 2 +- pkg/server/wire_gen.go | 4 +- .../secret/metadata/decrypt_store_test.go | 9 +- .../secret/metadata/metrics/metrics.go | 46 +----- .../secret/metadata/secure_value_store.go | 49 ++++-- 8 files changed, 317 insertions(+), 66 deletions(-) create mode 100644 pkg/registry/apis/secret/service/metrics/metrics.go diff --git a/pkg/registry/apis/secret/contracts/decrypt.go b/pkg/registry/apis/secret/contracts/decrypt.go index 32f9f7f6c1d..2c5403c790d 100644 --- a/pkg/registry/apis/secret/contracts/decrypt.go +++ b/pkg/registry/apis/secret/contracts/decrypt.go @@ -27,7 +27,7 @@ type DecryptAuthorizer interface { Authorize(ctx context.Context, secureValueName string, secureValueDecrypters []string) (identity string, allowed bool) } -// DecryptService is the inferface for the decrypt service. +// DecryptService is the interface for the decrypt service. type DecryptService interface { Decrypt(ctx context.Context, namespace string, names ...string) (map[string]DecryptResult, error) Close() error diff --git a/pkg/registry/apis/secret/service/metrics/metrics.go b/pkg/registry/apis/secret/service/metrics/metrics.go new file mode 100644 index 00000000000..b179ead20b7 --- /dev/null +++ b/pkg/registry/apis/secret/service/metrics/metrics.go @@ -0,0 +1,128 @@ +package metrics + +import ( + "sync" + + "github.com/prometheus/client_golang/prometheus" +) + +const ( + namespace = "grafana_secrets_manager" + subsystem = "service" +) + +// SecureValueServiceMetrics is a struct that contains all the metrics for SecureValue. +type SecureValueServiceMetrics struct { + SecureValueCreateDuration *prometheus.HistogramVec + SecureValueCreateCount *prometheus.CounterVec + SecureValueUpdateDuration *prometheus.HistogramVec + SecureValueUpdateCount *prometheus.CounterVec + SecureValueReadDuration *prometheus.HistogramVec + SecureValueReadCount *prometheus.CounterVec + SecureValueListDuration *prometheus.HistogramVec + SecureValueListCount *prometheus.CounterVec + SecureValueDeleteDuration *prometheus.HistogramVec + SecureValueDeleteCount *prometheus.CounterVec +} + +func newSecureValueServiceMetrics() *SecureValueServiceMetrics { + return &SecureValueServiceMetrics{ + SecureValueCreateDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "secure_value_create_duration_seconds", + Help: "Duration of Secure Value create operations", + Buckets: prometheus.DefBuckets, + }, []string{"success"}), + SecureValueCreateCount: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "secure_value_create_count", + Help: "Count of Secure Value create operations", + }, []string{"success"}), + SecureValueReadDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "secure_value_read_duration_seconds", + Help: "Duration of Secure Value read operations", + Buckets: prometheus.DefBuckets, + }, []string{"success"}), + SecureValueReadCount: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "secure_value_read_count", + Help: "Count of Secure Value read operations", + }, []string{"success"}), + SecureValueUpdateDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "secure_value_update_duration_seconds", + Help: "Duration of Secure Value update operations", + Buckets: prometheus.DefBuckets, + }, []string{"success"}), + SecureValueUpdateCount: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "secure_value_update_count", + Help: "Count of Secure Value update operations", + }, []string{"success"}), + SecureValueListDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "secure_value_list_duration_seconds", + Help: "Duration of Secure Value list operations", + Buckets: prometheus.DefBuckets, + }, []string{"success"}), + SecureValueListCount: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "secure_value_list_count", + Help: "Count of Secure Value list operations", + }, []string{"success"}), + SecureValueDeleteDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "secure_value_delete_duration_seconds", + Help: "Duration of Secure Value delete operations", + Buckets: prometheus.DefBuckets, + }, []string{"success"}), + SecureValueDeleteCount: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "secure_value_delete_count", + Help: "Count of Secure Value delete operations", + }, []string{"success"}), + } +} + +var ( + initOnce sync.Once + metricsInstance *SecureValueServiceMetrics +) + +func NewSecureValueServiceMetrics(reg prometheus.Registerer) *SecureValueServiceMetrics { + initOnce.Do(func() { + m := newSecureValueServiceMetrics() + + if reg != nil { + reg.MustRegister( + m.SecureValueCreateDuration, + m.SecureValueCreateCount, + m.SecureValueReadDuration, + m.SecureValueReadCount, + m.SecureValueUpdateDuration, + m.SecureValueUpdateCount, + m.SecureValueListDuration, + m.SecureValueListCount, + m.SecureValueDeleteDuration, + m.SecureValueDeleteCount, + ) + } + metricsInstance = m + }) + return metricsInstance +} + +func NewTestMetrics() *SecureValueServiceMetrics { + return newSecureValueServiceMetrics() +} diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index f9bafad9467..fe32208562d 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -3,6 +3,8 @@ package service import ( "context" "fmt" + "strconv" + "time" claims "github.com/grafana/authlib/types" "go.opentelemetry.io/otel/attribute" @@ -12,9 +14,14 @@ import ( secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/service/metrics" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/codes" ) +var _ contracts.SecureValueService = (*SecureValueService)(nil) + type SecureValueService struct { tracer trace.Tracer accessClient claims.AccessClient @@ -22,6 +29,7 @@ type SecureValueService struct { secureValueMetadataStorage contracts.SecureValueMetadataStorage keeperMetadataStorage contracts.KeeperMetadataStorage keeperService contracts.KeeperService + metrics *metrics.SecureValueServiceMetrics } func ProvideSecureValueService( @@ -31,6 +39,7 @@ func ProvideSecureValueService( secureValueMetadataStorage contracts.SecureValueMetadataStorage, keeperMetadataStorage contracts.KeeperMetadataStorage, keeperService contracts.KeeperService, + reg prometheus.Registerer, ) contracts.SecureValueService { return &SecureValueService{ tracer: tracer, @@ -39,27 +48,77 @@ func ProvideSecureValueService( secureValueMetadataStorage: secureValueMetadataStorage, keeperMetadataStorage: keeperMetadataStorage, keeperService: keeperService, + metrics: metrics.NewSecureValueServiceMetrics(reg), } } -func (s *SecureValueService) Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) { +func (s *SecureValueService) Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (_ *secretv1beta1.SecureValue, createErr error) { + start := time.Now() + name, namespace := sv.GetName(), sv.GetNamespace() ctx, span := s.tracer.Start(ctx, "SecureValueService.Create", trace.WithAttributes( - attribute.String("name", sv.GetName()), - attribute.String("namespace", sv.GetNamespace()), + attribute.String("name", name), + attribute.String("namespace", namespace), attribute.String("actor", actorUID), )) defer span.End() + + defer func() { + args := []any{ + "name", name, + "namespace", namespace, + "actorUID", actorUID, + } + + success := createErr == nil + args = append(args, "success", success) + if !success { + span.SetStatus(codes.Error, "SecureValueService.Create failed") + span.RecordError(createErr) + args = append(args, "error", createErr) + } + + logging.FromContext(ctx).Info("SecureValueService.Create finished", args...) + + s.metrics.SecureValueCreateDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds()) + s.metrics.SecureValueCreateCount.WithLabelValues(strconv.FormatBool(success)).Inc() + }() + return s.createNewVersion(ctx, sv, actorUID) } -func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, bool, error) { +func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (_ *secretv1beta1.SecureValue, sync bool, updateErr error) { + start := time.Now() + name, namespace := newSecureValue.GetName(), newSecureValue.GetNamespace() + ctx, span := s.tracer.Start(ctx, "SecureValueService.Update", trace.WithAttributes( - attribute.String("name", newSecureValue.GetName()), - attribute.String("namespace", newSecureValue.GetNamespace()), + attribute.String("name", name), + attribute.String("namespace", namespace), attribute.String("actor", actorUID), )) defer span.End() + defer func() { + args := []any{ + "name", name, + "namespace", namespace, + "actorUID", actorUID, + "sync", sync, + } + + success := updateErr == nil + args = append(args, "success", success) + if !success { + span.SetStatus(codes.Error, "SecureValueService.Update failed") + span.RecordError(updateErr) + args = append(args, "error", updateErr) + } + + logging.FromContext(ctx).Info("SecureValueService.Update finished", args...) + + s.metrics.SecureValueUpdateDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds()) + s.metrics.SecureValueUpdateCount.WithLabelValues(strconv.FormatBool(success)).Inc() + }() + if newSecureValue.Spec.Value == nil { currentVersion, err := s.secureValueMetadataStorage.Read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{}) if err != nil { @@ -136,22 +195,66 @@ func (s *SecureValueService) createNewVersion(ctx context.Context, sv *secretv1b return createdSv, nil } -func (s *SecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) { +func (s *SecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (_ *secretv1beta1.SecureValue, readErr error) { + start := time.Now() + ctx, span := s.tracer.Start(ctx, "SecureValueService.Read", trace.WithAttributes( attribute.String("name", name), attribute.String("namespace", namespace.String()), )) + + defer func() { + args := []any{ + "name", name, + "namespace", namespace, + } + + success := readErr == nil + args = append(args, "success", success) + if !success { + span.SetStatus(codes.Error, "SecureValueService.Read failed") + span.RecordError(readErr) + args = append(args, "error", readErr) + } + + logging.FromContext(ctx).Info("SecureValueService.Read finished", args...) + + s.metrics.SecureValueReadDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds()) + s.metrics.SecureValueReadCount.WithLabelValues(strconv.FormatBool(success)).Inc() + }() + defer span.End() return s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: false}) } -func (s *SecureValueService) List(ctx context.Context, namespace xkube.Namespace) (*secretv1beta1.SecureValueList, error) { +func (s *SecureValueService) List(ctx context.Context, namespace xkube.Namespace) (_ *secretv1beta1.SecureValueList, listErr error) { + start := time.Now() + ctx, span := s.tracer.Start(ctx, "SecureValueService.List", trace.WithAttributes( attribute.String("namespace", namespace.String()), )) defer span.End() + defer func() { + args := []any{ + "namespace", namespace, + } + + success := listErr == nil + args = append(args, "success", success) + if !success { + span.SetStatus(codes.Error, "SecureValueService.List failed") + span.RecordError(listErr) + args = append(args, "error", listErr) + } + + logging.FromContext(ctx).Info("SecureValueService.List finished", args...) + + s.metrics.SecureValueListDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds()) + s.metrics.SecureValueListCount.WithLabelValues(strconv.FormatBool(success)).Inc() + }() + user, ok := claims.AuthInfoFrom(ctx) if !ok { return nil, fmt.Errorf("missing auth info in context") @@ -188,13 +291,35 @@ func (s *SecureValueService) List(ctx context.Context, namespace xkube.Namespace }, nil } -func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) { +func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespace, name string) (_ *secretv1beta1.SecureValue, deleteErr error) { + start := time.Now() + ctx, span := s.tracer.Start(ctx, "SecureValueService.Delete", trace.WithAttributes( attribute.String("name", name), attribute.String("namespace", namespace.String()), )) defer span.End() + defer func() { + args := []any{ + "name", name, + "namespace", namespace, + } + + success := deleteErr == nil + args = append(args, "success", success) + if !success { + span.SetStatus(codes.Error, "SecureValueService.Delete failed") + span.RecordError(deleteErr) + args = append(args, "error", deleteErr) + } + + logging.FromContext(ctx).Info("SecureValueService.Delete finished", args...) + + s.metrics.SecureValueDeleteDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds()) + s.metrics.SecureValueDeleteCount.WithLabelValues(strconv.FormatBool(success)).Inc() + }() + // TODO: does this need to be for update? sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: true}) if err != nil { diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index f2d24b05905..8b6765ba642 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -120,7 +120,7 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { keeperService = setupCfg.KeeperService } - secureValueService := service.ProvideSecureValueService(tracer, accessClient, database, secureValueMetadataStorage, keeperMetadataStorage, keeperService) + secureValueService := service.ProvideSecureValueService(tracer, accessClient, database, secureValueMetadataStorage, keeperMetadataStorage, keeperService, nil) decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index aeff3aa1924..3d055c451e5 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -780,7 +780,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - secureValueService := service12.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) + secureValueService := service12.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService, registerer) secureValueValidator := validator3.ProvideSecureValueValidator() secureValueClient := secret.ProvideSecureValueClient(secureValueService, secureValueValidator, accessClient) decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer) @@ -1341,7 +1341,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - secureValueService := service12.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) + secureValueService := service12.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService, registerer) secureValueValidator := validator3.ProvideSecureValueValidator() secureValueClient := secret.ProvideSecureValueClient(secureValueService, secureValueValidator, accessClient) decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer) diff --git a/pkg/storage/secret/metadata/decrypt_store_test.go b/pkg/storage/secret/metadata/decrypt_store_test.go index 03d0daace36..3c35a443794 100644 --- a/pkg/storage/secret/metadata/decrypt_store_test.go +++ b/pkg/storage/secret/metadata/decrypt_store_test.go @@ -292,8 +292,13 @@ func TestIntegrationDecrypt(t *testing.T) { require.NotEmpty(t, exposed) require.Equal(t, "value", exposed.DangerouslyExposeAndConsumeValue()) - require.Len(t, fakeLogger.InfoArgs, 1) - args := fakeLogger.InfoArgs[0] + require.Len(t, fakeLogger.InfoMsgs, 2) + require.Equal(t, fakeLogger.InfoMsgs[0], "SecureValueMetadataStorage.Read") + require.Equal(t, fakeLogger.InfoMsgs[1], "Secrets Audit Log") + + require.Len(t, fakeLogger.InfoArgs, 2) + // we only want to check the audit log args + args := fakeLogger.InfoArgs[1] require.Contains(t, args, "grafana_decrypter_identity") require.Contains(t, args, "decrypter_identity") for i, arg := range args { diff --git a/pkg/storage/secret/metadata/metrics/metrics.go b/pkg/storage/secret/metadata/metrics/metrics.go index aea3817f12c..f9baba1de64 100644 --- a/pkg/storage/secret/metadata/metrics/metrics.go +++ b/pkg/storage/secret/metadata/metrics/metrics.go @@ -25,12 +25,8 @@ type StorageMetrics struct { KeeperMetadataListCount prometheus.Counter KeeperMetadataGetKeeperConfigDuration prometheus.Histogram - SecureValueMetadataCreateDuration prometheus.Histogram - SecureValueMetadataCreateCount prometheus.Counter - SecureValueMetadataUpdateDuration prometheus.Histogram - SecureValueMetadataUpdateCount prometheus.Counter - SecureValueMetadataDeleteDuration prometheus.Histogram - SecureValueMetadataDeleteCount prometheus.Counter + SecureValueMetadataCreateDuration *prometheus.HistogramVec + SecureValueMetadataCreateCount *prometheus.CounterVec SecureValueMetadataGetDuration prometheus.Histogram SecureValueMetadataGetCount prometheus.Counter SecureValueMetadataListDuration prometheus.Histogram @@ -119,45 +115,19 @@ func newStorageMetrics() *StorageMetrics { }), // Secure value metrics - SecureValueMetadataCreateDuration: prometheus.NewHistogram(prometheus.HistogramOpts{ + SecureValueMetadataCreateDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: namespace, Subsystem: subsystem, Name: "secure_value_metadata_create_duration_seconds", Help: "Duration of secure value metadata create operations", Buckets: prometheus.DefBuckets, - }), - SecureValueMetadataCreateCount: prometheus.NewCounter(prometheus.CounterOpts{ + }, []string{"successful"}), + SecureValueMetadataCreateCount: prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: namespace, Subsystem: subsystem, Name: "secure_value_metadata_create_count", Help: "Count of secure value metadata create operations", - }), - SecureValueMetadataUpdateDuration: prometheus.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "secure_value_metadata_update_duration_seconds", - Help: "Duration of secure value metadata update operations", - Buckets: prometheus.DefBuckets, - }), - SecureValueMetadataUpdateCount: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "secure_value_metadata_update_count", - Help: "Count of secure value metadata update operations", - }), - SecureValueMetadataDeleteDuration: prometheus.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "secure_value_metadata_delete_duration_seconds", - Help: "Duration of secure value metadata delete operations", - Buckets: prometheus.DefBuckets, - }), - SecureValueMetadataDeleteCount: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "secure_value_metadata_delete_count", - Help: "Count of secure value metadata delete operations", - }), + }, []string{"successful"}), SecureValueMetadataGetDuration: prometheus.NewHistogram(prometheus.HistogramOpts{ Namespace: namespace, Subsystem: subsystem, @@ -241,10 +211,6 @@ func NewStorageMetrics(reg prometheus.Registerer) *StorageMetrics { m.KeeperMetadataGetKeeperConfigDuration, m.SecureValueMetadataCreateDuration, m.SecureValueMetadataCreateCount, - m.SecureValueMetadataUpdateDuration, - m.SecureValueMetadataUpdateCount, - m.SecureValueMetadataDeleteDuration, - m.SecureValueMetadataDeleteCount, m.SecureValueMetadataGetDuration, m.SecureValueMetadataGetCount, m.SecureValueMetadataListDuration, diff --git a/pkg/storage/secret/metadata/secure_value_store.go b/pkg/storage/secret/metadata/secure_value_store.go index 3e539b83b3e..46001baf3ae 100644 --- a/pkg/storage/secret/metadata/secure_value_store.go +++ b/pkg/storage/secret/metadata/secure_value_store.go @@ -3,18 +3,21 @@ package metadata import ( "context" "fmt" + "strconv" "time" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana-app-sdk/logging" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/secret/metadata/metrics" "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" + "go.opentelemetry.io/otel/codes" ) var _ contracts.SecureValueMetadataStorage = (*secureValueMetadataStorage)(nil) @@ -40,16 +43,39 @@ type secureValueMetadataStorage struct { tracer trace.Tracer } -func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) { +func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (_ *secretv1beta1.SecureValue, svmCreateErr error) { start := time.Now() + name := sv.GetName() + namespace := sv.GetNamespace() ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Create", trace.WithAttributes( - attribute.String("name", sv.GetName()), - attribute.String("namespace", sv.GetNamespace()), + attribute.String("name", name), + attribute.String("namespace", namespace), attribute.String("actorUID", actorUID), )) defer span.End() - // Set inside of the transaction callback + defer func() { + args := []any{ + "name", name, + "namespace", namespace, + "actorUID", actorUID, + } + + success := svmCreateErr == nil + args = append(args, "success", success) + if !success { + span.SetStatus(codes.Error, "SecureValueMetadataStorage.Create failed") + span.RecordError(svmCreateErr) + args = append(args, "error", svmCreateErr) + } + + logging.FromContext(ctx).Info("SecureValueMetadataStorage.Create", args...) + + s.metrics.SecureValueMetadataCreateDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds()) + s.metrics.SecureValueMetadataCreateCount.WithLabelValues(strconv.FormatBool(success)).Inc() + }() + + // Set inside the transaction callback var row *secureValueDB err := s.db.Transaction(ctx, func(ctx context.Context) error { @@ -145,9 +171,6 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv1bet return nil, fmt.Errorf("convert to kubernetes object: %w", err) } - s.metrics.SecureValueMetadataCreateDuration.Observe(time.Since(start).Seconds()) - s.metrics.SecureValueMetadataCreateCount.Inc() - return createdSecureValue, nil } @@ -230,7 +253,7 @@ func (s *secureValueMetadataStorage) readActiveVersion(ctx context.Context, name return secureValue, nil } -func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv1beta1.SecureValue, error) { +func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (_ *secretv1beta1.SecureValue, readErr error) { start := time.Now() ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Read", trace.WithAttributes( attribute.String("name", name), @@ -239,6 +262,13 @@ func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.N )) defer span.End() + defer func() { + logging.FromContext(ctx).Info("SecureValueMetadataStorage.Read", "namespace", namespace, "name", name, "success", readErr != nil, "error", readErr) + + s.metrics.SecureValueMetadataGetDuration.Observe(time.Since(start).Seconds()) + s.metrics.SecureValueMetadataGetCount.Inc() + }() + secureValue, err := s.readActiveVersion(ctx, namespace, name, opts) if err != nil { return nil, err @@ -249,9 +279,6 @@ func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.N return nil, fmt.Errorf("convert to kubernetes object: %w", err) } - s.metrics.SecureValueMetadataGetDuration.Observe(time.Since(start).Seconds()) - s.metrics.SecureValueMetadataGetCount.Inc() - return secureValueKub, nil } From eecd813c5938a04c3c87e866d06005e068231fb8 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Wed, 30 Jul 2025 16:02:36 -0300 Subject: [PATCH 066/126] Chore: Update test case (#108943) --- .../app/features/explore/spec/helper/testdata/testQueryList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/explore/spec/helper/testdata/testQueryList.ts b/public/app/features/explore/spec/helper/testdata/testQueryList.ts index 0f5d634490f..5d78a28215d 100644 --- a/public/app/features/explore/spec/helper/testdata/testQueryList.ts +++ b/public/app/features/explore/spec/helper/testdata/testQueryList.ts @@ -57,7 +57,7 @@ export const getTestQueryList = () => ({ kind: 'QueryTemplate', apiVersion: 'querylibrary.grafana.app/v0alpha1', metadata: { - name: 'ALoki296tj', + name: 'ALoki296ta', generateName: 'ALoki', namespace: 'default', uid: '3e71de65-efa7-40e3-8f23-124212cca455', From 5b0a118f32f9586b0a8f35688d4cadd956ad5759 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:28:58 -0500 Subject: [PATCH 067/126] Canvas: Add support for actions variables when one-click is enabled (#108724) --- packages/grafana-ui/src/index.ts | 1 + .../app/features/canvas/runtime/element.tsx | 63 ++++++++++++++++--- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/packages/grafana-ui/src/index.ts b/packages/grafana-ui/src/index.ts index db367a8790e..d35561e598f 100644 --- a/packages/grafana-ui/src/index.ts +++ b/packages/grafana-ui/src/index.ts @@ -358,6 +358,7 @@ export { type UPlotConfigPrepFn } from './components/uPlot/config/UPlotConfigBui export * from './components/PanelChrome/types'; export { Label as BrowserLabel } from './components/BrowserLabel/Label'; export { PanelContainer } from './components/PanelContainer/PanelContainer'; +export { VariablesInputModal } from './components/Actions/VariablesInputModal'; // ----------------------------------------------------- // Graveyard: exported, but no longer used internally diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 047d42a5e45..4659c2c0a84 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -10,9 +10,10 @@ import { ValueLinkConfig, OneClickMode, ActionModel, + ActionVariableInput, } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { ConfirmModal } from '@grafana/ui'; +import { ConfirmModal, VariablesInputModal } from '@grafana/ui'; import { LayerElement } from 'app/core/components/Layers/types'; import { notFoundItem } from 'app/features/canvas/elements/notFound'; import { DimensionContext } from 'app/features/dimensions/context'; @@ -58,7 +59,15 @@ export class ElementState implements LayerElement { // cached for tooltips/mousemove oneClickMode = OneClickMode.Off; - showConfirmation = false; + showActionConfirmation = false; + + showActionVarsModal = false; + actionVars: ActionVariableInput = {}; + + setActionVars = (vars: ActionVariableInput) => { + this.actionVars = vars; + this.forceUpdate(); + }; constructor( public item: CanvasElementItem, @@ -394,6 +403,8 @@ export class ElementState implements LayerElement { this.oneClickMode = OneClickMode.Link; } else if (this.options.actions?.some((action) => action.oneClick === true)) { this.oneClickMode = OneClickMode.Action; + } else { + this.oneClickMode = OneClickMode.Off; } if (frames) { @@ -705,8 +716,16 @@ export class ElementState implements LayerElement { window.open(primaryDataLink.href, primaryDataLink.target ?? '_self'); } } else if (this.oneClickMode === OneClickMode.Action) { - this.showConfirmation = true; - this.forceUpdate(); + const primaryAction = this.getPrimaryAction(); + const actionHasVariables = primaryAction?.variables && primaryAction.variables.length > 0; + + if (actionHasVariables) { + this.showActionVarsModal = true; + this.forceUpdate(); + } else { + this.showActionConfirmation = true; + this.forceUpdate(); + } } else { this.handleTooltip(event); this.onTooltipCallback(); @@ -748,7 +767,7 @@ export class ElementState implements LayerElement { return ( <> - {this.showConfirmation && action && ( + {this.showActionConfirmation && action && ( { - this.showConfirmation = false; - action.onClick(new MouseEvent('click')); + this.showActionConfirmation = false; + action.onClick(new MouseEvent('click'), null, this.actionVars); this.forceUpdate(); }} onDismiss={() => { - this.showConfirmation = false; + this.showActionConfirmation = false; this.forceUpdate(); }} /> @@ -770,6 +789,31 @@ export class ElementState implements LayerElement { ); }; + renderVariablesInputModal = (action: ActionModel | undefined) => { + if (!action || !action.variables || action.variables.length === 0) { + return; + } + + const onModalContinue = () => { + this.showActionVarsModal = false; + this.showActionConfirmation = true; + this.forceUpdate(); + }; + + return ( + { + this.showActionVarsModal = false; + this.forceUpdate(); + }} + onShowConfirm={onModalContinue} + /> + ); + }; + render() { const { item, div } = this; const scene = this.getScene(); @@ -794,7 +838,8 @@ export class ElementState implements LayerElement { isSelected={isSelected} />

    - {this.showConfirmation && this.renderActionsConfirmModal(this.getPrimaryAction())} + {this.showActionConfirmation && this.renderActionsConfirmModal(this.getPrimaryAction())} + {this.showActionVarsModal && this.renderVariablesInputModal(this.getPrimaryAction())} ); } From 9022357fb7f5462f0b34558bcd4cdb0a0530c9c1 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:26:36 -0500 Subject: [PATCH 068/126] Canvas: Add option to disable the tooltip (#108196) --- .../visualizations/canvas/index.md | 8 ++++ .../panelcfg/x/CanvasPanelCfg_types.gen.ts | 8 ++++ .../app/features/canvas/runtime/element.tsx | 11 ++--- public/app/features/canvas/runtime/scene.tsx | 43 +++++++++++-------- .../app/plugins/panel/canvas/CanvasPanel.tsx | 32 +++----------- .../panel/canvas/components/CanvasTooltip.tsx | 18 ++++---- public/app/plugins/panel/canvas/module.tsx | 24 ++++++++++- public/app/plugins/panel/canvas/panelcfg.cue | 5 +++ .../app/plugins/panel/canvas/panelcfg.gen.ts | 8 ++++ public/locales/en-US/grafana.json | 8 ++++ 10 files changed, 106 insertions(+), 59 deletions(-) diff --git a/docs/sources/panels-visualizations/visualizations/canvas/index.md b/docs/sources/panels-visualizations/visualizations/canvas/index.md index 4be10911af6..a5f4712c502 100644 --- a/docs/sources/panels-visualizations/visualizations/canvas/index.md +++ b/docs/sources/panels-visualizations/visualizations/canvas/index.md @@ -270,6 +270,14 @@ You can enable infinite panning in a canvas when pan and zoom is enabled. This a Infinite panning is an experimental feature that may not work as expected in all scenarios. For example, elements that are not top-left constrained may experience unexpected movement when panning. {{< /admonition >}} +### Tooltip options + +The **Tooltip mode** setting controls the display of tooltips when hovering over canvas elements that are connected to data, data links, or actions. +The options are: + +- **Enabled** - Show a tooltip when the cursor hovers over an element. +- **Disabled** - Tooltips are not shown on hover. + ### Layer options The **Layer** options let you add elements to the canvas and control its appearance: diff --git a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts index 983129db72b..722317c115a 100644 --- a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts @@ -112,6 +112,10 @@ export const defaultCanvasElementOptions: Partial = { connections: [], }; +export interface CanvasTooltip { + mode: ui.TooltipDisplayMode; +} + export interface Options { /** * Enable infinite pan @@ -147,6 +151,10 @@ export interface Options { * Show all available element types */ showAdvancedTypes: boolean; + /** + * Controls tooltip options + */ + tooltip: CanvasTooltip; } export const defaultOptions: Partial = { diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 4659c2c0a84..d06c7d5caee 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -13,6 +13,7 @@ import { ActionVariableInput, } from '@grafana/data'; import { t } from '@grafana/i18n'; +import { TooltipDisplayMode } from '@grafana/schema'; import { ConfirmModal, VariablesInputModal } from '@grafana/ui'; import { LayerElement } from 'app/core/components/Layers/types'; import { notFoundItem } from 'app/features/canvas/elements/notFound'; @@ -619,7 +620,7 @@ export class ElementState implements LayerElement { handleMouseEnter = (event: React.MouseEvent, isSelected: boolean | undefined) => { const scene = this.getScene(); - const shouldHandleTooltip = !scene?.isEditingEnabled && !scene?.tooltip?.isOpen; + const shouldHandleTooltip = !scene?.isEditingEnabled && !scene?.tooltipPayload?.isOpen; if (shouldHandleTooltip) { this.handleTooltip(event); } else if (!isSelected) { @@ -686,7 +687,7 @@ export class ElementState implements LayerElement { handleTooltip = (event: React.MouseEvent) => { const scene = this.getScene(); - if (scene?.tooltipCallback) { + if (scene?.tooltipCallback && scene.tooltipMode !== TooltipDisplayMode.None) { const rect = this.div?.getBoundingClientRect(); scene.tooltipCallback({ anchorPoint: { x: rect?.right ?? event.pageX, y: rect?.top ?? event.pageY }, @@ -698,7 +699,7 @@ export class ElementState implements LayerElement { handleMouseLeave = (event: React.MouseEvent) => { const scene = this.getScene(); - if (scene?.tooltipCallback && !scene?.tooltip?.isOpen) { + if (scene?.tooltipCallback && !scene?.tooltipPayload?.isOpen) { scene.tooltipCallback(undefined); } @@ -744,9 +745,9 @@ export class ElementState implements LayerElement { onTooltipCallback = () => { const scene = this.getScene(); - if (scene?.tooltipCallback && scene.tooltip?.anchorPoint) { + if (scene?.tooltipCallback && scene.tooltipPayload?.anchorPoint) { scene.tooltipCallback({ - anchorPoint: { x: scene.tooltip.anchorPoint.x, y: scene.tooltip.anchorPoint.y }, + anchorPoint: { x: scene.tooltipPayload.anchorPoint.x, y: scene.tooltipPayload.anchorPoint.y }, element: this, isOpen: true, }); diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 5e54afd9be8..298b232e16b 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -13,6 +13,7 @@ import { ScalarDimensionConfig, ScaleDimensionConfig, TextDimensionConfig, + TooltipDisplayMode, } from '@grafana/schema'; import { Portal } from '@grafana/ui'; import { config } from 'app/core/config'; @@ -27,11 +28,13 @@ import { import { CanvasContextMenu } from 'app/plugins/panel/canvas/components/CanvasContextMenu'; import { CanvasTooltip } from 'app/plugins/panel/canvas/components/CanvasTooltip'; import { Connections } from 'app/plugins/panel/canvas/components/connections/Connections'; +import { Options } from 'app/plugins/panel/canvas/panelcfg.gen'; import { AnchorPoint, CanvasTooltipPayload } from 'app/plugins/panel/canvas/types'; import { getTransformInstance } from 'app/plugins/panel/canvas/utils'; import appEvents from '../../../core/app_events'; import { CanvasPanel } from '../../../plugins/panel/canvas/CanvasPanel'; +import { getDashboardSrv } from '../../dashboard/services/DashboardSrv'; import { CanvasFrameOptions } from '../frame'; import { DEFAULT_CANVAS_ELEMENT_CONFIG } from '../registry'; @@ -71,6 +74,7 @@ export class Scene { shouldShowAdvancedTypes?: boolean; shouldPanZoom?: boolean; shouldInfinitePan?: boolean; + tooltipMode?: TooltipDisplayMode; skipNextSelectionBroadcast = false; ignoreDataUpdate = false; panel: CanvasPanel; @@ -93,7 +97,7 @@ export class Scene { setBackgroundCallback?: (anchorPoint: AnchorPoint) => void; tooltipCallback?: (tooltip: CanvasTooltipPayload | undefined) => void; - tooltip?: CanvasTooltipPayload; + tooltipPayload?: CanvasTooltipPayload; moveableActionCallback?: (moved: boolean) => void; @@ -106,15 +110,16 @@ export class Scene { transformComponentRef: RefObject | undefined; constructor( - cfg: CanvasFrameOptions, - enableEditing: boolean, - showAdvancedTypes: boolean, - panZoom: boolean, - infinitePan: boolean, + options: Options, public onSave: (cfg: CanvasFrameOptions) => void, panel: CanvasPanel ) { - this.root = this.load(cfg, enableEditing, showAdvancedTypes, panZoom, infinitePan); + // TODO: Will need to update this approach for dashboard scenes + // migration (new dashboard edit experience) + const dashboard = getDashboardSrv().getCurrent(); + const enableEditing = options.inlineEditing && dashboard?.editable; + + this.root = this.load(options, enableEditing); this.subscription = this.editModeEnabled.subscribe((open) => { if (!this.moveable || !this.isEditingEnabled) { @@ -147,15 +152,12 @@ export class Scene { return !this.byName.has(v); }; - load( - cfg: CanvasFrameOptions, - enableEditing: boolean, - showAdvancedTypes: boolean, - panZoom: boolean, - infinitePan: boolean - ) { + load(options: Options, enableEditing: boolean) { + const { root, showAdvancedTypes, panZoom, infinitePan, tooltip } = options; + const tooltipMode = tooltip?.mode ?? TooltipDisplayMode.Single; + this.root = new RootElement( - cfg ?? { + root ?? { type: 'frame', elements: [DEFAULT_CANVAS_ELEMENT_CONFIG], }, @@ -167,6 +169,7 @@ export class Scene { this.shouldShowAdvancedTypes = showAdvancedTypes; this.shouldPanZoom = panZoom; this.shouldInfinitePan = infinitePan; + this.tooltipMode = tooltipMode; setTimeout(() => { if (this.div) { @@ -283,11 +286,13 @@ export class Scene { }; render() { - const hasDataLinks = this.tooltip?.element?.getLinks && this.tooltip.element.getLinks({}).length > 0; - const hasActions = this.tooltip?.element?.options.actions && this.tooltip.element.options.actions.length > 0; + const hasDataLinks = this.tooltipPayload?.element?.getLinks && this.tooltipPayload.element.getLinks({}).length > 0; + const hasActions = + this.tooltipPayload?.element?.options.actions && this.tooltipPayload.element.options.actions.length > 0; - const isTooltipValid = hasDataLinks || hasActions || this.tooltip?.element?.data?.field; - const canShowElementTooltip = !this.isEditingEnabled && isTooltipValid; + const isTooltipValid = hasDataLinks || hasActions || this.tooltipPayload?.element?.data?.field; + const isTooltipEnabled = this.tooltipMode !== TooltipDisplayMode.None; + const canShowElementTooltip = !this.isEditingEnabled && isTooltipValid && isTooltipEnabled; const sceneDiv = (
    diff --git a/public/app/plugins/panel/canvas/CanvasPanel.tsx b/public/app/plugins/panel/canvas/CanvasPanel.tsx index 10f43819839..3af277aa833 100644 --- a/public/app/plugins/panel/canvas/CanvasPanel.tsx +++ b/public/app/plugins/panel/canvas/CanvasPanel.tsx @@ -1,5 +1,5 @@ -import { Component } from 'react'; import * as React from 'react'; +import { Component } from 'react'; import { ReplaySubject, Subscription } from 'rxjs'; import { PanelProps } from '@grafana/data'; @@ -8,7 +8,6 @@ import { PanelContext, PanelContextRoot } from '@grafana/ui'; import { CanvasFrameOptions } from 'app/features/canvas/frame'; import { ElementState } from 'app/features/canvas/runtime/element'; import { Scene } from 'app/features/canvas/runtime/scene'; -import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events'; import { SetBackground } from './components/SetBackground'; @@ -63,22 +62,9 @@ export class CanvasPanel extends Component { moveableAction: false, }; - // TODO: Will need to update this approach for dashboard scenes - // migration (new dashboard edit experience) - const dashboard = getDashboardSrv().getCurrent(); - const allowEditing = this.props.options.inlineEditing && dashboard?.editable; - // Only the initial options are ever used. // later changes are all controlled by the scene - this.scene = new Scene( - this.props.options.root, - allowEditing, - this.props.options.showAdvancedTypes, - this.props.options.panZoom, - this.props.options.infinitePan, - this.onUpdateScene, - this - ); + this.scene = new Scene(this.props.options, this.onUpdateScene, this); this.scene.updateSize(props.width, props.height); this.scene.updateData(props.data); this.scene.inlineEditingCallback = this.openInlineEdit; @@ -239,12 +225,14 @@ export class CanvasPanel extends Component { this.props.options.showAdvancedTypes !== nextProps.options.showAdvancedTypes; const panZoomSwitched = this.props.options.panZoom !== nextProps.options.panZoom; const infinitePanSwitched = this.props.options.infinitePan !== nextProps.options.infinitePan; + const tooltipModeSwitched = this.props.options.tooltip?.mode !== nextProps.options.tooltip?.mode; if ( this.needsReload || inlineEditingSwitched || shouldShowAdvancedTypesSwitched || panZoomSwitched || - infinitePanSwitched + infinitePanSwitched || + tooltipModeSwitched ) { if (inlineEditingSwitched) { // Replace scene div to prevent selecto instance leaks @@ -252,13 +240,7 @@ export class CanvasPanel extends Component { } this.needsReload = false; - this.scene.load( - nextProps.options.root, - nextProps.options.inlineEditing, - nextProps.options.showAdvancedTypes, - nextProps.options.panZoom, - nextProps.options.infinitePan - ); + this.scene.load(nextProps.options, nextProps.options.inlineEditing); this.scene.updateSize(nextProps.width, nextProps.height); this.scene.updateData(nextProps.data); changed = true; @@ -294,7 +276,7 @@ export class CanvasPanel extends Component { }; tooltipCallback = (tooltip: CanvasTooltipPayload | undefined) => { - this.scene.tooltip = tooltip; + this.scene.tooltipPayload = tooltip; this.forceUpdate(); }; diff --git a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx index ceb17a8ce7d..aa0e29741c5 100644 --- a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx @@ -36,7 +36,7 @@ export const CanvasTooltip = ({ scene }: Props) => { const styles = useStyles2(getStyles); const onClose = () => { - if (scene?.tooltipCallback && scene.tooltip) { + if (scene?.tooltipCallback && scene.tooltipPayload) { scene.tooltipCallback(undefined); } }; @@ -45,7 +45,7 @@ export const CanvasTooltip = ({ scene }: Props) => { const { overlayProps } = useOverlay({ onClose: onClose, isDismissable: true }, ref); const { dialogProps } = useDialog({}, ref); - const element = scene.tooltip?.element; + const element = scene.tooltipPayload?.element; if (!element) { return <>; } @@ -138,18 +138,18 @@ export const CanvasTooltip = ({ scene }: Props) => { return ( <> - {scene.tooltip?.element && scene.tooltip.anchorPoint && ( + {scene.tooltipPayload?.element && scene.tooltipPayload.anchorPoint && (
    - {scene.tooltip.isOpen && } - - {element.data.text && } + {scene.tooltipPayload.isOpen && } + + {element.data.text && } {(links.length > 0 || actions.length > 0) && }
    diff --git a/public/app/plugins/panel/canvas/module.tsx b/public/app/plugins/panel/canvas/module.tsx index af54dbc280d..512509841ca 100644 --- a/public/app/plugins/panel/canvas/module.tsx +++ b/public/app/plugins/panel/canvas/module.tsx @@ -1,6 +1,7 @@ import { FieldConfigProperty, PanelOptionsEditorBuilder, PanelPlugin } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; +import { TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen'; import { FrameState } from 'app/features/canvas/runtime/frame'; import { CanvasPanel, InstanceState } from './CanvasPanel'; @@ -12,7 +13,7 @@ import { canvasMigrationHandler } from './migrations'; import { Options } from './panelcfg.gen'; export const addStandardCanvasEditorOptions = (builder: PanelOptionsEditorBuilder) => { - const category = [t('canvas.category-canvas', 'Canvas')]; + let category = [t('canvas.category-canvas', 'Canvas')]; builder.addBooleanSwitch({ path: 'inlineEditing', name: t('canvas.name-inline-editing', 'Inline editing'), @@ -56,6 +57,27 @@ export const addStandardCanvasEditorOptions = (builder: PanelOptionsEditorBuilde defaultValue: false, showIf: (opts) => config.featureToggles.canvasPanelPanZoom && opts.panZoom, }); + + category = [t('canvas.category-tooltip', 'Tooltip')]; + + builder.addRadio({ + path: 'tooltip.mode', + name: t('canvas.tooltip-options.name-tooltip-mode', 'Tooltip mode'), + category, + defaultValue: TooltipDisplayMode.Single, + settings: { + options: [ + { + value: TooltipDisplayMode.Single, + label: t('canvas.tooltip-options.tooltip-mode-options.label-enabled', 'Enabled'), + }, + { + value: TooltipDisplayMode.None, + label: t('canvas.tooltip-options.tooltip-mode-options.label-disabled', 'Disabled'), + }, + ], + }, + }); }; export const plugin = new PanelPlugin(CanvasPanel) diff --git a/public/app/plugins/panel/canvas/panelcfg.cue b/public/app/plugins/panel/canvas/panelcfg.cue index 6da7cebacc4..f32837ea1e8 100644 --- a/public/app/plugins/panel/canvas/panelcfg.cue +++ b/public/app/plugins/panel/canvas/panelcfg.cue @@ -88,6 +88,9 @@ composableKinds: PanelCfg: { border?: LineConfig connections?: [...CanvasConnection] } @cuetsy(kind="interface") + CanvasTooltip: { + mode: ui.TooltipDisplayMode + } @cuetsy(kind="interface") Options: { // Enable inline editing @@ -108,6 +111,8 @@ composableKinds: PanelCfg: { // The list of canvas elements attached to the root element elements: [...CanvasElementOptions] } @cuetsy(kind="interface") + // Controls tooltip options + tooltip: CanvasTooltip } @cuetsy(kind="interface") } }] diff --git a/public/app/plugins/panel/canvas/panelcfg.gen.ts b/public/app/plugins/panel/canvas/panelcfg.gen.ts index 9706c886f6d..be425f6c764 100644 --- a/public/app/plugins/panel/canvas/panelcfg.gen.ts +++ b/public/app/plugins/panel/canvas/panelcfg.gen.ts @@ -110,6 +110,10 @@ export const defaultCanvasElementOptions: Partial = { connections: [], }; +export interface CanvasTooltip { + mode: ui.TooltipDisplayMode; +} + export interface Options { /** * Enable infinite pan @@ -145,6 +149,10 @@ export interface Options { * Show all available element types */ showAdvancedTypes: boolean; + /** + * Controls tooltip options + */ + tooltip: CanvasTooltip; } export const defaultOptions: Partial = { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 5e9f8d224bc..0a72605b908 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3738,6 +3738,7 @@ "category-selected-connection": "Selected connection", "category-selected-element": "Selected element ({{element}})", "category-size": "Size", + "category-tooltip": "Tooltip", "cloud-item": { "category-cloud": "Cloud", "label": { @@ -3982,6 +3983,13 @@ "auto": "Auto" } }, + "tooltip-options": { + "name-tooltip-mode": "Tooltip mode", + "tooltip-mode-options": { + "label-disabled": "Disabled", + "label-enabled": "Enabled" + } + }, "tree-navigation-editor": { "clear-selection": "Clear selection", "frame-selection": "Frame selection", From 0f53290ca70eb2bc00569dbba42993b1138b1d75 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:32:43 -0500 Subject: [PATCH 069/126] Chore: Decouple `hideFrom.viz` and `hideFrom.tooltip` (#106857) --- packages/grafana-data/src/field/fieldState.ts | 2 +- .../VizTooltip/VizTooltipColorIndicator.tsx | 20 ++++++++ .../VizTooltip/VizTooltipContent.tsx | 3 +- .../components/VizTooltip/VizTooltipRow.tsx | 9 +++- .../src/components/VizTooltip/types.ts | 1 + .../src/components/VizTooltip/utils.ts | 49 ++++++++++++++----- .../dashgrid/SeriesVisibilityConfigFactory.ts | 4 +- .../dashboard/state/DashboardMigrator.ts | 24 +++++++++ .../app/plugins/panel/piechart/PieChart.tsx | 2 +- .../panel/timeseries/TimeSeriesTooltip.tsx | 13 +---- 10 files changed, 98 insertions(+), 29 deletions(-) diff --git a/packages/grafana-data/src/field/fieldState.ts b/packages/grafana-data/src/field/fieldState.ts index 3ddaebbda63..2501444a3a0 100644 --- a/packages/grafana-data/src/field/fieldState.ts +++ b/packages/grafana-data/src/field/fieldState.ts @@ -62,7 +62,7 @@ export function cacheFieldDisplayNames(frames: DataFrame[]) { /** * * moves each field's config.custom.hideFrom to field.state.hideFrom - * and mutates orgiginal field.config.custom.hideFrom to one with explicit overrides only, (without the ad-hoc stateful __system override from legend toggle) + * and mutates original field.config.custom.hideFrom to one with explicit overrides only, (without the ad-hoc stateful __system override from legend toggle) */ export function decoupleHideFromState(frames: DataFrame[], fieldConfig: FieldConfigSource) { frames.forEach((frame) => { diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx index 34c251eee13..789b50cba2a 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipColorIndicator.tsx @@ -19,6 +19,7 @@ interface Props { colorIndicator?: ColorIndicator; position?: ColorIndicatorPosition; lineStyle?: LineStyle; + isHollow?: boolean; } export type ColorIndicatorStyles = ReturnType; @@ -28,9 +29,22 @@ export const VizTooltipColorIndicator = ({ colorIndicator = DEFAULT_COLOR_INDICATOR, position = ColorIndicatorPosition.Leading, lineStyle, + isHollow, }: Props) => { const styles = useStyles2(getStyles); + if (isHollow) { + return ( +
    + ); + } + if (colorIndicator === ColorIndicator.series) { return ( ({ trailing: css({ marginLeft: theme.spacing(0.5), }), + series: css({ + width: '14px', + height: '4px', + borderRadius: theme.shape.radius.pill, + minWidth: '14px', + }), value: css({ width: '12px', height: '12px', diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx index 40f19c37157..661581237a7 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx @@ -34,7 +34,7 @@ export const VizTooltipContent = ({ return (
    - {items.map(({ label, value, color, colorIndicator, colorPlacement, isActive, lineStyle }, i) => ( + {items.map(({ label, value, color, colorIndicator, colorPlacement, isActive, lineStyle, isHiddenFromViz }, i) => ( ))} {children} diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx index 19d0bed7874..1471dc5e38a 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx @@ -18,6 +18,7 @@ interface VizTooltipRowProps extends Omit { marginRight?: string; isPinned: boolean; showValueScroll?: boolean; + isHiddenFromViz?: boolean; } enum LabelValueTypes { @@ -41,6 +42,7 @@ export const VizTooltipRow = ({ isPinned, lineStyle, showValueScroll, + isHiddenFromViz, }: VizTooltipRowProps) => { const styles = useStyles2(getStyles, justify, marginRight); @@ -132,7 +134,12 @@ export const VizTooltipRow = ({ {(color || label) && (
    {color && colorPlacement === ColorPlacement.first && ( - + )} {!isPinned ? (
    {label}
    diff --git a/packages/grafana-ui/src/components/VizTooltip/types.ts b/packages/grafana-ui/src/components/VizTooltip/types.ts index de4fbae048f..bd4f66488fa 100644 --- a/packages/grafana-ui/src/components/VizTooltip/types.ts +++ b/packages/grafana-ui/src/components/VizTooltip/types.ts @@ -27,6 +27,7 @@ export interface VizTooltipItem { colorPlacement?: ColorPlacement; isActive?: boolean; lineStyle?: LineStyle; + isHiddenFromViz?: boolean; // internal/tmp for sorting numeric?: number; diff --git a/packages/grafana-ui/src/components/VizTooltip/utils.ts b/packages/grafana-ui/src/components/VizTooltip/utils.ts index e192340974f..d14d636caa5 100644 --- a/packages/grafana-ui/src/components/VizTooltip/utils.ts +++ b/packages/grafana-ui/src/components/VizTooltip/utils.ts @@ -47,6 +47,8 @@ export const calculateTooltipPosition = ( export const getColorIndicatorClass = (colorIndicator: string, styles: ColorIndicatorStyles) => { switch (colorIndicator) { + case ColorIndicator.series: + return styles.series; case ColorIndicator.value: return styles.value; case ColorIndicator.hexagon: @@ -80,7 +82,8 @@ export const getContentItems = ( mode: TooltipDisplayMode, sortOrder: SortOrder, fieldFilter = (field: Field) => true, - hideZeros = false + hideZeros = false, + _restFields?: Field[] ): VizTooltipItem[] => { let rows: VizTooltipItem[] = []; @@ -93,8 +96,7 @@ export const getContentItems = ( field === xField || field.type === FieldType.time || !fieldFilter(field) || - field.config.custom?.hideFrom?.tooltip || - field.config.custom?.hideFrom?.viz + field.config.custom?.hideFrom?.tooltip ) { continue; } @@ -130,15 +132,7 @@ export const getContentItems = ( ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER; - const colorMode = getFieldColorModeForField(field); - - let colorIndicator = ColorIndicator.series; - let colorPlacement = ColorPlacement.first; - - if (colorMode.isByValue) { - colorIndicator = ColorIndicator.value; - colorPlacement = ColorPlacement.trailing; - } + const { colorIndicator, colorPlacement } = getIndicatorAndPlacement(field); rows.push({ label: field.state?.displayName ?? field.name, @@ -152,6 +146,23 @@ export const getContentItems = ( }); } + _restFields?.forEach((field) => { + if (!field.config.custom?.hideFrom?.tooltip) { + const { colorIndicator, colorPlacement } = getIndicatorAndPlacement(field); + const display = field.display!(field.values[dataIdxs[0]!]); + + rows.push({ + label: field.state?.displayName ?? field.name, + value: formattedValueToString(display), + color: FALLBACK_COLOR, + colorIndicator, + colorPlacement, + lineStyle: field.config.custom?.lineStyle, + isHiddenFromViz: true, + }); + } + }); + if (sortOrder !== SortOrder.None && rows.length > 1) { const cmp = allNumeric ? numberCmp : stringCmp; const mult = sortOrder === SortOrder.Descending ? -1 : 1; @@ -160,3 +171,17 @@ export const getContentItems = ( return rows; }; + +const getIndicatorAndPlacement = (field: Field) => { + const colorMode = getFieldColorModeForField(field); + + let colorIndicator = ColorIndicator.series; + let colorPlacement = ColorPlacement.first; + + if (colorMode.isByValue) { + colorIndicator = ColorIndicator.value; + colorPlacement = ColorPlacement.trailing; + } + + return { colorIndicator, colorPlacement }; +}; diff --git a/public/app/features/dashboard/dashgrid/SeriesVisibilityConfigFactory.ts b/public/app/features/dashboard/dashgrid/SeriesVisibilityConfigFactory.ts index a03cc64bb5a..412d37d4bfc 100644 --- a/public/app/features/dashboard/dashgrid/SeriesVisibilityConfigFactory.ts +++ b/public/app/features/dashboard/dashgrid/SeriesVisibilityConfigFactory.ts @@ -97,7 +97,7 @@ function createOverride( value: { viz: true, legend: false, - tooltip: false, + tooltip: true, }, }; @@ -118,7 +118,7 @@ function createOverride( value: { viz: true, legend: false, - tooltip: false, + tooltip: true, }, }, ], diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index da9a51195f1..6b82290c993 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -927,6 +927,10 @@ export class DashboardMigrator { } } + if (oldVersion < 42) { + panelUpgrades.push(migrateHideFromFunctionality); + } + /** * -==- Add migration here -==- * Your migration should go below the previous @@ -1480,3 +1484,23 @@ function ensureXAxisVisibility(panel: PanelModel) { return panel; } + +function migrateHideFromFunctionality(panel: PanelModel) { + // migrate overrides with hideFrom.viz = true to also set tooltip = true + // this includes the __systemRef override + if (panel.fieldConfig && panel.fieldConfig.overrides) { + panel.fieldConfig.overrides = panel.fieldConfig.overrides.map((override) => { + if (override.properties) { + override.properties = override.properties.map((property) => { + if (property.id === 'custom.hideFrom' && property.value?.viz === true) { + property.value.tooltip = true; + } + return property; + }); + } + return override; + }); + } + + return panel; +} diff --git a/public/app/plugins/panel/piechart/PieChart.tsx b/public/app/plugins/panel/piechart/PieChart.tsx index f0bcb6d7cc0..3abbe438cfb 100644 --- a/public/app/plugins/panel/piechart/PieChart.tsx +++ b/public/app/plugins/panel/piechart/PieChart.tsx @@ -325,7 +325,7 @@ function getTooltipData( } const customConfig = pa.data.field.custom; - return !customConfig?.hideFrom?.tooltip && !customConfig?.hideFrom?.viz; + return !customConfig?.hideFrom?.tooltip; }) .map((pieArc) => { return { diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index 36cf73bb58a..8c0bae437e9 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -13,7 +13,6 @@ import { } from '@grafana/ui/internal'; import { getFieldActions } from '../status-history/utils'; -import { fmt } from '../xychart/utils'; import { isTooltipScrollable } from './utils'; @@ -71,18 +70,10 @@ export const TimeSeriesTooltip = ({ mode, sortOrder, (field) => field.type === FieldType.number || field.type === FieldType.enum, - hideZeros + hideZeros, + _rest ); - _rest?.forEach((field) => { - if (!field.config.custom?.hideFrom?.tooltip) { - contentItems.push({ - label: field.state?.displayName ?? field.name, - value: fmt(field, field.values[dataIdxs[0]!]), - }); - } - }); - let footer: ReactNode; if (seriesIdx != null) { From 8729ea2dbad9d3ee8cd49cb0fbf5d060a083a661 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Wed, 30 Jul 2025 17:11:39 -0400 Subject: [PATCH 070/126] Apps: Fix status key functions (#108915) --- apps/advisor/go.mod | 4 +- apps/advisor/go.sum | 12 +-- apps/alerting/notifications/go.mod | 4 +- apps/alerting/notifications/go.sum | 8 +- apps/dashboard/go.mod | 4 +- apps/dashboard/go.sum | 8 +- apps/folder/go.mod | 4 +- apps/folder/go.sum | 8 +- apps/iam/go.mod | 4 +- apps/iam/go.sum | 8 +- apps/investigations/go.mod | 4 +- apps/investigations/go.sum | 12 +-- apps/playlist/go.mod | 4 +- apps/playlist/go.sum | 8 +- apps/sdk.mk | 2 +- apps/secret/go.mod | 4 +- apps/secret/go.sum | 8 +- go.mod | 6 +- go.sum | 12 +-- go.work.sum | 22 ++++-- pkg/apiserver/go.mod | 2 +- pkg/apiserver/go.sum | 4 +- .../apiserver/appinstaller/installer.go | 1 - pkg/services/apiserver/appinstaller/server.go | 78 +++++++++++-------- 24 files changed, 129 insertions(+), 102 deletions(-) diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 4559d1045d9..33afd0a0ae6 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -7,8 +7,8 @@ require ( github.com/google/go-github/v70 v70.0.0 github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.40.1 - github.com/grafana/grafana-app-sdk/logging v0.40.0 + github.com/grafana/grafana-app-sdk v0.40.2 + github.com/grafana/grafana-app-sdk/logging v0.40.1 github.com/grafana/grafana-plugin-sdk-go v0.278.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250725144121-b1592b5e36d2 github.com/stretchr/testify v1.10.0 diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index b52f4e5a7ab..7adf9d1eb21 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -663,18 +663,18 @@ 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-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg2svGyO40Ex92G02aOyHzP6XHCE= github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E= -github.com/grafana/grafana-app-sdk v0.40.1 h1:W8d1CQSgMg/d37xf/0t4PiSa7wVD21XFE8mF6P59YSg= -github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM= -github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0H0OVhzzlOKd4Ehu+E= -github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana-app-sdk v0.40.2 h1:j2ftFuqhX+exYUipfEjeWDs3i7oiJkweTF8gFLL7wWU= +github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0= +github.com/grafana/grafana-app-sdk/logging v0.40.1 h1:ru+GqbaQk6jthA5l2Yo1WI/JbNXKNQmLiqNrxz7HGP4= +github.com/grafana/grafana-app-sdk/logging v0.40.1/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana-aws-sdk v1.0.4 h1:D14UAehsOqpjliHmHzveRQ1p43KCsMzdmb7GovWj+SY= github.com/grafana/grafana-aws-sdk v1.0.4/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA= github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0/go.mod h1:H9sVh9A4yg5egMGZeh0mifxT1Q/uqwKe1LBjBJU6pN8= github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho= github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71 h1:7AVAgb0CalPYtUC9SJKYAkByOBGwL6r50P4QMEiJi44= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71/go.mod h1:/iuseD/cEpXDiy7MpL+4qBFZ3H6esnUJTYzpoJMw9dw= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730164619-34019e5ec017 h1:Niy+KRDWHsUVqfhZQg0oZbAQFO6QcO6a4l9V/ouDEEs= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730164619-34019e5ec017/go.mod h1:/iuseD/cEpXDiy7MpL+4qBFZ3H6esnUJTYzpoJMw9dw= github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b h1:31MwoIKKT9Ay0ZjbT4lkfcPijiWogUWzXs2EjrCgodI= github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:dLtYBp1pza5HYalezNvzlP8JDeKrZ5BKTonDgEOE0NY= github.com/grafana/grafana/apps/secret v0.0.0-20250711114246-c9b2126c4ad5 h1:+fMhUoqwGdY8ntH0GL2icJa3uk+bTiIMicawDG2r9Uc= diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 8fd63458f4f..5698787f545 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/alerting/notifications go 1.24.5 require ( - github.com/grafana/grafana-app-sdk v0.40.1 - github.com/grafana/grafana-app-sdk/logging v0.40.0 + github.com/grafana/grafana-app-sdk v0.40.2 + github.com/grafana/grafana-app-sdk/logging v0.40.1 k8s.io/apimachinery v0.33.3 k8s.io/apiserver v0.33.3 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index e3c83758737..2931a8f5200 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -84,10 +84,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/grafana-app-sdk v0.40.1 h1:W8d1CQSgMg/d37xf/0t4PiSa7wVD21XFE8mF6P59YSg= -github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM= -github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0H0OVhzzlOKd4Ehu+E= -github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana-app-sdk v0.40.2 h1:j2ftFuqhX+exYUipfEjeWDs3i7oiJkweTF8gFLL7wWU= +github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0= +github.com/grafana/grafana-app-sdk/logging v0.40.1 h1:ru+GqbaQk6jthA5l2Yo1WI/JbNXKNQmLiqNrxz7HGP4= +github.com/grafana/grafana-app-sdk/logging v0.40.1/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= 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-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 8bbf2e9f607..0d00cb6c511 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -4,7 +4,7 @@ go 1.24.5 require ( cuelang.org/go v0.11.1 - github.com/grafana/grafana-app-sdk v0.40.1 + github.com/grafana/grafana-app-sdk v0.40.2 github.com/grafana/grafana-plugin-sdk-go v0.278.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/stretchr/testify v1.10.0 @@ -44,7 +44,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/grafana-app-sdk/logging v0.40.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.40.1 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 4f725783309..bb90120dc2b 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -94,10 +94,10 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/grafana-app-sdk v0.40.1 h1:W8d1CQSgMg/d37xf/0t4PiSa7wVD21XFE8mF6P59YSg= -github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM= -github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0H0OVhzzlOKd4Ehu+E= -github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana-app-sdk v0.40.2 h1:j2ftFuqhX+exYUipfEjeWDs3i7oiJkweTF8gFLL7wWU= +github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0= +github.com/grafana/grafana-app-sdk/logging v0.40.1 h1:ru+GqbaQk6jthA5l2Yo1WI/JbNXKNQmLiqNrxz7HGP4= +github.com/grafana/grafana-app-sdk/logging v0.40.1/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho= github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= diff --git a/apps/folder/go.mod b/apps/folder/go.mod index 31f8f6078a3..7736f202c1c 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/folder go 1.24.5 require ( - github.com/grafana/grafana-app-sdk v0.40.1 + github.com/grafana/grafana-app-sdk v0.40.2 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e k8s.io/apimachinery v0.33.3 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff @@ -23,7 +23,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect - github.com/grafana/grafana-app-sdk/logging v0.40.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.40.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/apps/folder/go.sum b/apps/folder/go.sum index 9707dfb4d4a..3612fc451ee 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -32,10 +32,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.40.1 h1:W8d1CQSgMg/d37xf/0t4PiSa7wVD21XFE8mF6P59YSg= -github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM= -github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0H0OVhzzlOKd4Ehu+E= -github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana-app-sdk v0.40.2 h1:j2ftFuqhX+exYUipfEjeWDs3i7oiJkweTF8gFLL7wWU= +github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0= +github.com/grafana/grafana-app-sdk/logging v0.40.1 h1:ru+GqbaQk6jthA5l2Yo1WI/JbNXKNQmLiqNrxz7HGP4= +github.com/grafana/grafana-app-sdk/logging v0.40.1/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 4b98e9c089f..30df8cdcd1f 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/iam go 1.24.5 require ( - github.com/grafana/grafana-app-sdk v0.40.1 + github.com/grafana/grafana-app-sdk v0.40.2 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e k8s.io/apimachinery v0.33.3 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff @@ -23,7 +23,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect - github.com/grafana/grafana-app-sdk/logging v0.40.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.40.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 9707dfb4d4a..3612fc451ee 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -32,10 +32,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.40.1 h1:W8d1CQSgMg/d37xf/0t4PiSa7wVD21XFE8mF6P59YSg= -github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM= -github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0H0OVhzzlOKd4Ehu+E= -github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana-app-sdk v0.40.2 h1:j2ftFuqhX+exYUipfEjeWDs3i7oiJkweTF8gFLL7wWU= +github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0= +github.com/grafana/grafana-app-sdk/logging v0.40.1 h1:ru+GqbaQk6jthA5l2Yo1WI/JbNXKNQmLiqNrxz7HGP4= +github.com/grafana/grafana-app-sdk/logging v0.40.1/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 33eb4e290ac..f1bb896a0ff 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -4,7 +4,7 @@ go 1.24.5 require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.40.1 + github.com/grafana/grafana-app-sdk v0.40.2 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250725152715-69d3b9023cec github.com/stretchr/testify v1.10.0 k8s.io/apimachinery v0.33.3 @@ -82,7 +82,7 @@ require ( github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 // indirect - github.com/grafana/grafana-app-sdk/logging v0.40.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.40.1 // indirect github.com/grafana/grafana-aws-sdk v1.0.4 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // indirect github.com/grafana/grafana-plugin-sdk-go v0.278.0 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 65357085011..b17f4ea40a5 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -341,18 +341,18 @@ 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-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg2svGyO40Ex92G02aOyHzP6XHCE= github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E= -github.com/grafana/grafana-app-sdk v0.40.1 h1:W8d1CQSgMg/d37xf/0t4PiSa7wVD21XFE8mF6P59YSg= -github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM= -github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0H0OVhzzlOKd4Ehu+E= -github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana-app-sdk v0.40.2 h1:j2ftFuqhX+exYUipfEjeWDs3i7oiJkweTF8gFLL7wWU= +github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0= +github.com/grafana/grafana-app-sdk/logging v0.40.1 h1:ru+GqbaQk6jthA5l2Yo1WI/JbNXKNQmLiqNrxz7HGP4= +github.com/grafana/grafana-app-sdk/logging v0.40.1/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana-aws-sdk v1.0.4 h1:D14UAehsOqpjliHmHzveRQ1p43KCsMzdmb7GovWj+SY= github.com/grafana/grafana-aws-sdk v1.0.4/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA= github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0/go.mod h1:H9sVh9A4yg5egMGZeh0mifxT1Q/uqwKe1LBjBJU6pN8= github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho= github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71 h1:7AVAgb0CalPYtUC9SJKYAkByOBGwL6r50P4QMEiJi44= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71/go.mod h1:/iuseD/cEpXDiy7MpL+4qBFZ3H6esnUJTYzpoJMw9dw= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730164619-34019e5ec017 h1:Niy+KRDWHsUVqfhZQg0oZbAQFO6QcO6a4l9V/ouDEEs= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730164619-34019e5ec017/go.mod h1:/iuseD/cEpXDiy7MpL+4qBFZ3H6esnUJTYzpoJMw9dw= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250725152715-69d3b9023cec h1:cg1GbDVZ7goqDrqoMzqeN4AeAcD271MGYjOvdVTDwfw= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250725152715-69d3b9023cec/go.mod h1:3ZgUe0E3rIhI026xF4DKFptOst/jpDHJ/Sn+bRODzI4= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250627191313-2f1a6ae1712b h1:QyJLJn3xwFTIXu9KPZujsrIUN0X8DdiR9b2h75L0AfI= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 2cf20613fc8..6a3f8d519fd 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/playlist go 1.24.5 require ( - github.com/grafana/grafana-app-sdk v0.40.1 + github.com/grafana/grafana-app-sdk v0.40.2 k8s.io/apimachinery v0.33.3 k8s.io/client-go v0.33.3 k8s.io/klog/v2 v2.130.1 @@ -30,7 +30,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.40.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.40.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index b061911f7b2..88512ac2ed8 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -47,10 +47,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.40.1 h1:W8d1CQSgMg/d37xf/0t4PiSa7wVD21XFE8mF6P59YSg= -github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM= -github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0H0OVhzzlOKd4Ehu+E= -github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana-app-sdk v0.40.2 h1:j2ftFuqhX+exYUipfEjeWDs3i7oiJkweTF8gFLL7wWU= +github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0= +github.com/grafana/grafana-app-sdk/logging v0.40.1 h1:ru+GqbaQk6jthA5l2Yo1WI/JbNXKNQmLiqNrxz7HGP4= +github.com/grafana/grafana-app-sdk/logging v0.40.1/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/apps/sdk.mk b/apps/sdk.mk index 093075f0be4..07105fe3973 100644 --- a/apps/sdk.mk +++ b/apps/sdk.mk @@ -1,4 +1,4 @@ -APP_SDK_VERSION = v0.40.1 +APP_SDK_VERSION = v0.40.2 APP_SDK_DIR = $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) APP_SDK_BIN = $(APP_SDK_DIR)/grafana-app-sdk diff --git a/apps/secret/go.mod b/apps/secret/go.mod index 7f0517a504a..45567c3943a 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/secret go 1.24.5 require ( - github.com/grafana/grafana-app-sdk v0.40.1 + github.com/grafana/grafana-app-sdk v0.40.2 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.10.0 google.golang.org/grpc v1.74.2 @@ -28,7 +28,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect - github.com/grafana/grafana-app-sdk/logging v0.40.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.40.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/apps/secret/go.sum b/apps/secret/go.sum index 78d210e14a9..3705f402c16 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -36,10 +36,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.40.1 h1:W8d1CQSgMg/d37xf/0t4PiSa7wVD21XFE8mF6P59YSg= -github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM= -github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0H0OVhzzlOKd4Ehu+E= -github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana-app-sdk v0.40.2 h1:j2ftFuqhX+exYUipfEjeWDs3i7oiJkweTF8gFLL7wWU= +github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0= +github.com/grafana/grafana-app-sdk/logging v0.40.1 h1:ru+GqbaQk6jthA5l2Yo1WI/JbNXKNQmLiqNrxz7HGP4= +github.com/grafana/grafana-app-sdk/logging v0.40.1/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf/go.mod h1:eAlOam2uWhrsEZlOoAr7XZ9hbBP7SyYGYn31/aQAPs8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/go.mod b/go.mod index 6d9b4e0d65b..27500ca7227 100644 --- a/go.mod +++ b/go.mod @@ -95,8 +95,8 @@ require ( github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend - github.com/grafana/grafana-app-sdk v0.40.1 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-app-sdk/logging v0.40.0 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk v0.40.2 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk/logging v0.40.1 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-aws-sdk v1.0.4 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad @@ -230,7 +230,7 @@ require ( require ( github.com/grafana/grafana/apps/advisor v0.0.0-20250627191313-2f1a6ae1712b // @grafana/plugins-platform-backend github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250627191313-2f1a6ae1712b // @grafana/alerting-backend - github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71 // @grafana/grafana-app-platform-squad @grafana/dashboards-squad + github.com/grafana/grafana/apps/dashboard v0.0.0-20250730164619-34019e5ec017 // @grafana/grafana-app-platform-squad @grafana/dashboards-squad github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b // @grafana/grafana-search-and-storage github.com/grafana/grafana/apps/iam v0.0.0-20250627191313-2f1a6ae1712b // @grafana/identity-access-team github.com/grafana/grafana/apps/investigations v0.0.0-20250627191313-2f1a6ae1712b // @fcjack @matryer diff --git a/go.sum b/go.sum index 2009fce4070..41353473ddc 100644 --- a/go.sum +++ b/go.sum @@ -1600,10 +1600,10 @@ github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= -github.com/grafana/grafana-app-sdk v0.40.1 h1:W8d1CQSgMg/d37xf/0t4PiSa7wVD21XFE8mF6P59YSg= -github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM= -github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0H0OVhzzlOKd4Ehu+E= -github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana-app-sdk v0.40.2 h1:j2ftFuqhX+exYUipfEjeWDs3i7oiJkweTF8gFLL7wWU= +github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0= +github.com/grafana/grafana-app-sdk/logging v0.40.1 h1:ru+GqbaQk6jthA5l2Yo1WI/JbNXKNQmLiqNrxz7HGP4= +github.com/grafana/grafana-app-sdk/logging v0.40.1/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana-aws-sdk v1.0.4 h1:D14UAehsOqpjliHmHzveRQ1p43KCsMzdmb7GovWj+SY= github.com/grafana/grafana-aws-sdk v1.0.4/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA= @@ -1620,8 +1620,8 @@ github.com/grafana/grafana/apps/advisor v0.0.0-20250627191313-2f1a6ae1712b h1:8o github.com/grafana/grafana/apps/advisor v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:q+h3HbmqU/PposW6lq8cMle1v8vuyX1LCMrGzbabHxc= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250627191313-2f1a6ae1712b h1:jr+C3epmjhd5Yyob4P1Z/dPaW4LRTkU5UJLXsI4eaeM= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:WpI7TCck4P2wKTO2WJLBRcfOWvUGvTdxYu3QqS3z7jM= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71 h1:7AVAgb0CalPYtUC9SJKYAkByOBGwL6r50P4QMEiJi44= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250730154715-ae15ba562e71/go.mod h1:/iuseD/cEpXDiy7MpL+4qBFZ3H6esnUJTYzpoJMw9dw= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730164619-34019e5ec017 h1:Niy+KRDWHsUVqfhZQg0oZbAQFO6QcO6a4l9V/ouDEEs= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250730164619-34019e5ec017/go.mod h1:/iuseD/cEpXDiy7MpL+4qBFZ3H6esnUJTYzpoJMw9dw= github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b h1:31MwoIKKT9Ay0ZjbT4lkfcPijiWogUWzXs2EjrCgodI= github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:dLtYBp1pza5HYalezNvzlP8JDeKrZ5BKTonDgEOE0NY= github.com/grafana/grafana/apps/iam v0.0.0-20250627191313-2f1a6ae1712b h1:NV8v9xdM/pzjjy+1cLqUseia3bYcvQGh88vZdMW/jA0= diff --git a/go.work.sum b/go.work.sum index cd47b184cb0..d129bd22b1f 100644 --- a/go.work.sum +++ b/go.work.sum @@ -16,6 +16,7 @@ cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= cloud.google.com/go v0.118.1/go.mod h1:CFO4UPEPi8oV21xoezZCrd3d81K4fFkDTEJu4R8K+9M= @@ -516,7 +517,9 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk= github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.1 h1:o/Ws6bEqMeKZUfj1RRm3mQ51O8JGU5w+Qdg2AhHib6A= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.1/go.mod h1:6QAMYBAbQeeKX+REFJMZ1nFWu9XLw/PPcjYpuc9RDFs= @@ -705,6 +708,7 @@ github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nC github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= 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= @@ -902,6 +906,7 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 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/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E= github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= @@ -964,12 +969,17 @@ 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/gomemcache v0.0.0-20250228145437-da7b95fd2ac1/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= +github.com/grafana/grafana-app-sdk v0.40.0/go.mod h1:fn943JEM0CR3mY/Gd3816MUcpob5xnKc8MoojnbMjYY= github.com/grafana/grafana-app-sdk/logging v0.38.0/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= github.com/grafana/grafana-app-sdk/logging v0.39.0 h1:3GgN5+dUZYqq74Q+GT9/ET+yo+V54zWQk/Q2/JsJQB4= github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= +github.com/grafana/grafana-app-sdk/logging v0.39.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana-aws-sdk v0.38.2 h1:TzQD0OpWsNjtldi5G5TLDlBRk8OyDf+B5ujcoAu4Dp0= github.com/grafana/grafana-aws-sdk v0.38.2/go.mod h1:j3vi+cXYHEFqjhBGrI6/lw1TNM+dl0Y3f0cSnDOPy+s= +github.com/grafana/grafana-aws-sdk v1.0.2 h1:98eBuHYFmgvH0xO9kKf4RBsEsgQRp8EOA/9yhDIpkss= +github.com/grafana/grafana-aws-sdk v1.0.2/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= +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/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana-plugin-sdk-go v0.269.1/go.mod h1:yv2KbO4mlr9WuDK2f+2gHAMTwwLmLuqaEnrPXTRU+OI= @@ -1051,7 +1061,6 @@ github.com/jaegertracing/jaeger v1.67.0 h1:t0BiJZVW9D3Z16y3uHqKzV9bKFTusooTH1Kgr github.com/jaegertracing/jaeger v1.67.0/go.mod h1:tE/FEQfybCSdUbBgel51YaCSkc58O+Njih8oTl6j8vw= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= -github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -1285,6 +1294,7 @@ github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc 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= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/relvacode/iso8601 v1.6.0 h1:eFXUhMJN3Gz8Rcq82f9DTMW0svjtAVuIEULglM7QHTU= github.com/relvacode/iso8601 v1.6.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/richardartoul/molecule v1.0.0 h1:+LFA9cT7fn8KF39zy4dhOnwcOwRoqKiBkPqKqya+8+U= @@ -1353,8 +1363,6 @@ github.com/testcontainers/testcontainers-go/modules/mongodb v0.35.0 h1:i1Kh9fmXg github.com/testcontainers/testcontainers-go/modules/mongodb v0.35.0/go.mod h1:SD8nVMK1m7b/K2YJqYjYNzfHmZfqHtqNOlI44nfxjdg= github.com/testcontainers/testcontainers-go/modules/redis v0.35.0 h1:RBgVefU5j5IWapp3TNKqMTYX+M22OSjtuORjPd4+g08= github.com/testcontainers/testcontainers-go/modules/redis v0.35.0/go.mod h1:UgghVXQ0//D3MjC8X71Bpb/lUCChidjNCRILD+btqfU= -github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM= -github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -1434,7 +1442,6 @@ github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1 h1:ixAiqjj2S/dNuJqrz4AxSqgw2P5OBM github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1/go.mod h1:l5sSv153E18VvYcsmr51hok9Sjc16tEC8AXGbwrk+ho= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= -github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -1451,6 +1458,7 @@ go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA= go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= go.opentelemetry.io/collector v0.124.0 h1:g/dfdGFhBcQI0ggGxTmGlJnJ6Yl6T2gVxQoIj4UfXCc= go.opentelemetry.io/collector v0.124.0/go.mod h1:QzERYfmHUedawjr8Ph/CBEEkVqWS8IlxRLAZt+KHlCg= go.opentelemetry.io/collector/client v1.30.0 h1:QbvOrvwUGcnVjnIBn2zyLLubisOjgh7kMgkzDAiYpHg= @@ -1709,6 +1717,7 @@ golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFK golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b h1:DU+gwOBXU+6bO0sEyO7o/NeMlxZxCZEvI7v+J4a1zRQ= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= @@ -1732,6 +1741,7 @@ golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= @@ -1806,7 +1816,7 @@ google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3i google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= @@ -1833,6 +1843,7 @@ honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34= honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= k8s.io/code-generator v0.33.1 h1:ZLzIRdMsh3Myfnx9BaooX6iQry29UJjVfVG+BuS+UMw= k8s.io/code-generator v0.33.1/go.mod h1:HUKT7Ubp6bOgIbbaPIs9lpd2Q02uqkMCMx9/GjDrWpY= k8s.io/code-generator v0.33.2 h1:PCJ0Y6viTCxxJHMOyGqYwWEteM4q6y1Hqo2rNpl6jF4= @@ -1869,3 +1880,4 @@ sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2Lfw sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 24e2c11626c..4efad4e627d 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -5,7 +5,7 @@ go 1.24.5 require ( github.com/google/go-cmp v0.7.0 github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 - github.com/grafana/grafana-app-sdk/logging v0.40.0 + github.com/grafana/grafana-app-sdk/logging v0.40.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.22.0 github.com/stretchr/testify v1.10.0 diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 5547406bf98..dc2e70d63a1 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -84,8 +84,8 @@ github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 h1:NlkGMnVi/ github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg2svGyO40Ex92G02aOyHzP6XHCE= github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E= -github.com/grafana/grafana-app-sdk/logging v0.40.0 h1:3LHA0pFM9mGGFyq6qoHvJOGEe0H0OVhzzlOKd4Ehu+E= -github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana-app-sdk/logging v0.40.1 h1:ru+GqbaQk6jthA5l2Yo1WI/JbNXKNQmLiqNrxz7HGP4= +github.com/grafana/grafana-app-sdk/logging v0.40.1/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= diff --git a/pkg/services/apiserver/appinstaller/installer.go b/pkg/services/apiserver/appinstaller/installer.go index d80e865b5f0..fb5427b761f 100644 --- a/pkg/services/apiserver/appinstaller/installer.go +++ b/pkg/services/apiserver/appinstaller/installer.go @@ -134,7 +134,6 @@ func InstallAPIs( builderMetrics *builder.BuilderMetrics, ) error { logger := logging.FromContext(ctx) - for _, installer := range appInstallers { logger.Debug("Installing APIs for app installer", "app", installer.ManifestData().AppName) wrapper := &serverWrapper{ diff --git a/pkg/services/apiserver/appinstaller/server.go b/pkg/services/apiserver/appinstaller/server.go index 723c0753605..ea9b88e031e 100644 --- a/pkg/services/apiserver/appinstaller/server.go +++ b/pkg/services/apiserver/appinstaller/server.go @@ -8,6 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + genericrest "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" @@ -38,17 +39,9 @@ type serverWrapper struct { func (s *serverWrapper) InstallAPIGroup(apiGroupInfo *genericapiserver.APIGroupInfo) error { log := logging.FromContext(s.ctx) - legacyProvider, ok := s.installer.(LegacyStorageProvider) - if !ok { - return s.GenericAPIServer.InstallAPIGroup(apiGroupInfo) - } for v, storageMap := range apiGroupInfo.VersionedResourcesStorageMap { for storagePath, restStorage := range storageMap { - genericStorage, ok := restStorage.(*genericregistry.Store) - if !ok { - log.Error("Expected generic registry store", "storagePath", storagePath, "version", v) - continue - } + legacyProvider, dualWriteSupported := s.installer.(LegacyStorageProvider) resource, err := getResourceFromStoragePath(storagePath) if err != nil { return err @@ -57,29 +50,28 @@ func (s *serverWrapper) InstallAPIGroup(apiGroupInfo *genericapiserver.APIGroupI Group: s.installer.ManifestData().Group, Resource: resource, } - genericStorage.KeyRootFunc = grafanaregistry.KeyRootFunc(gr) - genericStorage.KeyFunc = grafanaregistry.NamespaceKeyFunc(gr) - genericStorage.UpdateStrategy = &updateStrategyWrapper{ - RESTUpdateStrategy: genericStorage.UpdateStrategy, + storage := s.configureStorage(gr, dualWriteSupported, restStorage) + if unifiedStorage, ok := storage.(grafanarest.Storage); ok && dualWriteSupported { + log.Debug("Configuring dual writer for storage", "resource", gr.String(), "version", v, "storagePath", storagePath) + dw, err := NewDualWriter( + s.ctx, + gr, + s.storageOpts, + legacyProvider.GetLegacyStorage(gr.WithVersion(v)), + unifiedStorage, + s.kvStore, + s.lock, + s.namespaceMapper, + s.dualWriteService, + s.dualWriterMetrics, + s.builderMetrics, + ) + if err != nil { + return err + } + storage = dw } - - dw, err := NewDualWriter( - s.ctx, - gr, - s.storageOpts, - legacyProvider.GetLegacyStorage(gr.WithVersion(v)), - grafanarest.Storage(genericStorage), - s.kvStore, - s.lock, - s.namespaceMapper, - s.dualWriteService, - s.dualWriterMetrics, - s.builderMetrics, - ) - if err != nil { - return err - } - apiGroupInfo.VersionedResourcesStorageMap[v][storagePath] = dw + apiGroupInfo.VersionedResourcesStorageMap[v][storagePath] = storage } } @@ -93,3 +85,27 @@ func getResourceFromStoragePath(storagePath string) (string, error) { } return parts[0], nil } + +func (s *serverWrapper) configureStorage(gr schema.GroupResource, dualWriteSupported bool, storage genericrest.Storage) genericrest.Storage { + if gs, ok := storage.(*genericregistry.Store); ok { + // if dual write is supported, we need to modify the update strategy + // this is not needed for the status store + if dualWriteSupported { + gs.UpdateStrategy = &updateStrategyWrapper{ + RESTUpdateStrategy: gs.UpdateStrategy, + } + } + gs.KeyFunc = grafanaregistry.NamespaceKeyFunc(gr) + gs.KeyRootFunc = grafanaregistry.KeyRootFunc(gr) + return gs + } + + // if the storage is a status store, we need to extract the underlying generic registry store + if statusStore, ok := storage.(*appsdkapiserver.StatusREST); ok { + statusStore.Store.KeyFunc = grafanaregistry.NamespaceKeyFunc(gr) + statusStore.Store.KeyRootFunc = grafanaregistry.KeyRootFunc(gr) + return statusStore + } + + return storage +} From f1821988d9db9c04f8ae2e7a88350892f92d10e5 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 00:40:54 +0000 Subject: [PATCH 071/126] I18n: Download translations from Crowdin (#108954) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 45 +++++++++++++++++++---------- public/locales/de-DE/grafana.json | 45 +++++++++++++++++++---------- public/locales/es-ES/grafana.json | 45 +++++++++++++++++++---------- public/locales/fr-FR/grafana.json | 45 +++++++++++++++++++---------- public/locales/hu-HU/grafana.json | 45 +++++++++++++++++++---------- public/locales/id-ID/grafana.json | 45 +++++++++++++++++++---------- public/locales/it-IT/grafana.json | 45 +++++++++++++++++++---------- public/locales/ja-JP/grafana.json | 45 +++++++++++++++++++---------- public/locales/ko-KR/grafana.json | 45 +++++++++++++++++++---------- public/locales/nl-NL/grafana.json | 45 +++++++++++++++++++---------- public/locales/pl-PL/grafana.json | 45 +++++++++++++++++++---------- public/locales/pt-BR/grafana.json | 45 +++++++++++++++++++---------- public/locales/pt-PT/grafana.json | 45 +++++++++++++++++++---------- public/locales/ru-RU/grafana.json | 45 +++++++++++++++++++---------- public/locales/sv-SE/grafana.json | 45 +++++++++++++++++++---------- public/locales/tr-TR/grafana.json | 45 +++++++++++++++++++---------- public/locales/zh-Hans/grafana.json | 45 +++++++++++++++++++---------- public/locales/zh-Hant/grafana.json | 45 +++++++++++++++++++---------- 18 files changed, 540 insertions(+), 270 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 8bda1a81127..308c5ba4955 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -1560,8 +1560,7 @@ "group-not-editable": "Vybranou skupinu nelze upravit", "group-not-editable-description": "Tato skupina patří ke zdroji dat, který nepodporuje úpravy.", "page-title": "Upravit skupinu pravidel", - "rule-group-error": "Chyba při načítání skupiny pravidel", - "title": "Upravit hodnotící skupinu" + "rule-group-error": "Chyba při načítání skupiny pravidel" }, "group-loader": { "group-load-failed": "Nepodařilo se načíst pravidla ze skupiny {{ groupName }} v jmenném prostoru {{ namespaceName }}", @@ -1595,8 +1594,7 @@ "synthetic-monitoring-card-item-2": "Spusťte kontroly pingu, DNS, HTTP/S a TCP na každé síťové vrstvě.", "synthetic-monitoring-card-item-3": "Použijte více než 20 globálních nebo soukromých sond za firewallem.", "synthetic-monitoring-card-item-4": "Sledujte SLO pomocí vestavěných Prometheus výstrah přímo z uživatelského rozhraní.", - "synthetic-monitoring-card-title": "Syntetické monitorování", - "title-alerting": "Výstrahy" + "synthetic-monitoring-card-title": "Syntetické monitorování" }, "import-to-gma": { "action-button": "Importovat", @@ -3778,6 +3776,7 @@ "category-selected-connection": "Vybrané připojení", "category-selected-element": "Vybraný prvek ({{element}})", "category-size": "Velikost", + "category-tooltip": "", "cloud-item": { "category-cloud": "Cloud", "label": { @@ -4022,6 +4021,13 @@ "auto": "Automaticky" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Vymazat výběr", "frame-selection": "Výběr snímku", @@ -7050,6 +7056,9 @@ "pro-tip-define-sources-through-configuration-files": " Pro tip: Zdroje dat můžete definovat také prostřednictvím konfiguračních souborů. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Předch.", "queryless-apps-extensions": { "aria-label-go-queryless": "Vyhnout se dotazům" @@ -7316,18 +7325,24 @@ "split-tooltip": "Rozdělit podokno", "split-widen": "Rozšířit podokno" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Poskytnout zpětnou vazbu", - "label-copied": "Zkopírováno!", - "label-export": "Exportovat", - "label-trace-id": "ID trasování", - "title-share-thoughts-about-tracing-grafana": "Podělte se o svůj názor na trasování v Grafaně." - }, "trace-page-header": { - "text-partial-trace": "Částečné trasování", - "tooltip-url": "{{url}} nebo {{target}} nebo {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Tlačítko Vymazat filtry", diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index f3b222b2c46..c583a7bc4f0 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -1548,8 +1548,7 @@ "group-not-editable": "Die ausgewählte Gruppe kann nicht bearbeitet werden", "group-not-editable-description": "Diese Gruppe gehört zu einer Datenquelle, die keine Bearbeitung unterstützt.", "page-title": "Regelgruppe bearbeiten", - "rule-group-error": "Fehler beim Laden der Regelgruppe", - "title": "Evaluierungsgruppe bearbeiten" + "rule-group-error": "Fehler beim Laden der Regelgruppe" }, "group-loader": { "group-load-failed": "Die Regeln der Gruppe {{ groupName }} in {{ namespaceName }} konnten nicht geladen werden", @@ -1583,8 +1582,7 @@ "synthetic-monitoring-card-item-2": "Führen Sie auf jeder Netzwerkebene Ping-, DNS-, HTTP/S- und TCP-Prüfungen durch.", "synthetic-monitoring-card-item-3": "Nutzen Sie mehr als 20 globale Tests oder private Tests hinter Ihrer Firewall.", "synthetic-monitoring-card-item-4": "Verfolgen Sie SLOs mit integrierten Warnungen im Stil von Prometheus – direkt über die Benutzeroberfläche.", - "synthetic-monitoring-card-title": "Synthetisches Monitoring", - "title-alerting": "Meldungen" + "synthetic-monitoring-card-title": "Synthetisches Monitoring" }, "import-to-gma": { "action-button": "Importieren", @@ -3740,6 +3738,7 @@ "category-selected-connection": "Ausgewählte Verbindung", "category-selected-element": "Ausgewähltes Element ({{element}})", "category-size": "Größe", + "category-tooltip": "", "cloud-item": { "category-cloud": "Cloud", "label": { @@ -3984,6 +3983,13 @@ "auto": "Auto" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Auswahl löschen", "frame-selection": "Frame-Auswahl", @@ -7006,6 +7012,9 @@ "pro-tip-define-sources-through-configuration-files": " Profi-Tipp: Sie können Datenquellen auch über Konfigurationsdateien festlegen. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Zurück", "queryless-apps-extensions": { "aria-label-go-queryless": "Ohne Abfrage fortfahren" @@ -7272,18 +7281,24 @@ "split-tooltip": "Bereich teilen", "split-widen": "Bereich verbreitern" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Feedback geben", - "label-copied": "Kopiert!", - "label-export": "Exportieren", - "label-trace-id": "Trace-ID", - "title-share-thoughts-about-tracing-grafana": "Sagen Sie Ihre Meinung zum Tracing in Grafana." - }, "trace-page-header": { - "text-partial-trace": "Teilweises Trace", - "tooltip-url": "{{url}} oder {{target}} oder {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Schaltfläche „Filter löschen“", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index ab32ab5c265..dacff308cb7 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -1548,8 +1548,7 @@ "group-not-editable": "El grupo seleccionado no se puede editar", "group-not-editable-description": "Este grupo pertenece a una fuente de datos que no admite la edición.", "page-title": "Editar grupo de reglas", - "rule-group-error": "Error al cargar el grupo de reglas", - "title": "Editar grupo de evaluación" + "rule-group-error": "Error al cargar el grupo de reglas" }, "group-loader": { "group-load-failed": "Error al cargar las reglas del grupo {{ groupName }} en {{ namespaceName }}", @@ -1583,8 +1582,7 @@ "synthetic-monitoring-card-item-2": "", "synthetic-monitoring-card-item-3": "", "synthetic-monitoring-card-item-4": "", - "synthetic-monitoring-card-title": "", - "title-alerting": "Alertas" + "synthetic-monitoring-card-title": "" }, "import-to-gma": { "action-button": "Importar", @@ -3740,6 +3738,7 @@ "category-selected-connection": "", "category-selected-element": "", "category-size": "", + "category-tooltip": "", "cloud-item": { "category-cloud": "", "label": { @@ -3984,6 +3983,13 @@ "auto": "Auto" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Eliminar selección", "frame-selection": "Selección de marco", @@ -7006,6 +7012,9 @@ "pro-tip-define-sources-through-configuration-files": " Un consejo: también puedes definir fuentes de datos a través de archivos de configuración. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Ant.", "queryless-apps-extensions": { "aria-label-go-queryless": "Seguir sin consulta" @@ -7272,18 +7281,24 @@ "split-tooltip": "Dividir el panel", "split-widen": "Agrandar panel" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Enviar comentarios", - "label-copied": "¡Copiado!", - "label-export": "Exportar", - "label-trace-id": "ID de traza", - "title-share-thoughts-about-tracing-grafana": "Comparte tu opinión sobre el seguimiento en Grafana." - }, "trace-page-header": { - "text-partial-trace": "Traza parcial", - "tooltip-url": "{{url}} o {{target}} o {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Botón para borrar filtros", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 4a38e05d966..2485164635d 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -1548,8 +1548,7 @@ "group-not-editable": "Le groupe sélectionné ne peut pas être modifié", "group-not-editable-description": "Ce groupe appartient à une source de données qui ne prend pas en charge la modification.", "page-title": "Modifier le groupe de règles", - "rule-group-error": "Erreur lors du chargement du groupe de règles", - "title": "Modifier le groupe d’évaluation" + "rule-group-error": "Erreur lors du chargement du groupe de règles" }, "group-loader": { "group-load-failed": "Échec du chargement des règles du groupe {{ groupName }} dans {{ namespaceName }}", @@ -1583,8 +1582,7 @@ "synthetic-monitoring-card-item-2": "Effectuez des vérifications ping, DNS, HTTP/S et TCP à tous les niveaux du réseau.", "synthetic-monitoring-card-item-3": "Utilisez plus de 20 sondes mondiales ou déployez vos propres sondes privées derrière votre pare-feu.", "synthetic-monitoring-card-item-4": "Suivez vos SLO grâce à des alertes natives au format Prometheus, directement depuis l’interface utilisateur.", - "synthetic-monitoring-card-title": "Monitoring synthétique", - "title-alerting": "Alerte" + "synthetic-monitoring-card-title": "Monitoring synthétique" }, "import-to-gma": { "action-button": "Importer", @@ -3740,6 +3738,7 @@ "category-selected-connection": "Connexion sélectionnée", "category-selected-element": "Élément sélectionné ({{element}})", "category-size": "Taille", + "category-tooltip": "", "cloud-item": { "category-cloud": "Cloud", "label": { @@ -3984,6 +3983,13 @@ "auto": "Auto" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Effacer la sélection", "frame-selection": "Sélection de trame", @@ -7006,6 +7012,9 @@ "pro-tip-define-sources-through-configuration-files": " Conseil de pro : vous pouvez également définir des sources de données via des fichiers de configuration. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Précédent", "queryless-apps-extensions": { "aria-label-go-queryless": "Passer en mode sans requête" @@ -7272,18 +7281,24 @@ "split-tooltip": "Scinder le panneau", "split-widen": "Élargir le panneau" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Publiez votre commentaire", - "label-copied": "Copié !", - "label-export": "Exporter", - "label-trace-id": "ID de trace", - "title-share-thoughts-about-tracing-grafana": "Veuillez partager vos réflexions sur les traces dans Grafana." - }, "trace-page-header": { - "text-partial-trace": "Trace partielle", - "tooltip-url": "{{url}} ou {{target}} ou {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Bouton Supprimer les filtres", diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 94b862a9048..5e9f8acb6d0 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -1548,8 +1548,7 @@ "group-not-editable": "A kijelölt csoport nem szerkeszthető", "group-not-editable-description": "Ez a csoport olyan adatforráshoz tartozik, amely nem támogatja a szerkesztést.", "page-title": "Szabálycsoport szerkesztése", - "rule-group-error": "Hiba történt a szabálycsoport betöltésekor", - "title": "Értékelési csoport szerkesztése" + "rule-group-error": "Hiba történt a szabálycsoport betöltésekor" }, "group-loader": { "group-load-failed": "A szabályok betöltése a(z) {{ groupName }} csoportból nem sikerült a(z) {{ namespaceName }} névtérben", @@ -1583,8 +1582,7 @@ "synthetic-monitoring-card-item-2": "Futtasson pingelést, DNS-, HTTP/S- és TCP-ellenőrzéseket minden hálózati rétegben.", "synthetic-monitoring-card-item-3": "Használhat több mint 20 globális szondát vagy privát szondát a tűzfal mögött.", "synthetic-monitoring-card-item-4": "Kövesse nyomon az SLO-kat beépített Prometheus-stílusú riasztásokkal – közvetlenül a felhasználói felületről.", - "synthetic-monitoring-card-title": "Szintetikus megfigyelés", - "title-alerting": "Riasztás" + "synthetic-monitoring-card-title": "Szintetikus megfigyelés" }, "import-to-gma": { "action-button": "Importálás", @@ -3740,6 +3738,7 @@ "category-selected-connection": "Kiválasztott kapcsolat", "category-selected-element": "Kiválasztott elem ({{element}})", "category-size": "Méret", + "category-tooltip": "", "cloud-item": { "category-cloud": "Felhő", "label": { @@ -3984,6 +3983,13 @@ "auto": "Automatikus" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Kijelölés törlése", "frame-selection": "Keret kiválasztása", @@ -7006,6 +7012,9 @@ "pro-tip-define-sources-through-configuration-files": " Tipp: Adatforrásokat konfigurációs fájlokon keresztül is definiálhat. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Előző", "queryless-apps-extensions": { "aria-label-go-queryless": "Lekérdezés nélkül" @@ -7272,18 +7281,24 @@ "split-tooltip": "Ablaktábla felosztása", "split-widen": "Ablaktábla szélesítése" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Visszajelzés küldése", - "label-copied": "Kimásolva!", - "label-export": "Exportálás", - "label-trace-id": "Nyomkövetési azonosító", - "title-share-thoughts-about-tracing-grafana": "Ossza meg gondolatait a Grafanában elérhető nyomon követésről." - }, "trace-page-header": { - "text-partial-trace": "Részleges nyomvonal", - "tooltip-url": "{{url}}, vagy {{target}}, vagy {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Szűrők törlése gomb", diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 52e1b7bd2f0..ee7e3db48e3 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -1542,8 +1542,7 @@ "group-not-editable": "Grup yang dipilih tidak dapat diedit", "group-not-editable-description": "Grup ini termasuk dalam sumber data yang tidak mendukung pengeditan.", "page-title": "Edit grup aturan", - "rule-group-error": "Kesalahan saat memuat grup aturan", - "title": "Edit grup evaluasi" + "rule-group-error": "Kesalahan saat memuat grup aturan" }, "group-loader": { "group-load-failed": "Gagal memuat aturan dari grup {{ groupName }} di {{ namespaceName }}", @@ -1577,8 +1576,7 @@ "synthetic-monitoring-card-item-2": "Jalankan pemeriksaan ping, DNS, HTTP/S, dan TCP di setiap layer jaringan.", "synthetic-monitoring-card-item-3": "Gunakan 20+ probe global atau probe pribadi di belakang firewall Anda.", "synthetic-monitoring-card-item-4": "Lacak SLO dengan peringatan yang mirip cara kerja Prometheus bawaan—langsung dari UI.", - "synthetic-monitoring-card-title": "Pemantauan Sintetis", - "title-alerting": "Alerting" + "synthetic-monitoring-card-title": "Pemantauan Sintetis" }, "import-to-gma": { "action-button": "Impor", @@ -3721,6 +3719,7 @@ "category-selected-connection": "Koneksi yang dipilih", "category-selected-element": "Elemen yang dipilih ({{element}})", "category-size": "Ukuran", + "category-tooltip": "", "cloud-item": { "category-cloud": "Cloud", "label": { @@ -3965,6 +3964,13 @@ "auto": "Auto" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Hapus pilihan", "frame-selection": "Pilihan frame", @@ -6984,6 +6990,9 @@ "pro-tip-define-sources-through-configuration-files": " ProTip: Anda juga dapat menentukan sumber data melalui file konfigurasi. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Sebelumnya", "queryless-apps-extensions": { "aria-label-go-queryless": "Buka tanpa kueri" @@ -7250,18 +7259,24 @@ "split-tooltip": "Pisahkan panel", "split-widen": "Lebarkan panel" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Berikan umpan balik", - "label-copied": "Disalin!", - "label-export": "Ekspor", - "label-trace-id": "ID Pelacakan", - "title-share-thoughts-about-tracing-grafana": "Bagikan pendapat Anda tentang pelacakan di Grafana." - }, "trace-page-header": { - "text-partial-trace": "Jejak parsial", - "tooltip-url": "{{url}} atau {{target}} atau {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Tombol hapus filter", diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index cb88ba60b9b..b9280ddbc9f 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -1548,8 +1548,7 @@ "group-not-editable": "Il gruppo selezionato non può essere modificato", "group-not-editable-description": "Questo gruppo appartiene a un'origine dati che non supporta la modifica.", "page-title": "Modifica gruppo di regole", - "rule-group-error": "Errore durante il caricamento del gruppo di regole", - "title": "Modifica il gruppo di valutazione" + "rule-group-error": "Errore durante il caricamento del gruppo di regole" }, "group-loader": { "group-load-failed": "Impossibile caricare le regole dal gruppo {{ groupName }} in {{ namespaceName }}", @@ -1583,8 +1582,7 @@ "synthetic-monitoring-card-item-2": "", "synthetic-monitoring-card-item-3": "", "synthetic-monitoring-card-item-4": "", - "synthetic-monitoring-card-title": "", - "title-alerting": "Avvisi" + "synthetic-monitoring-card-title": "" }, "import-to-gma": { "action-button": "Importa", @@ -3740,6 +3738,7 @@ "category-selected-connection": "", "category-selected-element": "", "category-size": "", + "category-tooltip": "", "cloud-item": { "category-cloud": "", "label": { @@ -3984,6 +3983,13 @@ "auto": "Automatico" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Cancella selezione", "frame-selection": "", @@ -7006,6 +7012,9 @@ "pro-tip-define-sources-through-configuration-files": " Suggerimento: è inoltre possibile definire le origini dei dati tramite i file di configurazione. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Prec.", "queryless-apps-extensions": { "aria-label-go-queryless": "Passa a senza query" @@ -7272,18 +7281,24 @@ "split-tooltip": "Dividi il riquadro", "split-widen": "Allarga riquadro" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Lascia un feedback", - "label-copied": "Copiato!", - "label-export": "Esporta", - "label-trace-id": "ID traccia", - "title-share-thoughts-about-tracing-grafana": "Condividi i tuoi pensieri sul tracciamento in Grafana." - }, "trace-page-header": { - "text-partial-trace": "Traccia parziale", - "tooltip-url": "{{url}} o {{target}} o {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Tasto Cancella filtri", diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index c20aa149d2f..9d43cef2cfb 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -1542,8 +1542,7 @@ "group-not-editable": "選択したグループは編集できません", "group-not-editable-description": "このグループは、編集をサポートしていないデータソースに属しています。", "page-title": "ルールグループを編集", - "rule-group-error": "ルールグループの読み込み中にエラーが発生しました", - "title": "評価グループを編集" + "rule-group-error": "ルールグループの読み込み中にエラーが発生しました" }, "group-loader": { "group-load-failed": "{{ namespaceName }}の{{ groupName }}グループからルールを読み込めませんでした", @@ -1577,8 +1576,7 @@ "synthetic-monitoring-card-item-2": "全ネットワーク層でping、DNS、HTTP/S、TCPチェックを実行します。", "synthetic-monitoring-card-item-3": "20以上のグローバルプローブまたはファイアウォール内のプライベートプローブを使用します。", "synthetic-monitoring-card-item-4": "UIから直接、組み込みのPrometheusスタイルアラートでSLOを追跡します。", - "synthetic-monitoring-card-title": "合成モニタリング", - "title-alerting": "アラート" + "synthetic-monitoring-card-title": "合成モニタリング" }, "import-to-gma": { "action-button": "インポート", @@ -3721,6 +3719,7 @@ "category-selected-connection": "選択した接続", "category-selected-element": "選択した要素({{element}})", "category-size": "サイズ", + "category-tooltip": "", "cloud-item": { "category-cloud": "クラウド", "label": { @@ -3965,6 +3964,13 @@ "auto": "自動" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "選択をクリア", "frame-selection": "フレームの選択", @@ -6984,6 +6990,9 @@ "pro-tip-define-sources-through-configuration-files": "豆知識:設定ファイルでもデータソースを定義できます。" } }, + "pane": { + "loading-placeholder": "" + }, "prev": "前へ", "queryless-apps-extensions": { "aria-label-go-queryless": "クエリなしにする" @@ -7250,18 +7259,24 @@ "split-tooltip": "ペインを分割する", "split-widen": "ペインを広げる" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "フィードバックを送信", - "label-copied": "コピーしました!", - "label-export": "エクスポート", - "label-trace-id": "トレースID", - "title-share-thoughts-about-tracing-grafana": "Grafanaでのトレースについてご意見をお聞かせください。" - }, "trace-page-header": { - "text-partial-trace": "部分トレース", - "tooltip-url": "{{url}}、{{target}}または{{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "フィルターのクリアボタン", diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index f105ff1a09c..06e3e47560b 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -1542,8 +1542,7 @@ "group-not-editable": "선택한 그룹은 편집할 수 없습니다.", "group-not-editable-description": "이 그룹은 편집을 지원하지 않는 데이터 소스에 속해 있습니다.", "page-title": "규칙 그룹 편집", - "rule-group-error": "규칙 그룹 로딩 중 오류 발생", - "title": "평가 그룹 편집" + "rule-group-error": "규칙 그룹 로딩 중 오류 발생" }, "group-loader": { "group-load-failed": "{{ namespaceName }}의 {{ groupName }} 그룹에서 규칙 로딩 실패", @@ -1577,8 +1576,7 @@ "synthetic-monitoring-card-item-2": "", "synthetic-monitoring-card-item-3": "", "synthetic-monitoring-card-item-4": "", - "synthetic-monitoring-card-title": "", - "title-alerting": "경고" + "synthetic-monitoring-card-title": "" }, "import-to-gma": { "action-button": "가져오기", @@ -3721,6 +3719,7 @@ "category-selected-connection": "", "category-selected-element": "", "category-size": "", + "category-tooltip": "", "cloud-item": { "category-cloud": "", "label": { @@ -3965,6 +3964,13 @@ "auto": "자동" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "선택 초기화", "frame-selection": "프레임 선택", @@ -6984,6 +6990,9 @@ "pro-tip-define-sources-through-configuration-files": " 전문가 팁: 구성 파일을 통해 데이터 소스를 정의할 수도 있습니다. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "이전", "queryless-apps-extensions": { "aria-label-go-queryless": "쿼리 없이 진행" @@ -7250,18 +7259,24 @@ "split-tooltip": "창 분할", "split-widen": "창 확대" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "피드백 제출하기", - "label-copied": "복사됨!", - "label-export": "내보내기", - "label-trace-id": "트레이스 ID", - "title-share-thoughts-about-tracing-grafana": "Grafana에서 트레이싱에 대한 의견을 공유하세요." - }, "trace-page-header": { - "text-partial-trace": "부분 트레이스", - "tooltip-url": "{{url}} 또는 {{target}} 또는 {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "필터 지우기 버튼", diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 13dea31f267..c1603266c7d 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -1548,8 +1548,7 @@ "group-not-editable": "Geselecteerde groep kan niet worden bewerkt", "group-not-editable-description": "Deze groep behoort tot een gegevensbron die geen bewerking ondersteunt.", "page-title": "Regelgroep bewerken", - "rule-group-error": "Er is een fout opgetreden bij het laden van de regelgroep", - "title": "Evaluatiegroep bewerken" + "rule-group-error": "Er is een fout opgetreden bij het laden van de regelgroep" }, "group-loader": { "group-load-failed": "Kan regels uit groep {{ groupName }} in {{ namespaceName }} niet laden", @@ -1583,8 +1582,7 @@ "synthetic-monitoring-card-item-2": "Voer ping-, DNS-, HTTP/S- en TCP-checks uit op elk netwerklaag.", "synthetic-monitoring-card-item-3": "Gebruik meer dan 20 wereldwijde probes of eigen probes achter je firewall.", "synthetic-monitoring-card-item-4": "Volg SLO’s met ingebouwde waarschuwingen in Prometheus-stijl, direct vanuit de UI.", - "synthetic-monitoring-card-title": "Synthetische monitoring", - "title-alerting": "Alerting" + "synthetic-monitoring-card-title": "Synthetische monitoring" }, "import-to-gma": { "action-button": "Importeren", @@ -3740,6 +3738,7 @@ "category-selected-connection": "Geselecteerde verbinding", "category-selected-element": "Geselecteerd element ({{element}})", "category-size": "Grootte", + "category-tooltip": "", "cloud-item": { "category-cloud": "Cloud", "label": { @@ -3984,6 +3983,13 @@ "auto": "Automatisch" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Selectie wissen", "frame-selection": "Frameselectie", @@ -7006,6 +7012,9 @@ "pro-tip-define-sources-through-configuration-files": "ProTip: je kunt ook gegevensbronnen definiëren via configuratiebestanden. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Vorige", "queryless-apps-extensions": { "aria-label-go-queryless": "Zonder query's" @@ -7272,18 +7281,24 @@ "split-tooltip": "Deelvenster splitsen", "split-widen": "Deelvenster verbreden" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Feedback geven", - "label-copied": "Gekopieerd.", - "label-export": "Exporteren", - "label-trace-id": "Tracering-id", - "title-share-thoughts-about-tracing-grafana": "Deel je mening over tracing in Grafana." - }, "trace-page-header": { - "text-partial-trace": "Gedeeltelijk spoor", - "tooltip-url": "{{url}} of {{target}} of {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Knop Filters wissen", diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 42b4fbd5c2b..94f57f04483 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -1560,8 +1560,7 @@ "group-not-editable": "Nie można edytować wybranej grupy", "group-not-editable-description": "Ta grupa należy do źródła danych, które nie obsługuje edycji.", "page-title": "Edytuj grupę reguł", - "rule-group-error": "Błąd podczas ładowania grupy reguł", - "title": "Edytuj grupę ocen" + "rule-group-error": "Błąd podczas ładowania grupy reguł" }, "group-loader": { "group-load-failed": "Nie udało się wczytać reguł z grupy {{ groupName }} w przestrzeni nazw {{ namespaceName }}", @@ -1595,8 +1594,7 @@ "synthetic-monitoring-card-item-2": "", "synthetic-monitoring-card-item-3": "", "synthetic-monitoring-card-item-4": "", - "synthetic-monitoring-card-title": "", - "title-alerting": "Alerty" + "synthetic-monitoring-card-title": "" }, "import-to-gma": { "action-button": "Importuj", @@ -3778,6 +3776,7 @@ "category-selected-connection": "", "category-selected-element": "", "category-size": "", + "category-tooltip": "", "cloud-item": { "category-cloud": "", "label": { @@ -4022,6 +4021,13 @@ "auto": "Automatycznie" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Usuń zaznaczone", "frame-selection": "Wybór ramki", @@ -7050,6 +7056,9 @@ "pro-tip-define-sources-through-configuration-files": " Wskazówka: źródła danych można też definiować za pomocą plików konfiguracyjnych. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Wstecz", "queryless-apps-extensions": { "aria-label-go-queryless": "Bez zapytań" @@ -7316,18 +7325,24 @@ "split-tooltip": "Podziel okienko", "split-widen": "Poszerz okno" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Przekaż opinię", - "label-copied": "Skopiowano", - "label-export": "Eksport", - "label-trace-id": "Identyfikator śledzenia", - "title-share-thoughts-about-tracing-grafana": "Podziel się swoimi przemyśleniami na temat śledzenia w usłudze Grafana." - }, "trace-page-header": { - "text-partial-trace": "Częściowy ślad", - "tooltip-url": "{{url}}, {{target}} lub {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Przycisk Wyczyść filtry", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 0fa35e4fbf5..4515bb35b80 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -1548,8 +1548,7 @@ "group-not-editable": "O grupo selecionado não pode ser editado", "group-not-editable-description": "Este grupo pertence a uma fonte de dados que não é compatível com edição.", "page-title": "Editar grupo de regras", - "rule-group-error": "Erro ao carregar o grupo de regras", - "title": "Editar grupo de avaliação" + "rule-group-error": "Erro ao carregar o grupo de regras" }, "group-loader": { "group-load-failed": "Falha ao carregar regras do grupo {{ groupName }} em {{ namespaceName }}", @@ -1583,8 +1582,7 @@ "synthetic-monitoring-card-item-2": "", "synthetic-monitoring-card-item-3": "", "synthetic-monitoring-card-item-4": "", - "synthetic-monitoring-card-title": "", - "title-alerting": "Alertas" + "synthetic-monitoring-card-title": "" }, "import-to-gma": { "action-button": "Importar", @@ -3740,6 +3738,7 @@ "category-selected-connection": "", "category-selected-element": "", "category-size": "", + "category-tooltip": "", "cloud-item": { "category-cloud": "", "label": { @@ -3984,6 +3983,13 @@ "auto": "Automático" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Limpar seleção", "frame-selection": "Seleção de quadro", @@ -7006,6 +7012,9 @@ "pro-tip-define-sources-through-configuration-files": " Dica: você também pode definir fontes de dados por meio de arquivos de configuração. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Anterior", "queryless-apps-extensions": { "aria-label-go-queryless": "Continuar sem consulta" @@ -7272,18 +7281,24 @@ "split-tooltip": "Dividir o painel", "split-widen": "Painel amplo" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Dar feedback", - "label-copied": "Copiado!", - "label-export": "Exportar", - "label-trace-id": "ID do rastro", - "title-share-thoughts-about-tracing-grafana": "Compartilhe sua opinião sobre os traços na Grafana." - }, "trace-page-header": { - "text-partial-trace": "Rastro parcial", - "tooltip-url": "{{url}} ou {{target}} ou {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Botão para limpar filtros", diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index e24b5589e56..fd87d26a16b 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -1548,8 +1548,7 @@ "group-not-editable": "O grupo selecionado não pode ser editado", "group-not-editable-description": "Este grupo pertence a uma origem de dados que não suporta edição.", "page-title": "Editar grupo de regras", - "rule-group-error": "Erro ao carregar grupo de regras", - "title": "Editar grupo de avaliação" + "rule-group-error": "Erro ao carregar grupo de regras" }, "group-loader": { "group-load-failed": "Falha ao carregar regras do grupo {{ groupName }} em {{ namespaceName }}", @@ -1583,8 +1582,7 @@ "synthetic-monitoring-card-item-2": "", "synthetic-monitoring-card-item-3": "", "synthetic-monitoring-card-item-4": "", - "synthetic-monitoring-card-title": "", - "title-alerting": "Alerta" + "synthetic-monitoring-card-title": "" }, "import-to-gma": { "action-button": "Importar", @@ -3740,6 +3738,7 @@ "category-selected-connection": "", "category-selected-element": "", "category-size": "", + "category-tooltip": "", "cloud-item": { "category-cloud": "", "label": { @@ -3984,6 +3983,13 @@ "auto": "Automóvel" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Limpar a seleção", "frame-selection": "Seleção de moldura", @@ -7006,6 +7012,9 @@ "pro-tip-define-sources-through-configuration-files": "Dica: também pode definir origens de dados através de ficheiros de configuração. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Anterior", "queryless-apps-extensions": { "aria-label-go-queryless": "Sem consulta" @@ -7272,18 +7281,24 @@ "split-tooltip": "Dividir o painel", "split-widen": "Alargar painel" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Dar feedback", - "label-copied": "Copiado!", - "label-export": "Exportar", - "label-trace-id": "ID trace", - "title-share-thoughts-about-tracing-grafana": "Partilhe a sua opinião sobre o rastreamento na Grafana." - }, "trace-page-header": { - "text-partial-trace": "Trace parcial", - "tooltip-url": "{{url}} ou {{target}} ou {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Botão para limpar filtros", diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index ca39347f187..01ef6fe7de1 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -1560,8 +1560,7 @@ "group-not-editable": "Выбранную группу нельзя редактировать", "group-not-editable-description": "Эта группа принадлежит к источнику данных, который не поддерживает редактирование.", "page-title": "Редактирование группы правил", - "rule-group-error": "Ошибка при загрузке группы правил", - "title": "Редактирование группы оценки" + "rule-group-error": "Ошибка при загрузке группы правил" }, "group-loader": { "group-load-failed": "Не удалось загрузить правила из группы {{ groupName }} в {{ namespaceName }}", @@ -1595,8 +1594,7 @@ "synthetic-monitoring-card-item-2": "", "synthetic-monitoring-card-item-3": "", "synthetic-monitoring-card-item-4": "", - "synthetic-monitoring-card-title": "", - "title-alerting": "Оповещение" + "synthetic-monitoring-card-title": "" }, "import-to-gma": { "action-button": "Импорт", @@ -3778,6 +3776,7 @@ "category-selected-connection": "", "category-selected-element": "", "category-size": "", + "category-tooltip": "", "cloud-item": { "category-cloud": "", "label": { @@ -4022,6 +4021,13 @@ "auto": "Авто" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Очистить выбор", "frame-selection": "Выбор фрейма", @@ -7050,6 +7056,9 @@ "pro-tip-define-sources-through-configuration-files": "Совет. Источники данных можно также задавать в файлах конфигурации. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Назад", "queryless-apps-extensions": { "aria-label-go-queryless": "Не выполнять запрос" @@ -7316,18 +7325,24 @@ "split-tooltip": "Разделить область", "split-widen": "Расширить область" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Отправить отзыв", - "label-copied": "Скопировано", - "label-export": "Экспорт", - "label-trace-id": "Идентификатор трассировки", - "title-share-thoughts-about-tracing-grafana": "Ваше мнение о трассировке в Grafana" - }, "trace-page-header": { - "text-partial-trace": "Частичная трассировка", - "tooltip-url": "{{url}} или {{target}}, или {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Кнопка очистки фильтров", diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 357d3900787..c25f9d8d19b 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -1548,8 +1548,7 @@ "group-not-editable": "Vald grupp kan inte redigeras", "group-not-editable-description": "Den här gruppen tillhör en datakälla som inte stöder redigering.", "page-title": "Redigera regelgrupp", - "rule-group-error": "Fel vid laddning av regelgrupp", - "title": "Redigera utvärderingsgrupp" + "rule-group-error": "Fel vid laddning av regelgrupp" }, "group-loader": { "group-load-failed": "Det gick inte att ladda reglerna från gruppen {{ groupName }} i {{ namespaceName }}", @@ -1583,8 +1582,7 @@ "synthetic-monitoring-card-item-2": "", "synthetic-monitoring-card-item-3": "", "synthetic-monitoring-card-item-4": "", - "synthetic-monitoring-card-title": "", - "title-alerting": "Varnar" + "synthetic-monitoring-card-title": "" }, "import-to-gma": { "action-button": "Importera", @@ -3740,6 +3738,7 @@ "category-selected-connection": "", "category-selected-element": "", "category-size": "", + "category-tooltip": "", "cloud-item": { "category-cloud": "", "label": { @@ -3984,6 +3983,13 @@ "auto": "Auto" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Rensa val", "frame-selection": "Ramval", @@ -7006,6 +7012,9 @@ "pro-tip-define-sources-through-configuration-files": " Tips: Du kan även definiera datakällor via konfigurationsfiler. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Tidigare", "queryless-apps-extensions": { "aria-label-go-queryless": "Kör utan frågor" @@ -7272,18 +7281,24 @@ "split-tooltip": "Dela rutan", "split-widen": "Bredda rutan" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Ge feedback", - "label-copied": "Kopierad! ", - "label-export": "Exportera", - "label-trace-id": "Spårnings-ID", - "title-share-thoughts-about-tracing-grafana": "Dela dina tankar om att spåra i Grafana." - }, "trace-page-header": { - "text-partial-trace": "Partiell spårning", - "tooltip-url": "{{url}} eller {{target}} eller {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Knappen Rensa filter", diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index cbcae4254be..ae7289f7817 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -1548,8 +1548,7 @@ "group-not-editable": "Seçilen grup düzenlenemez", "group-not-editable-description": "Bu grup, düzenlemeyi desteklemeyen bir veri kaynağına aittir.", "page-title": "Kural grubunu düzenle", - "rule-group-error": "Kural grubu yüklenirken hata oluştu", - "title": "Değerlendirme grubunu düzenle" + "rule-group-error": "Kural grubu yüklenirken hata oluştu" }, "group-loader": { "group-load-failed": "{{ namespaceName }} alanındaki {{ groupName }} grubundan kurallar yüklenemedi", @@ -1583,8 +1582,7 @@ "synthetic-monitoring-card-item-2": "", "synthetic-monitoring-card-item-3": "", "synthetic-monitoring-card-item-4": "", - "synthetic-monitoring-card-title": "", - "title-alerting": "Uyarı sistemi" + "synthetic-monitoring-card-title": "" }, "import-to-gma": { "action-button": "İçe aktar", @@ -3740,6 +3738,7 @@ "category-selected-connection": "", "category-selected-element": "", "category-size": "", + "category-tooltip": "", "cloud-item": { "category-cloud": "", "label": { @@ -3984,6 +3983,13 @@ "auto": "Otomatik" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "Seçimi temizle", "frame-selection": "Çerçeve seçimi", @@ -7006,6 +7012,9 @@ "pro-tip-define-sources-through-configuration-files": "İpucu: Veri kaynaklarını yapılandırma dosyaları aracılığıyla da tanımlayabilirsiniz. " } }, + "pane": { + "loading-placeholder": "" + }, "prev": "Önceki", "queryless-apps-extensions": { "aria-label-go-queryless": "Sorgu olmadan devam et" @@ -7272,18 +7281,24 @@ "split-tooltip": "Bölmeyi böl", "split-widen": "Bölmeyi genişlet" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "Geri bildirim gönder", - "label-copied": "Kopyalandı!", - "label-export": "Dışa aktar", - "label-trace-id": "İzleme kimliği", - "title-share-thoughts-about-tracing-grafana": "Grafana'daki izleme deneyiminiz hakkındaki görüşlerinizi paylaşın." - }, "trace-page-header": { - "text-partial-trace": "Kısmi izleme", - "tooltip-url": "{{url}} veya {{target}} veya {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "Filtreleri temizle düğmesi", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index e4fa606b72d..6a0e7daee75 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -1542,8 +1542,7 @@ "group-not-editable": "无法编辑所选组", "group-not-editable-description": "此组所属的数据源不支持编辑。", "page-title": "编辑规则组", - "rule-group-error": "加载规则组时出错", - "title": "编辑评估组" + "rule-group-error": "加载规则组时出错" }, "group-loader": { "group-load-failed": "从 {{ namespaceName }} 的组 {{ groupName }} 中加载规则失败", @@ -1577,8 +1576,7 @@ "synthetic-monitoring-card-item-2": "在每个网络层运行 ping、DNS、HTTP/S 和 TCP 检查。", "synthetic-monitoring-card-item-3": "使用 20 多个全球探测点,或部署在防火墙后的私有探测点。", "synthetic-monitoring-card-item-4": "使用内置的 Prometheus 风格的警报功能,直接从用户界面跟踪 SLO。", - "synthetic-monitoring-card-title": "综合监测", - "title-alerting": "警报" + "synthetic-monitoring-card-title": "综合监测" }, "import-to-gma": { "action-button": "导入", @@ -3721,6 +3719,7 @@ "category-selected-connection": "所选连接", "category-selected-element": "所选元素 ({{element}})", "category-size": "大小", + "category-tooltip": "", "cloud-item": { "category-cloud": "Cloud", "label": { @@ -3965,6 +3964,13 @@ "auto": "自动" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "清除选择", "frame-selection": "帧选择", @@ -6984,6 +6990,9 @@ "pro-tip-define-sources-through-configuration-files": "专业提示:您还可以通过配置文件定义数据源。" } }, + "pane": { + "loading-placeholder": "" + }, "prev": "上一步", "queryless-apps-extensions": { "aria-label-go-queryless": "转为无查询" @@ -7250,18 +7259,24 @@ "split-tooltip": "拆分窗格", "split-widen": "宽窗格" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "提供反馈", - "label-copied": "已复制!", - "label-export": "导出", - "label-trace-id": "跟踪 ID", - "title-share-thoughts-about-tracing-grafana": "分享您对 Grafana 跟踪功能的看法。" - }, "trace-page-header": { - "text-partial-trace": "部分跟踪", - "tooltip-url": "{{url}} 或 {{target}} 或 {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "清除筛选按钮", diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index a73bab83765..13ff7bc7f60 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -1542,8 +1542,7 @@ "group-not-editable": "無法編輯所選群組", "group-not-editable-description": "此群組屬於不支援編輯的資料來源。", "page-title": "編輯規則群組", - "rule-group-error": "載入規則群組時發生錯誤", - "title": "編輯評估群組" + "rule-group-error": "載入規則群組時發生錯誤" }, "group-loader": { "group-load-failed": "無法從「{{ namespaceName }}」中的群組「{{ groupName }}」載入規則", @@ -1577,8 +1576,7 @@ "synthetic-monitoring-card-item-2": "在每個網路層執行 ping、DNS、HTTP/S 和 TCP 檢查。", "synthetic-monitoring-card-item-3": "在防火牆後使用 20 多個全球探測器或私人探測器。", "synthetic-monitoring-card-item-4": "使用內建的 Prometheus 風格警報追蹤 SLO - 直接從使用者介面操作。", - "synthetic-monitoring-card-title": "綜合監控", - "title-alerting": "警報" + "synthetic-monitoring-card-title": "綜合監控" }, "import-to-gma": { "action-button": "匯入", @@ -3721,6 +3719,7 @@ "category-selected-connection": "所選連線", "category-selected-element": "所選元素 ({{element}})", "category-size": "大小", + "category-tooltip": "", "cloud-item": { "category-cloud": "雲端", "label": { @@ -3965,6 +3964,13 @@ "auto": "自動" } }, + "tooltip-options": { + "name-tooltip-mode": "", + "tooltip-mode-options": { + "label-disabled": "", + "label-enabled": "" + } + }, "tree-navigation-editor": { "clear-selection": "清空選項", "frame-selection": "框選項目", @@ -6984,6 +6990,9 @@ "pro-tip-define-sources-through-configuration-files": "專家提示:您也可以透過設定檔案來定義資料來源。" } }, + "pane": { + "loading-placeholder": "" + }, "prev": "上一頁", "queryless-apps-extensions": { "aria-label-go-queryless": "停用查詢" @@ -7250,18 +7259,24 @@ "split-tooltip": "分割窗格", "split-widen": "擴大窗格" }, - "trace-page-actions": { - "ariaLabel-copy-trace-id": "", - "ariaLabel-export-trace": "", - "give-feedback": "提供意見回饋", - "label-copied": "已複製!", - "label-export": "匯出", - "label-trace-id": "追蹤 ID", - "title-share-thoughts-about-tracing-grafana": "分享對於在 Grafana 中進行追蹤的想法。" - }, "trace-page-header": { - "text-partial-trace": "部分追蹤", - "tooltip-url": "{{url}} 或 {{target}} 或 {{path}}" + "duration": "", + "export-started": "", + "give-feedback": "", + "link-copied": "", + "path": "", + "route": "", + "services": "", + "share": "", + "share-copy-link": "", + "share-export-json": "", + "share-tooltip": "", + "start-time": "", + "target": "", + "title-share-thoughts-about-tracing-grafana": "", + "tooltip-url": "", + "trace-id": "", + "url": "" }, "trace-page-search-bar": { "aria-label-clear-filters": "清除篩選條件按鈕", From ef80129bd99a9bf4d66f796ec7ef4dea826ce6ec Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 31 Jul 2025 09:17:42 +0200 Subject: [PATCH 072/126] Alerting: Fix contact points scoped permission check (#108851) * Fix scoped contact point permission check * Use annotations permissions for k8s API and globals permissions for external AMs * Fix scoped permission check for GrafanaReceiverForm --- .../contact-points/ContactPointHeader.tsx | 4 +- .../contact-points/ContactPoints.test.tsx | 60 +++++++++++++++---- .../contact-points/EditContactPoint.test.tsx | 3 +- .../receivers/form/GrafanaReceiverForm.tsx | 6 +- 4 files changed, 55 insertions(+), 18 deletions(-) diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx index 40a7afeddeb..1825b5e40a0 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx @@ -68,14 +68,14 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade */ const isReferencedByAnything = usingK8sApi ? Boolean(numberOfPolicies || numberOfRules) : policies.length > 0; /** Does the current user have permissions to edit the contact point? */ - const hasAbilityToEdit = canEditEntity(contactPoint) || editAllowed; + const hasAbilityToEdit = usingK8sApi ? canEditEntity(contactPoint) : editAllowed; /** Can the contact point actually be edited via the UI? */ const contactPointIsEditable = !provisioned; /** Given the alertmanager, the user's permissions, and the state of the contact point - can it actually be edited? */ const canEdit = editSupported && hasAbilityToEdit && contactPointIsEditable; /** Does the current user have permissions to delete the contact point? */ - const hasAbilityToDelete = canDeleteEntity(contactPoint) || deleteAllowed; + const hasAbilityToDelete = usingK8sApi ? canDeleteEntity(contactPoint) : deleteAllowed; /** Can the contact point actually be deleted, regardless of permissions? i.e. ensuring it isn't provisioned and isn't referenced elsewhere */ const contactPointIsDeleteable = !provisioned && !numberOfPoliciesPreventingDeletion && !numberOfRules; /** Given the alertmanager, the user's permissions, and the state of the contact point - can it actually be deleted? */ diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx index 79cec523d69..db39d474957 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx @@ -198,12 +198,37 @@ describe('contact points', () => { const unusedBadge = screen.getAllByLabelText('unused'); expect(unusedBadge).toHaveLength(4); - const viewProvisioned = screen.getByTestId('view-action'); - expect(viewProvisioned).toBeInTheDocument(); - expect(viewProvisioned).toBeEnabled(); + // Two contact points should have view buttons: grafana-default-email (cannot be edited) and provisioned-contact-point (provisioned) + const viewButtons = screen.getAllByRole('link', { name: /^view$/i }); + expect(viewButtons).toHaveLength(2); + + // Check view buttons by their href to verify which contact points they belong to + // The url is the same but the form should be readonly + expect(viewButtons[0]).toHaveAttribute('href', '/alerting/notifications/receivers/grafana-default-email/edit'); + expect(viewButtons[1]).toHaveAttribute( + 'href', + '/alerting/notifications/receivers/provisioned-contact-point/edit' + ); + + viewButtons.forEach((button) => { + expect(button).toBeEnabled(); + }); + + // Three contact points should have edit buttons: lotsa-emails, Slack with multiple channels, OnCall Contact point + const editButtons = screen.getAllByRole('link', { name: /^edit$/i }); + expect(editButtons).toHaveLength(3); + + // Check edit buttons by their href to verify which contact points they belong to + expect(editButtons[0]).toHaveAttribute('href', '/alerting/notifications/receivers/lotsa-emails/edit'); + expect(editButtons[1]).toHaveAttribute( + 'href', + '/alerting/notifications/receivers/OnCall%20Conctact%20point/edit' + ); + expect(editButtons[2]).toHaveAttribute( + 'href', + '/alerting/notifications/receivers/Slack%20with%20multiple%20channels/edit' + ); - const editButtons = screen.getAllByTestId('edit-action'); - expect(editButtons).toHaveLength(4); editButtons.forEach((button) => { expect(button).toBeEnabled(); }); @@ -227,11 +252,11 @@ describe('contact points', () => { expect(screen.getByRole('link', { name: 'add contact point' })).toHaveAttribute('aria-disabled', 'true'); // edit permission is based on API response - we should have 3 buttons - const editButtons = await screen.findAllByTestId('edit-action'); + const editButtons = await screen.findAllByRole('link', { name: /^edit$/i }); expect(editButtons).toHaveLength(3); // there should be view buttons though - one for provisioned, and one for the un-editable contact point - const viewButtons = screen.getAllByTestId('view-action'); + const viewButtons = screen.getAllByRole('link', { name: /^view$/i }); expect(viewButtons).toHaveLength(2); // check buttons in Notification Templates @@ -329,7 +354,18 @@ describe('contact points', () => { }, ]; - const { user } = renderWithProvider(); + // Add the necessary K8s annotations to allow deletion + const contactPointWithDeletePermission: ContactPointWithMetadata = { + ...basicContactPoint, + metadata: { + annotations: { + [K8sAnnotations.AccessDelete]: 'true', + }, + }, + policies, + }; + + const { user } = renderWithProvider(); const moreActions = screen.getByRole('button', { name: /More/ }); await user.click(moreActions); @@ -387,7 +423,7 @@ describe('contact points', () => { const unusedBadge = screen.getAllByLabelText('unused'); expect(unusedBadge).toHaveLength(1); - const editButtons = screen.getAllByTestId('edit-action'); + const editButtons = screen.getAllByRole('link', { name: /^edit$/i }); expect(editButtons).toHaveLength(2); editButtons.forEach((button) => { expect(button).toBeEnabled(); @@ -431,9 +467,9 @@ describe('contact points', () => { expect(screen.queryByRole('link', { name: 'add contact point' })).not.toBeInTheDocument(); - const viewProvisioned = screen.getByTestId('view-action'); - expect(viewProvisioned).toBeInTheDocument(); - expect(viewProvisioned).toBeEnabled(); + const viewButton = screen.getByRole('link', { name: /^view$/i }); + expect(viewButton).toBeInTheDocument(); + expect(viewButton).toBeEnabled(); // check buttons in Notification Templates const notificationTemplatesTab = screen.getByRole('tab', { name: 'Notification Templates' }); diff --git a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx index 04f6fcdb435..75413e20e33 100644 --- a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx @@ -44,7 +44,8 @@ beforeEach(() => { grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite]); }); -const getTemplatePreviewContent = async () => within(screen.getByTestId('template-preview')).findByTestId('mockeditor'); +const getTemplatePreviewContent = async () => + within(await screen.findByTestId('template-preview')).findByTestId('mockeditor'); const templatesSelectorTestId = 'existing-templates-selector'; diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx index 44da5c3588b..8971c9b34d7 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx @@ -135,9 +135,9 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode } } }; - const isEditable = Boolean( - (!readOnly || (contactPoint && canEditEntity(contactPoint))) && !contactPoint?.provisioned - ); + // If there is no contact point it means we're creating a new one, so scoped permissions doesn't exist yet + const hasScopedEditPermissions = contactPoint ? canEditEntity(contactPoint) : true; + const isEditable = !readOnly && hasScopedEditPermissions && !contactPoint?.provisioned; const isTestable = !readOnly; if (isLoadingNotifiers || isLoadingOnCallIntegration) { From 91e5e8019df41924d79909bd881525d07dfd5aaa Mon Sep 17 00:00:00 2001 From: Bruno Abrantes Date: Thu, 31 Jul 2025 09:18:29 +0200 Subject: [PATCH 073/126] chore: add documentation on unified search (#108847) * chore: add documentation on unified search Signed-off-by: Bruno Abrantes * fix: add additional needed feature flags for unified search Signed-off-by: Bruno Abrantes * fix: add docs about index_min_count and index_max_count Signed-off-by: Bruno Abrantes * fix: add documentation about sortable fields and the discrepancy with search/sortable Signed-off-by: Bruno Abrantes * fix: kubernetesClientDashboardsFolders feature flag is no more, remove it from the docs Signed-off-by: Bruno Abrantes * fix: simplify request flow diagrams Signed-off-by: Bruno Abrantes --------- Signed-off-by: Bruno Abrantes --- pkg/storage/unified/README.md | 509 ++++++++++++++++++++++++++++++++++ 1 file changed, 509 insertions(+) diff --git a/pkg/storage/unified/README.md b/pkg/storage/unified/README.md index 03301cf12bc..dd53fa622b0 100644 --- a/pkg/storage/unified/README.md +++ b/pkg/storage/unified/README.md @@ -777,3 +777,512 @@ The dual writer system provides metrics for monitoring: - `dual_writer_mode_transitions_total`: Counter of mode transitions Use these metrics to monitor the health of your migration and identify any issues with the dual writer system. + +--- + +## Unified Search System + +The Unified Search system provides a scalable, distributed search capability for Grafana's Unified Storage. It uses a ring-based architecture to distribute search requests across multiple search server instances, with namespace-based sharding for optimal performance and data distribution. + +### System Architecture + +The search system provides both unified and legacy search capabilities, with routing based on dual writer mode configuration: + +```mermaid +graph TB + subgraph "Request Sources" + A[Grafana UI
    User Search] + B[Dashboard Service
    Resource Searches] + C[Folder Service
    Resource Searches] + D[Alerting Service
    Resource Searches] + E[Provisioning Service
    Resource Searches] + F[API Endpoints
    Search Operations] + end + + subgraph "API Gateway Layer" + G[Grafana API Server
    Search Endpoint] + H[Search Client
    Dual Writer Aware] + end + + subgraph "Routing Decision" + I{Dual Writer Mode
    Check} + end + + subgraph "Unified Search Path (Mode 3+)" + J[Search Distributor
    Ring-based Routing] + K[Ring
    Consistent Hashing] + L[Search API Server 1
    Namespace Sharding
    + Embedded Bleve Backend] + M[Search API Server 2
    Namespace Sharding
    + Embedded Bleve Backend] + N[Search API Server 3
    Namespace Sharding
    + Embedded Bleve Backend] + O[Unified Storage
    K8s-style Resources] + end + + subgraph "Legacy Search Path (Mode 0-2)" + Q[Legacy Search Service
    SQL-based Search] + R[Legacy Storage
    Traditional Tables] + S[Shadow Traffic
    Mode 1-2 + Flag Enabled] + end + + A --> G + B --> H + C --> H + D --> H + E --> H + F --> G + G --> H + H --> I + + I -->|Mode 3+| J + I -->|Mode 0-2| Q + + J --> K + K --> L + K --> M + K --> N + L -.->|Just-in-Time
    Indexing| O + M -.->|Just-in-Time
    Indexing| O + N -.->|Just-in-Time
    Indexing| O + + Q --> R + Q -.->|Shadow Traffic
    Mode 1-2 + Flag| S + S -.-> J +``` + +### Search Backend Routing + +The search client routes requests based on the dual writer mode configuration for each resource type: + +#### Dual Writer Mode → Backend Routing +- **Mode 0-2**: Route to **Legacy Search** + - Mode 1-2: Shadow traffic to Unified Search (if `unifiedStorageSearchDualReaderEnabled` is enabled) + - Mode 0: No shadow traffic +- **Mode 3+**: Route to **Unified Search** + - No shadow traffic needed (unified is primary) + +### Feature Flags + +Unified Search requires several feature flags to be enabled depending on the desired functionality: + +#### Prerequisites (Required for Unified Storage) + +| Feature Flag | Purpose | Stage | Required For | +|--------------|---------|-------|--------------| +| `grafanaAPIServerWithExperimentalAPIs` | Allow experimental API groups | Development | Access to v0alpha1 APIs (including search) | + +#### Unified Search Specific Flags + +| Feature Flag | Purpose | Stage | Required For | +|--------------|---------|-------|--------------| +| `unifiedStorageSearch` | Core search functionality | Experimental | Search API servers, indexing | +| `unifiedStorageSearchUI` | Frontend search interface | Experimental | Grafana UI search | +| `unifiedStorageSearchPermissionFiltering` | User permission filtering | GA | Access control in search results | +| `unifiedStorageSearchSprinkles` | Usage insights integration | Experimental | Dashboard usage sorting (Enterprise) | +| `unifiedStorageSearchDualReaderEnabled` | Shadow traffic to unified search | Experimental | Shadow traffic during migration | + +#### Basic Configuration +```ini +[feature_toggles] +; Prerequisites for unified storage (required) +grafanaAPIServerWithExperimentalAPIs = true + +; Core search functionality (required) +unifiedStorageSearch = true + +; Enable search UI (required for frontend) +unifiedStorageSearchUI = true + +; Enable permission filtering (recommended) +unifiedStorageSearchPermissionFiltering = true + +; Enable shadow traffic during migration (optional) +unifiedStorageSearchDualReaderEnabled = true + +; Enable usage insights sorting (Enterprise only) +unifiedStorageSearchSprinkles = true +``` + +### Request Flow Diagrams + +#### Search Request Flow with Dual Writer Mode Routing + +Search requests originate from multiple sources, and the search client routes based on dual writer mode configuration: + +```mermaid +flowchart TD + A[Search Request] --> B{Dual Writer Mode} + B -->|Mode 3+| C[Unified Search] + B -->|Mode 0-2| D[Legacy Search] + C --> E[Return Results] + D --> E +``` + +#### Search Request Flow with Shadow Traffic + +When `unifiedStorageSearchDualReaderEnabled` is enabled and resource is in dual writer Mode 1-2 (legacy primary), shadow traffic is generated: + +```mermaid +flowchart TD + A[Search Request] --> B{Shadow Traffic Enabled?} + B -->|Yes| C[Primary: Legacy Search] + B -->|No| D[Single Search Path] + C --> E[Background: Unified Search] + C --> F[Return Legacy Results] + E --> G[Log Results for Comparison] + D --> H[Return Results] +``` + +### Distributor Architecture + +The Search Distributor acts as a smart proxy that routes search requests to the appropriate search API server based on namespace hashing: + +```mermaid +flowchart TD + A[Incoming Search Request] --> B[Hash Namespace] + B --> C[Select Random Instance] + C --> D[Proxy Request] + D --> E[Return Response] +``` + +#### Key Features: +- **Namespace-based routing**: Each request is routed based on the target namespace +- **Load balancing**: Random selection among available replicas for the namespace +- **Health awareness**: Only routes to `ACTIVE` ring instances +- **Connection pooling**: Reuses gRPC connections for efficiency +- **Proxy headers**: Adds metadata for debugging and tracing + +### Ring Architecture + +The hash ring provides consistent, distributed assignment of namespaces to search API servers: + +```mermaid +flowchart TD + A[Namespace] --> B[Hash Function] + B --> C[Ring Position] + C --> D[Assigned Instance] + D --> E[Search Processing] +``` + +#### Ring Properties: +- **Consistent hashing**: Uses FNV32 hash function for namespace distribution +- **128 tokens per instance**: Provides good distribution across the ring +- **Replication factor**: Configurable redundancy (default based on cluster size) +- **State management**: Instances transition through JOINING → ACTIVE → LEAVING +- **Automatic rebalancing**: Ring adjusts when instances join/leave + +### Namespace-Based Sharding + +Unified Search uses namespace-based sharding to distribute search indexes across multiple search API servers: + +```mermaid +flowchart LR + A[Namespaces] --> B[Hash Ring] + B --> C[Search Server 1] + B --> D[Search Server 2] + B --> E[Search Server 3] + C --> F[Indexes for Assigned Namespaces] + D --> G[Indexes for Assigned Namespaces] + E --> H[Indexes for Assigned Namespaces] +``` + +#### Sharding Benefits: +1. **Horizontal scalability**: Add more search servers to handle more namespaces +2. **Resource isolation**: Each namespace's index is independent +3. **Parallel processing**: Multiple searches can run simultaneously across different servers +4. **Fault tolerance**: Namespace availability depends only on its assigned server(s) + +#### Sharding Algorithm: +```go +func getSearchServer(namespace string) string { + hash := fnv.New32a() + hash.Write([]byte(namespace)) + + // Get replication set from ring + replicationSet := ring.GetWithOptions( + hash.Sum32(), + searchRingRead, + ring.WithReplicationFactor(ring.ReplicationFactor()) + ) + + // Random load balancing within replication set + instance := replicationSet.Instances[rand.Intn(len(replicationSet.Instances))] + return instance.Id +} +``` + +### Search Index Management + +Each search API server contains an embedded Bleve search engine that manages indexes for its assigned namespaces: + +```mermaid +flowchart TD + A[Search Request] --> B{Index Ready?} + B -->|Yes| C[Query Index] + B -->|No| D[Build Index] + D --> E[Fetch Resources] + E --> F[Create Index] + F --> C + C --> G[Return Results] + + H[Resource Changes] --> I[Update Index] + I --> F +``` + +#### Index Architecture Details: + +**Embedded Bleve Backend**: Each Search API Server contains its own Bleve search engine instance, not a shared external service. + +**Just-in-Time Indexing**: When a search request arrives for a namespace that doesn't have an index (or has an outdated index): +1. The Search API Server fetches all resources for that namespace from Unified Storage +2. Builds search documents in memory +3. Creates either a memory-based or disk-based Bleve index depending on size +4. Executes the search query against the newly built index +5. Returns results to the user + +**Index Storage Strategy**: +- **Memory indexes**: For small datasets (< `index_file_threshold` documents) +- **Disk indexes**: For large datasets (≥ `index_file_threshold` documents) +- Indexes are stored per Search API Server instance, not globally shared + +**Background Updates**: In addition to just-in-time indexing, Search API Servers also maintain indexes through background watch events for incremental updates. + +#### Index Configuration: +```ini +[unified_storage] +; Path for disk-based search indexes +index_path = /var/lib/grafana/unified-search/bleve + +; Threshold for file-based vs memory indexes +index_file_threshold = 1000 + +; Maximum batch size for indexing +index_max_batch_size = 100 + +; Number of worker threads for indexing +index_workers = 4 + +; Cache TTL for indexes +index_cache_ttl = 1h + +; Periodic rebuild interval (for usage insights) +index_rebuild_interval = 24h + +; Minimum resource count required to build an index (default: 1) +; If a namespace has fewer resources than this threshold, no index will be created +index_min_count = 1 + +; Maximum resource count before creating an empty index (default: 0 = no limit) +; When exceeded, creates an empty index instead of indexing all resources for performance +index_max_count = 0 +``` + +### Search Request Sources + +Unified Search serves multiple types of consumers within the Grafana ecosystem: + +#### 1. User-Initiated Searches +- **Source**: Grafana UI search interface +- **Purpose**: Interactive dashboard and folder discovery +- **Characteristics**: Real-time, user-facing, latency-sensitive +- **Endpoint**: `/api/v1/search` (legacy search UI) or `/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search` (when `unifiedStorageSearchUI` is enabled) + +#### 2. Internal Service Searches + +Internal services use different search backends depending on dual writer mode configuration: + +- **Dashboard Service**: + - Find related dashboards based on tags, folders, or content + - Discover dashboards for playlist creation + - Validate dashboard references during operations + - **Backend**: Depends on dashboard dual writer mode (Legacy for Mode 0-2, Unified for Mode 3+) + +- **Folder Service**: + - Retrieve folder contents and nested structures + - Resolve folder hierarchy relationships + - Check folder permissions and accessibility + - **Backend**: Depends on folder dual writer mode (Legacy for Mode 0-2, Unified for Mode 3+) + +- **Alerting Service**: + - Discover dashboards and panels for alert rule creation + - Find existing alert rules across namespaces + - Resolve dashboard/panel references in alert definitions + - **Backend**: Mixed - dashboard searches use dashboard dual writer mode, alert rule searches typically use legacy + +- **Provisioning Service**: + - Check for existing resources before provisioning + - Validate resource uniqueness and naming conflicts + - Discover resources for bulk operations + - **Backend**: Depends on each resource type's dual writer mode configuration + +- **API Services**: + - Backend support for various API endpoints + - Resource validation and dependency checking + - **Backend**: Routes based on resource type's dual writer mode + +#### 3. Search Operation Types + +Unified Search supports multiple types of search operations: + +##### Resource Search +- **Purpose**: Find resources (dashboards, folders, etc.) by content +- **Endpoint**: `/api/v1/search` (legacy) or `/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search` (when `unifiedStorageSearchUI` is enabled) +- **Additional endpoint**: `/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search/sortable` for retrieving sortable fields +- **Features**: Full-text search, filtering, sorting + +**Sortable Fields:** + +The `/search/sortable` endpoint currently returns a limited static list: +```json +{ + "fields": [ + {"field": "title", "display": "Title (A-Z)", "type": "string"}, + {"field": "-title", "display": "Title (Z-A)", "type": "string"} + ] +} +``` + +However, the search backend actually supports sorting by many more fields: + +**Standard Fields:** +- `title` - Resource display name (uses `title_phrase` for exact sorting) +- `name` - Kubernetes resource name +- `description` - Resource description +- `folder` - Parent folder name +- `created` - Creation timestamp (int64) +- `updated` - Last update timestamp (int64) +- `createdBy` - Creator user ID +- `updatedBy` - Last updater user ID +- `tags` - Resource tags (array) +- `rv` - Resource version (int64) + +**Dashboard-Specific Fields** (require `fields.` prefix): +- `fields.schema_version` - Dashboard schema version +- `fields.link_count` - Number of dashboard links +- `fields.panel_types` - Panel types used in dashboard +- `fields.ds_types` - Data source types used +- `fields.transformation` - Transformations used + +**Usage Insights Fields** (Enterprise only, require `fields.` prefix): +- `fields.views_total` - Total dashboard views +- `fields.views_last_1_days` / `fields.views_last_7_days` / `fields.views_last_30_days` - Recent views +- `fields.views_today` - Today's views +- `fields.queries_total` - Total queries executed +- `fields.queries_last_1_days` / `fields.queries_last_7_days` / `fields.queries_last_30_days` - Recent queries +- `fields.queries_today` - Today's queries +- `fields.errors_total` - Total errors +- `fields.errors_last_1_days` / `fields.errors_last_7_days` / `fields.errors_last_30_days` - Recent errors +- `fields.errors_today` - Today's errors + +**Usage Examples:** +```bash +# Sort by title (ascending) +GET /apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search?sortBy=title + +# Sort by creation date (descending) +GET /apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search?sortBy=-created + +# Sort by usage insights (Enterprise) +GET /apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search?sortBy=-fields.views_total +``` + +*Note: There's currently a discrepancy between the limited fields exposed by `/search/sortable` and the full range of fields actually supported by the search backend.* + +##### Federated Search +- **Purpose**: Search across multiple resource types simultaneously +- **Features**: Cross-resource queries, unified result ranking, combined sorting and faceting +- **Implementation**: Uses Bleve IndexAlias to combine multiple indexes for unified searching +- **Default behavior**: When no type is specified, automatically federates dashboards and folders + +**How Federated Search Works:** + +Federated search is implemented using Bleve's IndexAlias feature, which allows searching across multiple indexes as if they were a single unified index. This enables: + +1. **Cross-resource queries**: Search for content across dashboards, folders, and other resource types +2. **Unified sorting**: Results from different resource types are merged and sorted together +3. **Combined faceting**: Aggregate facet statistics across all federated resource types +4. **Permission filtering**: Respects user permissions for each resource type independently + +**API Usage Examples:** + +**1. Default Federation (Dashboards + Folders):** +```bash +# When no type is specified, automatically searches dashboards and folders +GET /apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search?query=my-search +``` + +**2. Single Resource Type Search:** +```bash +# Search only folders (despite the "dashboard" API group, type parameter controls what's searched) +GET /apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search?type=folders&query=my-search + +# Search only dashboards +GET /apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search?type=dashboards&query=my-search +``` + +**3. Explicit Two-Type Federation:** +```bash +# Search dashboards (primary) with folders federated +GET /apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search?type=dashboards&type=folders&query=my-search +``` + +**4. Protocol Buffer Request Structure:** +```protobuf +message ResourceSearchRequest { + ListOptions options = 1; // Primary resource type to search + repeated ResourceKey federated = 2; // Additional resource types to federate + string query = 3; // Search query applied across all types + // ... other fields +} +``` + +**Example gRPC/Protocol Buffer Usage:** +```go +searchRequest := &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, // Primary: search dashboards + }, + Federated: []*resourcepb.ResourceKey{ + folderKey, // Also search folders + }, + Query: "monitoring", + Limit: 50, + SortBy: []*resourcepb.ResourceSearchRequest_Sort{ + {Field: "title", Desc: false}, // Sort combined results by title + }, +} +``` + +**5. Unified Results:** +Federated search returns a single result set containing resources from all specified types, with: +- **Unified ranking**: All results scored and ranked together +- **Cross-type sorting**: Resources from different types sorted by common fields (title, tags, etc.) +- **Resource type identification**: Each result includes metadata indicating its resource type +- **Permission-aware filtering**: Only returns resources the user has permission to see + +**Limitations:** +- Federation only works across resource types with **common fields** (title, tags, folder, etc.) +- All federated indexes must be of the same search backend type (currently Bleve) +- Currently supports up to 2 resource types in federation via the API endpoint +- **Architectural note**: The search endpoint is under `dashboard.grafana.app` but can search any resource type via the `type` parameter - this is a design choice where the "dashboard search" has evolved into a generic search endpoint + +##### Managed Objects +- **Purpose**: Administrative queries for resource management +- **Operations**: Count, list, statistics + +##### Stats and Monitoring +- **Purpose**: Index health and performance metrics +- **Metrics**: Document counts, index sizes, search latency + + +### Monitoring and Observability + +Key metrics for monitoring Unified Search: + +- `unified_search_requests_total`: Search request counts by type and status +- `unified_search_request_duration_seconds`: Search request latency +- `unified_search_index_size_bytes`: Size of search indexes +- `unified_search_documents_total`: Number of indexed documents +- `unified_search_indexing_duration_seconds`: Time to build/update indexes +- `unified_search_shadow_requests_total`: Shadow traffic request counts +- `unified_search_ring_members`: Number of active search server instances + + From 6db07b901cb431e009569b23dcf517017d768a72 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 31 Jul 2025 09:55:08 +0200 Subject: [PATCH 074/126] Alerting: Enable HA clustering in remote primary mode (#108930) --- pkg/services/ngalert/ngalert.go | 1 - pkg/services/ngalert/notifier/multiorg_alertmanager.go | 8 ++------ pkg/setting/setting_unified_alerting.go | 1 - 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 3861fa60c54..e794460fc5e 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -225,7 +225,6 @@ func (ng *AlertNG) init() error { if remotePrimary { ng.Log.Debug("Starting Grafana with remote primary mode enabled") m.Info.WithLabelValues(metrics.ModeRemotePrimary).Set(1) - ng.Cfg.UnifiedAlerting.SkipClustering = true // This function will be used by the MOA to create new Alertmanagers. override = notifier.WithAlertmanagerOverride(func(factoryFn notifier.OrgAlertmanagerFactory) notifier.OrgAlertmanagerFactory { return func(ctx context.Context, orgID int64) (notifier.Alertmanager, error) { diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager.go b/pkg/services/ngalert/notifier/multiorg_alertmanager.go index 4ce738c08a9..4aa0151d18f 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager.go @@ -161,12 +161,8 @@ func NewMultiOrgAlertmanager( peer: &NilPeer{}, } - if cfg.UnifiedAlerting.SkipClustering { - l.Info("Skipping setting up clustering for MOA") - } else { - if err := moa.setupClustering(cfg); err != nil { - return nil, err - } + if err := moa.setupClustering(cfg); err != nil { + return nil, err } // Set up the default per tenant Alertmanager factory. diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index 665c751532f..0187652cb0e 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -121,7 +121,6 @@ type UnifiedAlertingSettings struct { DefaultRuleEvaluationInterval time.Duration Screenshots UnifiedAlertingScreenshotSettings ReservedLabels UnifiedAlertingReservedLabelSettings - SkipClustering bool StateHistory UnifiedAlertingStateHistorySettings NotificationHistory UnifiedAlertingNotificationHistorySettings RemoteAlertmanager RemoteAlertmanagerSettings From 6809df54276d313ef7ab936bd7a119d964833228 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 09:18:32 +0100 Subject: [PATCH 075/126] Update dependency jsdom-testing-mocks to v1.15.2 (#108940) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 386 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 335 insertions(+), 51 deletions(-) diff --git a/yarn.lock b/yarn.lock index a4a40fb40e5..9eb3e4e6d45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6334,6 +6334,23 @@ __metadata: languageName: node linkType: hard +"@puppeteer/browsers@npm:2.10.6": + version: 2.10.6 + resolution: "@puppeteer/browsers@npm:2.10.6" + dependencies: + debug: "npm:^4.4.1" + extract-zip: "npm:^2.0.1" + progress: "npm:^2.0.3" + proxy-agent: "npm:^6.5.0" + semver: "npm:^7.7.2" + tar-fs: "npm:^3.1.0" + yargs: "npm:^17.7.2" + bin: + browsers: lib/cjs/main-cli.js + checksum: 10/f6af7260b9df0bc4af55be139bab64e4a92b2019553eb48e57659cf589e80cc2a2f82fd613fec2fb933eb2de5a096ede53ba8a3471c71f14d170065880155ea3 + languageName: node + linkType: hard + "@radix-ui/react-compose-refs@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-compose-refs@npm:1.0.1" @@ -8920,6 +8937,13 @@ __metadata: languageName: node linkType: hard +"@tootallnate/quickjs-emscripten@npm:^0.23.0": + version: 0.23.0 + resolution: "@tootallnate/quickjs-emscripten@npm:0.23.0" + checksum: 10/95cbad451d195b9d8f312103abafcc010741eb9256e98d7953e7c026d4c1ed4abb2248a14018bf49e3201c350104fc643137b23aa0bbed2744c795c39dc48a28 + languageName: node + linkType: hard + "@tree-sitter-grammars/tree-sitter-yaml@npm:=0.7.1": version: 0.7.1 resolution: "@tree-sitter-grammars/tree-sitter-yaml@npm:0.7.1" @@ -11129,12 +11153,10 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": - version: 7.1.1 - resolution: "agent-base@npm:7.1.1" - dependencies: - debug: "npm:^4.3.4" - checksum: 10/c478fec8f79953f118704d007a38f2a185458853f5c45579b9669372bd0e12602e88dc2ad0233077831504f7cd6fcc8251c383375bba5eaaf563b102938bda26 +"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": + version: 7.1.4 + resolution: "agent-base@npm:7.1.4" + checksum: 10/79bef167247789f955aaba113bae74bf64aa1e1acca4b1d6bb444bdf91d82c3e07e9451ef6a6e2e35e8f71a6f97ce33e3d855a5328eb9fad1bc3cc4cfd031ed8 languageName: node linkType: hard @@ -11692,6 +11714,15 @@ __metadata: languageName: node linkType: hard +"ast-types@npm:^0.13.4": + version: 0.13.4 + resolution: "ast-types@npm:0.13.4" + dependencies: + tslib: "npm:^2.0.1" + checksum: 10/c55b375b9aaf44713d8c0f77a08215ab6d44f368b13e44f2141c421022af3c62b615a30c8ea629457f0cbaec409c713401c0188a124552c8fe4a5ad6b17ff3c3 + languageName: node + linkType: hard + "ast-types@npm:^0.16.1": version: 0.16.1 resolution: "ast-types@npm:0.16.1" @@ -11852,6 +11883,13 @@ __metadata: languageName: node linkType: hard +"b4a@npm:^1.6.4": + version: 1.6.7 + resolution: "b4a@npm:1.6.7" + checksum: 10/1ac056e3bce378d4d3e570e57319360a9d3125ab6916a1921b95bea33d9ee646698ebc75467561fd6fcc80ff697612124c89bb9b95e80db94c6dc23fcb977705 + languageName: node + linkType: hard + "babel-jest@npm:29.7.0, babel-jest@npm:^29.7.0": version: 29.7.0 resolution: "babel-jest@npm:29.7.0" @@ -12014,6 +12052,62 @@ __metadata: languageName: node linkType: hard +"bare-events@npm:^2.2.0, bare-events@npm:^2.5.4": + version: 2.6.0 + resolution: "bare-events@npm:2.6.0" + checksum: 10/d76a344d7202618c91290bdf94556bf86b8b76c34c001906bb1345e11323e960e092d8923f241c40ba7572e890a6bad855710e5cacd94efc43818311028e2534 + languageName: node + linkType: hard + +"bare-fs@npm:^4.0.1": + version: 4.1.6 + resolution: "bare-fs@npm:4.1.6" + dependencies: + bare-events: "npm:^2.5.4" + bare-path: "npm:^3.0.0" + bare-stream: "npm:^2.6.4" + peerDependencies: + bare-buffer: "*" + peerDependenciesMeta: + bare-buffer: + optional: true + checksum: 10/b56968a3d12bd7616b9251571d4de9550f07cd263c5150547f9cb1f45028275c25862762c3f62b3b3c08a44df4a37aa156e1b8b0697ad3f129048c1cdfa0fda3 + languageName: node + linkType: hard + +"bare-os@npm:^3.0.1": + version: 3.6.1 + resolution: "bare-os@npm:3.6.1" + checksum: 10/285d95c391250166128e64da2947f4a348ae127de680afffec1f6c82445856be0d1f259672b471afe06517e4cd3831183c373a1d63ef7799ed4aaa1321b86b67 + languageName: node + linkType: hard + +"bare-path@npm:^3.0.0": + version: 3.0.0 + resolution: "bare-path@npm:3.0.0" + dependencies: + bare-os: "npm:^3.0.1" + checksum: 10/712d90e9cd8c3263cc11b0e0d386d1531a452706d7840c081ee586b34b00d72544e65df7a40013d47c1b177277495225deeede65cb2984db88a979cb65aaa2ff + languageName: node + linkType: hard + +"bare-stream@npm:^2.6.4": + version: 2.6.5 + resolution: "bare-stream@npm:2.6.5" + dependencies: + streamx: "npm:^2.21.0" + peerDependencies: + bare-buffer: "*" + bare-events: "*" + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + checksum: 10/0f5ca2167fbbccc118157bce7c53a933e21726268e03d751461211550d72b2d01c296b767ccf96aae8ab28e106b126407c6fe0d29f915734b844ffe6057f0a08 + languageName: node + linkType: hard + "baron@npm:3.0.3": version: 3.0.3 resolution: "baron@npm:3.0.3" @@ -12051,6 +12145,13 @@ __metadata: languageName: node linkType: hard +"basic-ftp@npm:^5.0.2": + version: 5.0.5 + resolution: "basic-ftp@npm:5.0.5" + checksum: 10/3dc56b2092b10d67e84621f5b9bbb0430469499178e857869194184d46fbdd367a9aa9fad660084388744b074b5f540e6ac8c22c0826ebba4fcc86a9d1c324e2 + languageName: node + linkType: hard + "bcrypt-pbkdf@npm:^1.0.0": version: 1.0.2 resolution: "bcrypt-pbkdf@npm:1.0.2" @@ -12893,6 +12994,18 @@ __metadata: languageName: node linkType: hard +"chromium-bidi@npm:7.2.0": + version: 7.2.0 + resolution: "chromium-bidi@npm:7.2.0" + dependencies: + mitt: "npm:^3.0.1" + zod: "npm:^3.24.1" + peerDependencies: + devtools-protocol: "*" + checksum: 10/edbf60cd24b8d59f35674b414a5d65c52b2d4807aca10dc575702fc3240b826d43a08ef1d21b6e5f6ddc1d6d6998a2b3aeffb639fe083d628d118866d8a0eab5 + languageName: node + linkType: hard + "ci-info@npm:^3.2.0": version: 3.9.0 resolution: "ci-info@npm:3.9.0" @@ -14634,6 +14747,13 @@ __metadata: languageName: node linkType: hard +"data-uri-to-buffer@npm:^6.0.2": + version: 6.0.2 + resolution: "data-uri-to-buffer@npm:6.0.2" + checksum: 10/8b6927c33f9b54037f442856be0aa20e5fd49fa6c9c8ceece408dc306445d593ad72d207d57037c529ce65f413b421da800c6827b1dbefb607b8056f17123a61 + languageName: node + linkType: hard + "data-urls@npm:^3.0.2": version: 3.0.2 resolution: "data-urls@npm:3.0.2" @@ -14896,6 +15016,17 @@ __metadata: languageName: node linkType: hard +"degenerator@npm:^5.0.0": + version: 5.0.1 + resolution: "degenerator@npm:5.0.1" + dependencies: + ast-types: "npm:^0.13.4" + escodegen: "npm:^2.1.0" + esprima: "npm:^4.0.1" + checksum: 10/a64fa39cdf6c2edd75188157d32338ee9de7193d7dbb2aeb4acb1eb30fa4a15ed80ba8dae9bd4d7b085472cf174a5baf81adb761aaa8e326771392c922084152 + languageName: node + linkType: hard + "del@npm:^7.1.0": version: 7.1.0 resolution: "del@npm:7.1.0" @@ -14993,6 +15124,13 @@ __metadata: languageName: node linkType: hard +"devtools-protocol@npm:0.0.1464554": + version: 0.0.1464554 + resolution: "devtools-protocol@npm:0.0.1464554" + checksum: 10/c179c23a27d180d6f2196199babaea3c1e59995515df1b082621930cbff7428c2ca2fc12ef0a8fc82fcb133e08dcddaf80e17a7fe841924d6042c2c54511c431 + languageName: node + linkType: hard + "devtools-protocol@npm:0.0.869402": version: 0.0.869402 resolution: "devtools-protocol@npm:0.0.869402" @@ -16011,7 +16149,7 @@ __metadata: languageName: node linkType: hard -"escodegen@npm:^2.0.0": +"escodegen@npm:^2.0.0, escodegen@npm:^2.1.0": version: 2.1.0 resolution: "escodegen@npm:2.1.0" dependencies: @@ -16700,7 +16838,7 @@ __metadata: languageName: node linkType: hard -"extract-zip@npm:2.0.1, extract-zip@npm:^2.0.0": +"extract-zip@npm:2.0.1, extract-zip@npm:^2.0.0, extract-zip@npm:^2.0.1": version: 2.0.1 resolution: "extract-zip@npm:2.0.1" dependencies: @@ -16752,7 +16890,7 @@ __metadata: languageName: node linkType: hard -"fast-fifo@npm:^1.1.0": +"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": version: 1.3.2 resolution: "fast-fifo@npm:1.3.2" checksum: 10/6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 @@ -17738,6 +17876,17 @@ __metadata: languageName: node linkType: hard +"get-uri@npm:^6.0.1": + version: 6.0.5 + resolution: "get-uri@npm:6.0.5" + dependencies: + basic-ftp: "npm:^5.0.2" + data-uri-to-buffer: "npm:^6.0.2" + debug: "npm:^4.3.4" + checksum: 10/6daa56eb367dc030ae7bf6db4b5d36f200c9bb47ab00593c142176e4f33f22e129a294ac94329c6bcaebda19b7506080267a336742d20a915fb2bef9c400347f + languageName: node + linkType: hard + "get-user-locale@npm:^2.2.1": version: 2.3.0 resolution: "get-user-locale@npm:2.3.0" @@ -19068,7 +19217,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": +"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -19140,13 +19289,13 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1": - version: 7.0.4 - resolution: "https-proxy-agent@npm:7.0.4" +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": + version: 7.0.6 + resolution: "https-proxy-agent@npm:7.0.6" dependencies: - agent-base: "npm:^7.0.2" + agent-base: "npm:^7.1.2" debug: "npm:4" - checksum: 10/405fe582bba461bfe5c7e2f8d752b384036854488b828ae6df6a587c654299cbb2c50df38c4b6ab303502c3c5e029a793fbaac965d1e86ee0be03faceb554d63 + checksum: 10/784b628cbd55b25542a9d85033bdfd03d4eda630fb8b3c9477959367f3be95dc476ed2ecbb9836c359c7c698027fc7b45723a302324433590f45d6c1706e8c13 languageName: node linkType: hard @@ -21251,12 +21400,13 @@ __metadata: linkType: hard "jsdom-testing-mocks@npm:^1.13.1": - version: 1.14.0 - resolution: "jsdom-testing-mocks@npm:1.14.0" + version: 1.15.2 + resolution: "jsdom-testing-mocks@npm:1.15.2" dependencies: bezier-easing: "npm:^2.1.0" css-mediaquery: "npm:^0.1.2" - checksum: 10/b7f89f4687345f437f9e022cacc9ed577e58c825b4283b61dd46ab61f9612d8a715b6acc12aa91bcd3f6c97455dfe61eeae86ce4694832469f3ad95d179bd671 + puppeteer: "npm:^24.15.0" + checksum: 10/be5fceccbbae9c34b5065704cb9335403de95462b84ad01e314dff6f577bec1f62c86ce72219e1c5bb1415e8a0a1a1ad90d41e4360a367a221ca7ecb967fe2fb languageName: node linkType: hard @@ -22299,6 +22449,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^7.14.1": + version: 7.18.3 + resolution: "lru-cache@npm:7.18.3" + checksum: 10/6029ca5aba3aacb554e919d7ef804fffd4adfc4c83db00fac8248c7c78811fb6d4b6f70f7fd9d55032b3823446546a007edaa66ad1f2377ae833bd983fac5d98 + languageName: node + linkType: hard + "lru-memoize@npm:^1.1.0": version: 1.1.0 resolution: "lru-memoize@npm:1.1.0" @@ -23013,6 +23170,13 @@ __metadata: languageName: node linkType: hard +"mitt@npm:^3.0.1": + version: 3.0.1 + resolution: "mitt@npm:3.0.1" + checksum: 10/287c70d8e73ffc25624261a4989c783768aed95ecb60900f051d180cf83e311e3e59865bfd6e9d029cdb149dc20ba2f128a805e9429c5c4ce33b1416c65bbd14 + languageName: node + linkType: hard + "mkdirp-classic@npm:^0.5.2": version: 0.5.3 resolution: "mkdirp-classic@npm:0.5.3" @@ -23405,6 +23569,13 @@ __metadata: languageName: node linkType: hard +"netmask@npm:^2.0.2": + version: 2.0.2 + resolution: "netmask@npm:2.0.2" + checksum: 10/375cabe898a5832816958664f26206f0a1e9f3605aa1816bfce803e060ff20f9d6ce56a2377e46f1470938358c31c27b3a8086f4a5e3ef678896147884d63ffa + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -24666,6 +24837,32 @@ __metadata: languageName: node linkType: hard +"pac-proxy-agent@npm:^7.1.0": + version: 7.2.0 + resolution: "pac-proxy-agent@npm:7.2.0" + dependencies: + "@tootallnate/quickjs-emscripten": "npm:^0.23.0" + agent-base: "npm:^7.1.2" + debug: "npm:^4.3.4" + get-uri: "npm:^6.0.1" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.6" + pac-resolver: "npm:^7.0.1" + socks-proxy-agent: "npm:^8.0.5" + checksum: 10/187656be62d5a6b983d90a86d64106a38b1a9ee78f591fabb27b3cf0d51e5d528456a9faaaf981c93dd54dc9c9ee8d33e35a51072b73a19ec1a8e0d0c36a2b99 + languageName: node + linkType: hard + +"pac-resolver@npm:^7.0.1": + version: 7.0.1 + resolution: "pac-resolver@npm:7.0.1" + dependencies: + degenerator: "npm:^5.0.0" + netmask: "npm:^2.0.2" + checksum: 10/839134328781b80d49f9684eae1f5c74f50a1d4482076d44c84fc2f3ca93da66fa11245a4725a057231e06b311c20c989fd0681e662a0792d17f644d8fe62a5e + languageName: node + linkType: hard + "package-hash@npm:^4.0.0": version: 4.0.0 resolution: "package-hash@npm:4.0.0" @@ -25968,7 +26165,7 @@ __metadata: languageName: node linkType: hard -"progress@npm:^2.0.1": +"progress@npm:^2.0.1, progress@npm:^2.0.3": version: 2.0.3 resolution: "progress@npm:2.0.3" checksum: 10/e6f0bcb71f716eee9dfac0fe8a2606e3704d6a64dd93baaf49fbadbc8499989a610fe14cf1bc6f61b6d6653c49408d94f4a94e124538084efd8e4cf525e0293d @@ -26106,6 +26303,22 @@ __metadata: languageName: node linkType: hard +"proxy-agent@npm:^6.5.0": + version: 6.5.0 + resolution: "proxy-agent@npm:6.5.0" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:^4.3.4" + http-proxy-agent: "npm:^7.0.1" + https-proxy-agent: "npm:^7.0.6" + lru-cache: "npm:^7.14.1" + pac-proxy-agent: "npm:^7.1.0" + proxy-from-env: "npm:^1.1.0" + socks-proxy-agent: "npm:^8.0.5" + checksum: 10/56d5a494d96dafad94868870af776939e7b9aaca172465a5c251d2523496a8353b029c32d2a72a012bd62622cdc9a43ba3df59b5738ab7b740bc6b362e9f9477 + languageName: node + linkType: hard + "proxy-from-env@npm:1.0.0": version: 1.0.0 resolution: "proxy-from-env@npm:1.0.0" @@ -26172,6 +26385,36 @@ __metadata: languageName: node linkType: hard +"puppeteer-core@npm:24.15.0": + version: 24.15.0 + resolution: "puppeteer-core@npm:24.15.0" + dependencies: + "@puppeteer/browsers": "npm:2.10.6" + chromium-bidi: "npm:7.2.0" + debug: "npm:^4.4.1" + devtools-protocol: "npm:0.0.1464554" + typed-query-selector: "npm:^2.12.0" + ws: "npm:^8.18.3" + checksum: 10/9fe0a7a982289f7dbc42799764a8273c814d00254f61431f3598a38fbdee7b0c6b171bddbe738d8be4f5868a9b4b034ecaa6c105e407452c276ae1046ef2e3b3 + languageName: node + linkType: hard + +"puppeteer@npm:^24.15.0": + version: 24.15.0 + resolution: "puppeteer@npm:24.15.0" + dependencies: + "@puppeteer/browsers": "npm:2.10.6" + chromium-bidi: "npm:7.2.0" + cosmiconfig: "npm:^9.0.0" + devtools-protocol: "npm:0.0.1464554" + puppeteer-core: "npm:24.15.0" + typed-query-selector: "npm:^2.12.0" + bin: + puppeteer: lib/cjs/puppeteer/node/cli.js + checksum: 10/f6d976d54643045ac0c12ed72dc807716e455ed0d8203305aac813043779c329fdf66261e44838b509dd9c57372332b6be458e636577cd7626d24f0d902166d4 + languageName: node + linkType: hard + "puppeteer@npm:~9.1.1": version: 9.1.1 resolution: "puppeteer@npm:9.1.1" @@ -26231,13 +26474,6 @@ __metadata: languageName: node linkType: hard -"queue-tick@npm:^1.0.1": - version: 1.0.1 - resolution: "queue-tick@npm:1.0.1" - checksum: 10/f447926c513b64a857906f017a3b350f7d11277e3c8d2a21a42b7998fa1a613d7a829091e12d142bb668905c8f68d8103416c7197856efb0c72fa835b8e254b5 - languageName: node - linkType: hard - "queue-typescript@npm:^1.0.1": version: 1.0.1 resolution: "queue-typescript@npm:1.0.1" @@ -28634,7 +28870,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.7.2, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, 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, semver@npm:^7.7.0, semver@npm:^7.7.1": +"semver@npm:7.7.2, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, 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, semver@npm:^7.7.0, semver@npm:^7.7.1, semver@npm:^7.7.2": version: 7.7.2 resolution: "semver@npm:7.7.2" bin: @@ -29295,24 +29531,24 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^8.0.3": - version: 8.0.3 - resolution: "socks-proxy-agent@npm:8.0.3" +"socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.5": + version: 8.0.5 + resolution: "socks-proxy-agent@npm:8.0.5" dependencies: - agent-base: "npm:^7.1.1" + agent-base: "npm:^7.1.2" debug: "npm:^4.3.4" - socks: "npm:^2.7.1" - checksum: 10/c2112c66d6322e497d68e913c3780f3683237fd394bfd480b9283486a86e36095d0020db96145d88f8ccd9cc73261b98165b461f9c1bf5dc17abfe75c18029ce + socks: "npm:^2.8.3" + checksum: 10/ee99e1dacab0985b52cbe5a75640be6e604135e9489ebdc3048635d186012fbaecc20fbbe04b177dee434c319ba20f09b3e7dfefb7d932466c0d707744eac05c languageName: node linkType: hard -"socks@npm:^2.6.1, socks@npm:^2.7.1": - version: 2.8.3 - resolution: "socks@npm:2.8.3" +"socks@npm:^2.6.1, socks@npm:^2.8.3": + version: 2.8.6 + resolution: "socks@npm:2.8.6" dependencies: ip-address: "npm:^9.0.5" smart-buffer: "npm:^4.2.0" - checksum: 10/ffcb622c22481dfcd7589aae71fbfd71ca34334064d181df64bf8b7feaeee19706aba4cffd1de35cc7bbaeeaa0af96be2d7f40fcbc7bc0ab69533a7ae9ffc4fb + checksum: 10/7aef197dee914a01a39d5f250a59f0c9fa0a9fd10f8135ee2cff6c42576993ae1c817503a12eb424f1bb69f1e234f8dbaf8cc48bfcfa10c51a12af8f0ea97698 languageName: node linkType: hard @@ -29724,13 +29960,17 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0": - version: 2.15.7 - resolution: "streamx@npm:2.15.7" +"streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0, streamx@npm:^2.15.0, streamx@npm:^2.21.0": + version: 2.22.1 + resolution: "streamx@npm:2.22.1" dependencies: - fast-fifo: "npm:^1.1.0" - queue-tick: "npm:^1.0.1" - checksum: 10/5f59f49383b41546e87b15ba54970cca262d1eb0826c5b88c4dc5329d18821fed06f92f849da68c764ad91ea05f394518510957371cb0abd3617bdabed465cdb + bare-events: "npm:^2.2.0" + fast-fifo: "npm:^1.3.2" + text-decoder: "npm:^1.1.0" + dependenciesMeta: + bare-events: + optional: true + checksum: 10/6d8576e0e5f4a67776427e3d29a877e66295bf7e17019a5b5c77d7fa026c5e8df6cdbd0cec2774999075af985179d70f07b25db7557b9226e33148fe67edd487 languageName: node linkType: hard @@ -30392,6 +30632,23 @@ __metadata: languageName: node linkType: hard +"tar-fs@npm:^3.1.0": + version: 3.1.0 + resolution: "tar-fs@npm:3.1.0" + dependencies: + bare-fs: "npm:^4.0.1" + bare-path: "npm:^3.0.0" + pump: "npm:^3.0.0" + tar-stream: "npm:^3.1.5" + dependenciesMeta: + bare-fs: + optional: true + bare-path: + optional: true + checksum: 10/272054aa93adf66f3febac1ab666c2e1ae80fb7d9b7483efa9e9216aca6f7900dfecb9ef04e83f179251f95a965f960d760c42ebbb1e04580a182da997af98da + languageName: node + linkType: hard + "tar-stream@npm:^2.1.4, tar-stream@npm:~2.2.0": version: 2.2.0 resolution: "tar-stream@npm:2.2.0" @@ -30405,6 +30662,17 @@ __metadata: languageName: node linkType: hard +"tar-stream@npm:^3.1.5": + version: 3.1.7 + resolution: "tar-stream@npm:3.1.7" + dependencies: + b4a: "npm:^1.6.4" + fast-fifo: "npm:^1.2.0" + streamx: "npm:^2.15.0" + checksum: 10/b21a82705a72792544697c410451a4846af1f744176feb0ff11a7c3dd0896961552e3def5e1c9a6bbee4f0ae298b8252a1f4c9381e9f991553b9e4847976f05c + languageName: node + linkType: hard + "tar@npm:6.2.1, tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2": version: 6.2.1 resolution: "tar@npm:6.2.1" @@ -30491,6 +30759,15 @@ __metadata: languageName: node linkType: hard +"text-decoder@npm:^1.1.0": + version: 1.2.3 + resolution: "text-decoder@npm:1.2.3" + dependencies: + b4a: "npm:^1.6.4" + checksum: 10/bcdec33c0f070aeac38e46e4cafdcd567a58473ed308bdf75260bfbd8f7dc76acbc0b13226afaec4a169d0cb44cec2ab89c57b6395ccf02e941eaebbe19e124a + languageName: node + linkType: hard + "text-extensions@npm:^1.0.0": version: 1.9.0 resolution: "text-extensions@npm:1.9.0" @@ -31251,6 +31528,13 @@ __metadata: languageName: node linkType: hard +"typed-query-selector@npm:^2.12.0": + version: 2.12.0 + resolution: "typed-query-selector@npm:2.12.0" + checksum: 10/e65b646830315e63282883acb44ea48ef8da3e9a044aa69e03f3bd876d7a69baae85f71c0918456b43f7c1bc2b448f2d64a424280f9699d34be2bae582121bc9 + languageName: node + linkType: hard + "typedarray-to-buffer@npm:^3.1.5": version: 3.1.5 resolution: "typedarray-to-buffer@npm:3.1.5" @@ -32720,9 +33004,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.2.3, ws@npm:^8.9.0": - version: 8.18.0 - resolution: "ws@npm:8.18.0" +"ws@npm:^8.18.3, ws@npm:^8.2.3, ws@npm:^8.9.0": + version: 8.18.3 + resolution: "ws@npm:8.18.3" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -32731,7 +33015,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/70dfe53f23ff4368d46e4c0b1d4ca734db2c4149c6f68bc62cb16fc21f753c47b35fcc6e582f3bdfba0eaeb1c488cddab3c2255755a5c3eecb251431e42b3ff6 + checksum: 10/725964438d752f0ab0de582cd48d6eeada58d1511c3f613485b5598a83680bedac6187c765b0fe082e2d8cc4341fc57707c813ae780feee82d0c5efe6a4c61b6 languageName: node linkType: hard @@ -33010,10 +33294,10 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.23.8, zod@npm:^3.25.3, zod@npm:^3.25.55": - version: 3.25.67 - resolution: "zod@npm:3.25.67" - checksum: 10/0e35432dcca7f053e63f5dd491a87c78abe0d981817547252c3b6d05f0f58788695d1a69724759c6501dff3fd62929be24c9f314a3625179bee889150f7a61fa +"zod@npm:^3.23.8, zod@npm:^3.24.1, zod@npm:^3.25.3, zod@npm:^3.25.55": + version: 3.25.76 + resolution: "zod@npm:3.25.76" + checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 languageName: node linkType: hard From b11509fea2cd4b34afc69d6b901163126678ec97 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:20:28 +0200 Subject: [PATCH 076/126] Loki: Decouple unit tests from core (#108917) * Loki: Decouple tests * Fix flaky tests * Add maxParallel 2 for plugin tests --- jest.config.js | 1 + package.json | 2 +- public/app/plugins/datasource/loki/LanguageProvider.test.ts | 6 ++++-- public/app/plugins/datasource/loki/jest-setup.js | 1 + public/app/plugins/datasource/loki/jest.config.js | 3 +++ public/app/plugins/datasource/loki/package.json | 5 ++++- yarn.lock | 1 + 7 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 public/app/plugins/datasource/loki/jest-setup.js create mode 100644 public/app/plugins/datasource/loki/jest.config.js diff --git a/jest.config.js b/jest.config.js index 6041aaa2b20..a576f4ec0c4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -83,6 +83,7 @@ module.exports = { '/public/app/plugins/datasource/grafana-pyroscope-datasource', '/public/app/plugins/datasource/grafana-testdata-datasource', '/public/app/plugins/datasource/jaeger', + '/public/app/plugins/datasource/loki', '/public/app/plugins/datasource/mysql', '/public/app/plugins/datasource/parca', '/public/app/plugins/datasource/tempo', diff --git a/package.json b/package.json index 5d6fcd944e4..cce32ec8537 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "plugin:build": "nx run-many -t build --projects='tag:scope:plugin'", "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", - "plugin:test:ci": "nx run-many -t test:ci --projects='tag:scope:plugin'", + "plugin:test:ci": "nx run-many -t test:ci --projects='tag:scope:plugin' --maxParallel=2", "plugin:i18n-extract": "nx run-many -t i18n-extract --projects='tag:scope:plugin'", "process-specs": "node --experimental-strip-types scripts/process-specs.ts", "generate-apis": "yarn process-specs && rtk-query-codegen-openapi ./scripts/generate-rtk-apis.ts", diff --git a/public/app/plugins/datasource/loki/LanguageProvider.test.ts b/public/app/plugins/datasource/loki/LanguageProvider.test.ts index b00b54393c0..02a365c7d9a 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.test.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.test.ts @@ -761,7 +761,8 @@ describe('Query imports', () => { maxLines: DEFAULT_MAX_LINES_SAMPLE, refId: 'data-samples', }, - languageProvider.getDefaultTimeRange() + // We are not interested in the time range here + expect.any(Object) ); }); @@ -782,7 +783,8 @@ describe('Query imports', () => { maxLines: 5, refId: 'data-samples', }, - languageProvider.getDefaultTimeRange() + // We are not interested in the time range here + expect.any(Object) ); }); diff --git a/public/app/plugins/datasource/loki/jest-setup.js b/public/app/plugins/datasource/loki/jest-setup.js new file mode 100644 index 00000000000..c85bf9d3a57 --- /dev/null +++ b/public/app/plugins/datasource/loki/jest-setup.js @@ -0,0 +1 @@ +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/loki/jest.config.js b/public/app/plugins/datasource/loki/jest.config.js new file mode 100644 index 00000000000..fabef448081 --- /dev/null +++ b/public/app/plugins/datasource/loki/jest.config.js @@ -0,0 +1,3 @@ +import defaultConfig from '@grafana/plugin-configs/jest/jest.config.js'; + +export default defaultConfig; diff --git a/public/app/plugins/datasource/loki/package.json b/public/app/plugins/datasource/loki/package.json index 69bf66c12a9..75a465d608c 100644 --- a/public/app/plugins/datasource/loki/package.json +++ b/public/app/plugins/datasource/loki/package.json @@ -37,6 +37,7 @@ "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/uuid": "10.0.0", + "jest": "29.7.0", "ts-node": "10.9.2", "typescript": "5.7.3", "webpack": "5.97.1" @@ -47,7 +48,9 @@ "scripts": { "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", - "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development", + "test": "jest --watch --onlyChanged", + "test:ci": "jest --maxWorkers 4" }, "packageManager": "yarn@4.9.2" } diff --git a/yarn.lock b/yarn.lock index 9eb3e4e6d45..e38aa7212a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2771,6 +2771,7 @@ __metadata: "@types/react-dom": "npm:18.3.5" "@types/uuid": "npm:10.0.0" d3-random: "npm:^3.0.1" + jest: "npm:29.7.0" lodash: "npm:4.17.21" micro-memoize: "npm:^4.1.2" react: "npm:18.3.1" From 66bebe94e064c9d6dc460254e1fc83355d5ae9cf Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:28:24 +0200 Subject: [PATCH 077/126] Scopes: Add autofocus, aria-label and search area to selector (#108906) * Add autofocus, aria-label and search area * Clean up search area * Clean up search area * Add test * Remove superflous memo * Undo values change --- public/app/features/scopes/selector/ScopesTree.tsx | 11 ++++++++++- .../features/scopes/selector/ScopesTreeSearch.tsx | 12 ++++++++++-- public/app/features/scopes/tests/tree.test.ts | 10 ++++++++++ public/locales/en-US/grafana.json | 2 +- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/public/app/features/scopes/selector/ScopesTree.tsx b/public/app/features/scopes/selector/ScopesTree.tsx index 71c7a879d63..e3fbc06e203 100644 --- a/public/app/features/scopes/selector/ScopesTree.tsx +++ b/public/app/features/scopes/selector/ScopesTree.tsx @@ -63,11 +63,20 @@ export function ScopesTree({ } } + // Used as a label and placeholder for search field + const nodeTitle = scopeNodes[tree.scopeNodeId]?.spec?.title || ''; + const searchArea = tree.scopeNodeId === '' ? '' : nodeTitle; + const lastExpandedNode = !anyChildExpanded && tree.expanded; return ( <> - + {tree.scopeNodeId === '' && !anyChildExpanded && recentScopes && diff --git a/public/app/features/scopes/selector/ScopesTreeSearch.tsx b/public/app/features/scopes/selector/ScopesTreeSearch.tsx index 99a251d20bd..e84407aa00f 100644 --- a/public/app/features/scopes/selector/ScopesTreeSearch.tsx +++ b/public/app/features/scopes/selector/ScopesTreeSearch.tsx @@ -10,11 +10,12 @@ import { TreeNode } from './types'; export interface ScopesTreeSearchProps { anyChildExpanded: boolean; + searchArea: string; treeNode: TreeNode; onNodeUpdate: (scopeNodeId: string, expanded: boolean, query: string) => void; } -export function ScopesTreeSearch({ anyChildExpanded, treeNode, onNodeUpdate }: ScopesTreeSearchProps) { +export function ScopesTreeSearch({ anyChildExpanded, treeNode, onNodeUpdate, searchArea }: ScopesTreeSearchProps) { const styles = useStyles2(getStyles); const [inputState, setInputState] = useState<{ value: string; dirty: boolean }>({ @@ -42,9 +43,16 @@ export function ScopesTreeSearch({ anyChildExpanded, treeNode, onNodeUpdate }: S return null; } + const searchLabel = t('scopes.tree.search', 'Search {{parentTitle}}', { + parentTitle: searchArea, + }); + return ( { await resetScenes([fetchNodesSpy, fetchScopeSpy]); }); + it('Gives autofocus to search field when node is expanded', async () => { + await openSelector(); + expect(screen.getByRole('textbox', { name: 'Search' })).not.toHaveFocus(); + + await expandResultApplications(); + expect(screen.getByRole('textbox', { name: 'Search Applications' })).toHaveFocus(); + }); + it('Fetches scope details on select', async () => { await openSelector(); await expandResultApplications(); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 0a72605b908..40ccbf5b567 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11874,7 +11874,7 @@ "recommended": "Recommended", "results": "Results" }, - "search": "Search" + "search": "Search {{parentTitle}}" } }, "search": { From 80d7892d6a3daec4794bec6c4f8a1f3d8a01b451 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Thu, 31 Jul 2025 10:42:19 +0200 Subject: [PATCH 078/126] Secrets: Save owner reference fields in secure value db table (#108905) * Secrets: Save owner reference fields in secure value db table * Save api group and version separately --- .../metadata/data/secure_value_create.sql | 24 +++++ .../metadata/data/secure_value_list.sql | 6 +- .../metadata/data/secure_value_read.sql | 6 +- pkg/storage/secret/metadata/query_test.go | 68 ++++++++------ .../secret/metadata/secure_value_model.go | 94 ++++++++++++++----- .../secret/metadata/secure_value_store.go | 5 +- ...l--secure_value_create-create-not-null.sql | 8 ++ .../mysql--secure_value_list-list.sql | 6 +- ...sql--secure_value_read-read-for-update.sql | 6 +- .../mysql--secure_value_read-read.sql | 6 +- ...s--secure_value_create-create-not-null.sql | 8 ++ .../postgres--secure_value_list-list.sql | 6 +- ...res--secure_value_read-read-for-update.sql | 6 +- .../postgres--secure_value_read-read.sql | 6 +- ...e--secure_value_create-create-not-null.sql | 8 ++ .../sqlite--secure_value_list-list.sql | 6 +- ...ite--secure_value_read-read-for-update.sql | 6 +- .../sqlite--secure_value_read-read.sql | 6 +- pkg/storage/secret/migrator/migrator.go | 29 ++++++ 19 files changed, 243 insertions(+), 67 deletions(-) diff --git a/pkg/storage/secret/metadata/data/secure_value_create.sql b/pkg/storage/secret/metadata/data/secure_value_create.sql index a120feaa5e5..66d404f5bd2 100644 --- a/pkg/storage/secret/metadata/data/secure_value_create.sql +++ b/pkg/storage/secret/metadata/data/secure_value_create.sql @@ -20,6 +20,18 @@ INSERT INTO {{ .Ident "secret_secure_value" }} ( {{ if .Row.Ref.Valid }} {{ .Ident "ref" }}, {{ end }} + {{ if .Row.OwnerReferenceAPIGroup.Valid }} + {{ .Ident "owner_reference_api_group" }}, + {{ end }} + {{ if .Row.OwnerReferenceAPIVersion.Valid }} + {{ .Ident "owner_reference_api_version" }}, + {{ end }} + {{ if .Row.OwnerReferenceKind.Valid }} + {{ .Ident "owner_reference_kind" }}, + {{ end }} + {{ if .Row.OwnerReferenceName.Valid }} + {{ .Ident "owner_reference_name" }}, + {{ end }} {{ .Ident "external_id" }} ) VALUES ( {{ .Arg .Row.GUID }}, @@ -43,5 +55,17 @@ INSERT INTO {{ .Ident "secret_secure_value" }} ( {{ if .Row.Ref.Valid }} {{ .Arg .Row.Ref.String }}, {{ end }} + {{ if .Row.OwnerReferenceAPIGroup.Valid }} + {{ .Arg .Row.OwnerReferenceAPIGroup.String }}, + {{ end }} + {{ if .Row.OwnerReferenceAPIVersion.Valid }} + {{ .Arg .Row.OwnerReferenceAPIVersion.String }}, + {{ end }} + {{ if .Row.OwnerReferenceKind.Valid }} + {{ .Arg .Row.OwnerReferenceKind.String }}, + {{ end }} + {{ if .Row.OwnerReferenceName.Valid }} + {{ .Arg .Row.OwnerReferenceName.String }}, + {{ end }} {{ .Arg .Row.ExternalID }} ); \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_list.sql b/pkg/storage/secret/metadata/data/secure_value_list.sql index 9c0c604b72d..5d2ecf51e34 100644 --- a/pkg/storage/secret/metadata/data/secure_value_list.sql +++ b/pkg/storage/secret/metadata/data/secure_value_list.sql @@ -14,7 +14,11 @@ SELECT {{ .Ident "ref" }}, {{ .Ident "external_id" }}, {{ .Ident "version" }}, - {{ .Ident "active" }} + {{ .Ident "active" }}, + {{ .Ident "owner_reference_api_group" }}, + {{ .Ident "owner_reference_api_version" }}, + {{ .Ident "owner_reference_kind" }}, + {{ .Ident "owner_reference_name" }} FROM {{ .Ident "secret_secure_value" }} WHERE diff --git a/pkg/storage/secret/metadata/data/secure_value_read.sql b/pkg/storage/secret/metadata/data/secure_value_read.sql index b90d54b4a5f..4f6e0ae0707 100644 --- a/pkg/storage/secret/metadata/data/secure_value_read.sql +++ b/pkg/storage/secret/metadata/data/secure_value_read.sql @@ -14,7 +14,11 @@ SELECT {{ .Ident "ref" }}, {{ .Ident "external_id" }}, {{ .Ident "active" }}, - {{ .Ident "version" }} + {{ .Ident "version" }}, + {{ .Ident "owner_reference_api_group" }}, + {{ .Ident "owner_reference_api_version" }}, + {{ .Ident "owner_reference_kind" }}, + {{ .Ident "owner_reference_name" }} FROM {{ .Ident "secret_secure_value" }} WHERE diff --git a/pkg/storage/secret/metadata/query_test.go b/pkg/storage/secret/metadata/query_test.go index 915fd261ec2..2b0c232f2c1 100644 --- a/pkg/storage/secret/metadata/query_test.go +++ b/pkg/storage/secret/metadata/query_test.go @@ -177,21 +177,25 @@ func TestSecureValueQueries(t *testing.T) { Data: &createSecureValue{ SQLTemplate: mocks.NewTestingSQLTemplate(), Row: &secureValueDB{ - GUID: "abc", - Name: "name", - Namespace: "ns", - Annotations: `{"x":"XXXX"}`, - Labels: `{"a":"AAA", "b", "BBBB"}`, - Created: 1234, - CreatedBy: "user:ryan", - Updated: 5678, - UpdatedBy: "user:cameron", - Version: 1, - Description: "description", - Keeper: toNullString(nil), - Decrypters: toNullString(nil), - Ref: toNullString(nil), - ExternalID: "extId", + GUID: "abc", + Name: "name", + Namespace: "ns", + Annotations: `{"x":"XXXX"}`, + Labels: `{"a":"AAA", "b", "BBBB"}`, + Created: 1234, + CreatedBy: "user:ryan", + Updated: 5678, + UpdatedBy: "user:cameron", + Version: 1, + Description: "description", + Keeper: toNullString(nil), + Decrypters: toNullString(nil), + Ref: toNullString(nil), + ExternalID: "extId", + OwnerReferenceAPIGroup: toNullString(nil), + OwnerReferenceAPIVersion: toNullString(nil), + OwnerReferenceKind: toNullString(nil), + OwnerReferenceName: toNullString(nil), }, }, }, @@ -200,21 +204,25 @@ func TestSecureValueQueries(t *testing.T) { Data: &createSecureValue{ SQLTemplate: mocks.NewTestingSQLTemplate(), Row: &secureValueDB{ - GUID: "abc", - Name: "name", - Namespace: "ns", - Annotations: `{"x":"XXXX"}`, - Labels: `{"a":"AAA", "b", "BBBB"}`, - Created: 1234, - CreatedBy: "user:ryan", - Updated: 5678, - UpdatedBy: "user:cameron", - Version: 1, - Description: "description", - Keeper: toNullString(ptr.To("keeper_test")), - Decrypters: toNullString(ptr.To("decrypters_test")), - Ref: toNullString(ptr.To("ref_test")), - ExternalID: "extId", + GUID: "abc", + Name: "name", + Namespace: "ns", + Annotations: `{"x":"XXXX"}`, + Labels: `{"a":"AAA", "b", "BBBB"}`, + Created: 1234, + CreatedBy: "user:ryan", + Updated: 5678, + UpdatedBy: "user:cameron", + Version: 1, + Description: "description", + Keeper: toNullString(ptr.To("keeper_test")), + Decrypters: toNullString(ptr.To("decrypters_test")), + Ref: toNullString(ptr.To("ref_test")), + ExternalID: "extId", + OwnerReferenceAPIGroup: toNullString(ptr.To("prometheus.datasource.grafana.app")), + OwnerReferenceAPIVersion: toNullString(ptr.To("v0alpha1")), + OwnerReferenceKind: toNullString(ptr.To("DataSource")), + OwnerReferenceName: toNullString(ptr.To("prom-config")), }, }, }, diff --git a/pkg/storage/secret/metadata/secure_value_model.go b/pkg/storage/secret/metadata/secure_value_model.go index 6e5c8511784..c64411bf19b 100644 --- a/pkg/storage/secret/metadata/secure_value_model.go +++ b/pkg/storage/secret/metadata/secure_value_model.go @@ -10,22 +10,26 @@ import ( secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" - "github.com/grafana/grafana/pkg/storage/secret/migrator" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ) type secureValueDB struct { // Kubernetes Metadata - GUID string - Name string - Namespace string - Annotations string // map[string]string - Labels string // map[string]string - Created int64 - CreatedBy string - Updated int64 - UpdatedBy string + GUID string + Name string + Namespace string + Annotations string // map[string]string + Labels string // map[string]string + Created int64 + CreatedBy string + Updated int64 + UpdatedBy string + OwnerReferenceAPIGroup sql.NullString + OwnerReferenceAPIVersion sql.NullString + OwnerReferenceKind sql.NullString + OwnerReferenceName sql.NullString // Kubernetes Status Active bool @@ -39,10 +43,6 @@ type secureValueDB struct { ExternalID string } -func (*secureValueDB) TableName() string { - return migrator.TableNameSecureValue -} - // toKubernetes maps a DB row into a Kubernetes resource (metadata + spec). func (sv *secureValueDB) toKubernetes() (*secretv1beta1.SecureValue, error) { annotations := make(map[string]string, 0) @@ -85,8 +85,6 @@ func (sv *secureValueDB) toKubernetes() (*secretv1beta1.SecureValue, error) { resource.Spec.Ref = &sv.Ref.String } - resource.Status.ExternalID = sv.ExternalID - // Set all meta fields here for consistency. meta, err := utils.MetaAccessor(resource) if err != nil { @@ -106,6 +104,20 @@ func (sv *secureValueDB) toKubernetes() (*secretv1beta1.SecureValue, error) { meta.SetUpdatedTimestamp(&updated) meta.SetResourceVersionInt64(sv.Updated) + hasOwnerReference := sv.OwnerReferenceAPIGroup.Valid && sv.OwnerReferenceAPIGroup.String != "" && + sv.OwnerReferenceAPIVersion.Valid && sv.OwnerReferenceAPIVersion.String != "" && + sv.OwnerReferenceKind.Valid && sv.OwnerReferenceKind.String != "" && + sv.OwnerReferenceName.Valid && sv.OwnerReferenceName.String != "" + if hasOwnerReference { + meta.SetOwnerReferences([]metav1.OwnerReference{ + { + APIVersion: schema.GroupVersion{Group: sv.OwnerReferenceAPIGroup.String, Version: sv.OwnerReferenceAPIVersion.String}.String(), + Kind: sv.OwnerReferenceKind.String, + Name: sv.OwnerReferenceName.String, + }, + }) + } + return resource, nil } @@ -179,16 +191,48 @@ func toRow(sv *secretv1beta1.SecureValue, externalID string) (*secureValueDB, er return nil, fmt.Errorf("failed to get resource version: %w", err) } + var ( + ownerReferenceAPIGroup sql.NullString + ownerReferenceAPIVersion sql.NullString + ownerReferenceKind sql.NullString + ownerReferenceName sql.NullString + ) + + ownerReferences := meta.GetOwnerReferences() + if len(ownerReferences) > 1 { + return nil, fmt.Errorf("only one owner reference is supported, found %d", len(ownerReferences)) + } + if len(ownerReferences) == 1 { + ownerReference := ownerReferences[0] + + gv, err := schema.ParseGroupVersion(ownerReference.APIVersion) + if err != nil { + return nil, fmt.Errorf("failed to parse owner reference API version %s: %w", ownerReference.APIVersion, err) + } + if gv.Group == "" { + return nil, fmt.Errorf("malformed api version %s requires / format", ownerReference.APIVersion) + } + + ownerReferenceAPIGroup = toNullString(&gv.Group) + ownerReferenceAPIVersion = toNullString(&gv.Version) + ownerReferenceKind = toNullString(&ownerReference.Kind) + ownerReferenceName = toNullString(&ownerReference.Name) + } + return &secureValueDB{ - GUID: string(sv.UID), - Name: sv.Name, - Namespace: sv.Namespace, - Annotations: annotations, - Labels: labels, - Created: meta.GetCreationTimestamp().UnixMilli(), - CreatedBy: meta.GetCreatedBy(), - Updated: updatedTimestamp, - UpdatedBy: meta.GetUpdatedBy(), + GUID: string(sv.UID), + Name: sv.Name, + Namespace: sv.Namespace, + Annotations: annotations, + Labels: labels, + Created: meta.GetCreationTimestamp().UnixMilli(), + CreatedBy: meta.GetCreatedBy(), + Updated: updatedTimestamp, + UpdatedBy: meta.GetUpdatedBy(), + OwnerReferenceAPIGroup: ownerReferenceAPIGroup, + OwnerReferenceAPIVersion: ownerReferenceAPIVersion, + OwnerReferenceKind: ownerReferenceKind, + OwnerReferenceName: ownerReferenceName, Version: sv.Status.Version, diff --git a/pkg/storage/secret/metadata/secure_value_store.go b/pkg/storage/secret/metadata/secure_value_store.go index 46001baf3ae..a9c2d0664d6 100644 --- a/pkg/storage/secret/metadata/secure_value_store.go +++ b/pkg/storage/secret/metadata/secure_value_store.go @@ -243,7 +243,9 @@ func (s *secureValueMetadataStorage) readActiveVersion(ctx context.Context, name &secureValue.Annotations, &secureValue.Labels, &secureValue.Created, &secureValue.CreatedBy, &secureValue.Updated, &secureValue.UpdatedBy, - &secureValue.Description, &secureValue.Keeper, &secureValue.Decrypters, &secureValue.Ref, &secureValue.ExternalID, &secureValue.Active, &secureValue.Version); err != nil { + &secureValue.Description, &secureValue.Keeper, &secureValue.Decrypters, &secureValue.Ref, &secureValue.ExternalID, &secureValue.Active, &secureValue.Version, + &secureValue.OwnerReferenceAPIGroup, &secureValue.OwnerReferenceAPIVersion, &secureValue.OwnerReferenceKind, &secureValue.OwnerReferenceName, + ); err != nil { return secureValueDB{}, fmt.Errorf("failed to scan secure value row: %w", err) } @@ -320,6 +322,7 @@ func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.N &row.Updated, &row.UpdatedBy, &row.Description, &row.Keeper, &row.Decrypters, &row.Ref, &row.ExternalID, &row.Version, &row.Active, + &row.OwnerReferenceAPIGroup, &row.OwnerReferenceAPIVersion, &row.OwnerReferenceKind, &row.OwnerReferenceName, ) if err != nil { diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql index aadea1d47a9..b629d32fc52 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql @@ -14,6 +14,10 @@ INSERT INTO `secret_secure_value` ( `keeper`, `decrypters`, `ref`, + `owner_reference_api_group`, + `owner_reference_api_version`, + `owner_reference_kind`, + `owner_reference_name`, `external_id` ) VALUES ( 'abc', @@ -31,5 +35,9 @@ INSERT INTO `secret_secure_value` ( 'keeper_test', 'decrypters_test', 'ref_test', + 'prometheus.datasource.grafana.app', + 'v0alpha1', + 'DataSource', + 'prom-config', 'extId' ); diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql index d73828bfae8..5faf0cca659 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql @@ -14,7 +14,11 @@ SELECT `ref`, `external_id`, `version`, - `active` + `active`, + `owner_reference_api_group`, + `owner_reference_api_version`, + `owner_reference_kind`, + `owner_reference_name` FROM `secret_secure_value` WHERE diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql index 4d8525dd0a0..f48f1a1d703 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql @@ -14,7 +14,11 @@ SELECT `ref`, `external_id`, `active`, - `version` + `version`, + `owner_reference_api_group`, + `owner_reference_api_version`, + `owner_reference_kind`, + `owner_reference_name` FROM `secret_secure_value` WHERE diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql index c42fd0037c5..3dfc6f1e0b9 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql @@ -14,7 +14,11 @@ SELECT `ref`, `external_id`, `active`, - `version` + `version`, + `owner_reference_api_group`, + `owner_reference_api_version`, + `owner_reference_kind`, + `owner_reference_name` FROM `secret_secure_value` WHERE diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql index cf43e0c130c..bbf94f89fcc 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql @@ -14,6 +14,10 @@ INSERT INTO "secret_secure_value" ( "keeper", "decrypters", "ref", + "owner_reference_api_group", + "owner_reference_api_version", + "owner_reference_kind", + "owner_reference_name", "external_id" ) VALUES ( 'abc', @@ -31,5 +35,9 @@ INSERT INTO "secret_secure_value" ( 'keeper_test', 'decrypters_test', 'ref_test', + 'prometheus.datasource.grafana.app', + 'v0alpha1', + 'DataSource', + 'prom-config', 'extId' ); diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql index 40ee42ebcf7..0095a993c35 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql @@ -14,7 +14,11 @@ SELECT "ref", "external_id", "version", - "active" + "active", + "owner_reference_api_group", + "owner_reference_api_version", + "owner_reference_kind", + "owner_reference_name" FROM "secret_secure_value" WHERE diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql index cce38e6db7a..162d97d042c 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql @@ -14,7 +14,11 @@ SELECT "ref", "external_id", "active", - "version" + "version", + "owner_reference_api_group", + "owner_reference_api_version", + "owner_reference_kind", + "owner_reference_name" FROM "secret_secure_value" WHERE diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql index 2d16c211a85..86aab54919d 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql @@ -14,7 +14,11 @@ SELECT "ref", "external_id", "active", - "version" + "version", + "owner_reference_api_group", + "owner_reference_api_version", + "owner_reference_kind", + "owner_reference_name" FROM "secret_secure_value" WHERE diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql index cf43e0c130c..bbf94f89fcc 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql @@ -14,6 +14,10 @@ INSERT INTO "secret_secure_value" ( "keeper", "decrypters", "ref", + "owner_reference_api_group", + "owner_reference_api_version", + "owner_reference_kind", + "owner_reference_name", "external_id" ) VALUES ( 'abc', @@ -31,5 +35,9 @@ INSERT INTO "secret_secure_value" ( 'keeper_test', 'decrypters_test', 'ref_test', + 'prometheus.datasource.grafana.app', + 'v0alpha1', + 'DataSource', + 'prom-config', 'extId' ); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql index 40ee42ebcf7..0095a993c35 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql @@ -14,7 +14,11 @@ SELECT "ref", "external_id", "version", - "active" + "active", + "owner_reference_api_group", + "owner_reference_api_version", + "owner_reference_kind", + "owner_reference_name" FROM "secret_secure_value" WHERE diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql index 2d16c211a85..86aab54919d 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql @@ -14,7 +14,11 @@ SELECT "ref", "external_id", "active", - "version" + "version", + "owner_reference_api_group", + "owner_reference_api_version", + "owner_reference_kind", + "owner_reference_name" FROM "secret_secure_value" WHERE diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql index 2d16c211a85..86aab54919d 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql @@ -14,7 +14,11 @@ SELECT "ref", "external_id", "active", - "version" + "version", + "owner_reference_api_group", + "owner_reference_api_version", + "owner_reference_kind", + "owner_reference_name" FROM "secret_secure_value" WHERE diff --git a/pkg/storage/secret/migrator/migrator.go b/pkg/storage/secret/migrator/migrator.go index 0b7790a875f..a208497c65a 100644 --- a/pkg/storage/secret/migrator/migrator.go +++ b/pkg/storage/secret/migrator/migrator.go @@ -152,4 +152,33 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { Cols: []string{"namespace", "label", "active"}, Type: migrator.IndexType, })) + + // Owner Reference columns + mg.AddMigration("add owner_reference_api_group column to "+TableNameSecureValue, migrator.NewAddColumnMigration(secureValueTable, &migrator.Column{ + Name: "owner_reference_api_group", + Type: migrator.DB_NVarchar, + Length: 253, // Limit enforced by K8s. + Nullable: true, + })) + + mg.AddMigration("add owner_reference_api_version column to "+TableNameSecureValue, migrator.NewAddColumnMigration(secureValueTable, &migrator.Column{ + Name: "owner_reference_api_version", + Type: migrator.DB_NVarchar, + Length: 253, // Limit enforced by K8s. + Nullable: true, + })) + + mg.AddMigration("add owner_reference_kind column to "+TableNameSecureValue, migrator.NewAddColumnMigration(secureValueTable, &migrator.Column{ + Name: "owner_reference_kind", + Type: migrator.DB_NVarchar, + Length: 253, // Limit enforced by K8s. + Nullable: true, + })) + + mg.AddMigration("add owner_reference_name column to "+TableNameSecureValue, migrator.NewAddColumnMigration(secureValueTable, &migrator.Column{ + Name: "owner_reference_name", + Type: migrator.DB_NVarchar, + Length: 253, // Limit enforced by K8s. + Nullable: true, + })) } From 8b600f40c13e279fb4489566b99cc5b86c995b75 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 08:50:01 +0000 Subject: [PATCH 079/126] Update dependency eslint-plugin-no-barrel-files to v1.2.2 (#108961) 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 e38aa7212a7..b89f00a6687 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16323,11 +16323,11 @@ __metadata: linkType: hard "eslint-plugin-no-barrel-files@npm:^1.1.1": - version: 1.2.1 - resolution: "eslint-plugin-no-barrel-files@npm:1.2.1" + version: 1.2.2 + resolution: "eslint-plugin-no-barrel-files@npm:1.2.2" dependencies: "@typescript-eslint/utils": "npm:^8.13.0" - checksum: 10/22a888dd3b49527bed0f92758f4c7f34e78397c9197688db1936d2bdad8369f9ded2a1231a6b85e54791f059784f1d7bcdbeeb21cf0b453399044a1957a153c4 + checksum: 10/9ab622f211270487016849a4da1d275890340721db3301fc90d2b96af5bb72d49934114c5e0fe1c3b69a354a9e0841b67a19e8c4fa4c6e35a02e3a3bc0973b1e languageName: node linkType: hard From c9cf44c4d77aa3bc77c230a54130fe0264cb3d7f Mon Sep 17 00:00:00 2001 From: Roberto Jimenez Sanchez Date: Thu, 31 Jul 2025 10:38:31 +0200 Subject: [PATCH 080/126] Add Git Sync team as owner of the integration tests --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b3ab77bcd19..f26cfd876aa 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -72,9 +72,12 @@ /hack/ @grafana/grafana-app-platform-squad /.air.toml @macabu +# Git Sync / App Platform Provisioning /pkg/apis/provisioning @grafana/grafana-git-ui-sync-team /public/app/features/provisioning @grafana/grafana-git-ui-sync-team /pkg/registry/apis/provisioning @grafana/grafana-git-ui-sync-team +/pkg/tests/apis/provisioning @grafana/grafana-git-ui-sync-team +# Git Sync frontend owned by frontent team as a whole. /apps/alerting/ @grafana/alerting-backend /apps/dashboard/ @grafana/grafana-app-platform-squad @grafana/dashboards-squad From f39b878b0a681152b64f15bcb200f374d16e921b Mon Sep 17 00:00:00 2001 From: Roberto Jimenez Sanchez Date: Thu, 31 Jul 2025 10:38:53 +0200 Subject: [PATCH 081/126] Add Git Sync team as owner of the Git Sync dependencies --- go.mod | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 27500ca7227..e1f5b010a2f 100644 --- a/go.mod +++ b/go.mod @@ -78,7 +78,7 @@ require ( github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group github.com/golang/snappy v1.0.0 // @grafana/alerting-backend github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group - github.com/google/go-github/v70 v70.0.0 // @grafana/grafana-app-platform-squad + github.com/google/go-github/v70 v70.0.0 // @grafana/grafana-git-ui-sync-team github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group github.com/google/wire v0.6.0 // @grafana/grafana-backend-group @@ -104,7 +104,7 @@ require ( github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group github.com/grafana/grafana-plugin-sdk-go v0.278.0 // @grafana/plugins-platform-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs - github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0 // @grafana-app-platform-squad + github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0 // @grafana/grafana-git-ui-sync-team 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 github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae // @grafana/observability-traces-and-profiling @@ -135,7 +135,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.22 // @grafana/grafana-backend-group github.com/matttproud/golang_protobuf_extensions v1.0.4 // @grafana/alerting-backend github.com/microsoft/go-mssqldb v1.8.0 // @grafana/partner-datasources - github.com/migueleliasweb/go-github-mock v1.1.0 // @grafana/grafana-app-platform-squad + github.com/migueleliasweb/go-github-mock v1.1.0 // @grafana/grafana-git-ui-sync-team github.com/mitchellh/copystructure v1.2.0 // @grafana/grafana-operator-experience-squad github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c //@grafana/identity-access-team github.com/mocktools/go-smtp-mock/v2 v2.3.1 // @grafana/grafana-backend-group From 6b1143565a15a1434f69437a7f5c4927bffad243 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Thu, 31 Jul 2025 11:25:19 +0200 Subject: [PATCH 082/126] Chore: Make cgo optional (for sqlite) (#108756) * make cgo optional for sqlite * update go.mod; check error code differently * reduce api surface even more * move test errors into sqlite package * add a comment --- apps/advisor/go.mod | 7 +++ apps/advisor/go.sum | 28 +++++++++++ apps/investigations/go.mod | 7 +++ apps/investigations/go.sum | 30 ++++++++++++ go.mod | 2 +- pkg/services/sqlstore/database_wrapper.go | 4 +- pkg/services/sqlstore/migrator/migrator.go | 5 +- .../sqlstore/migrator/sqlite_dialect.go | 23 ++-------- pkg/services/sqlstore/session_test.go | 4 +- pkg/storage/unified/sql/backend.go | 7 ++- pkg/storage/unified/sql/backend_test.go | 4 +- pkg/util/sqlite/sqlite_cgo.go | 46 +++++++++++++++++++ pkg/util/sqlite/sqlite_nocgo.go | 24 ++++++++++ pkg/util/xorm/dialect_sqlite3.go | 8 +--- pkg/util/xorm/xorm_test.go | 2 +- 15 files changed, 160 insertions(+), 41 deletions(-) create mode 100644 pkg/util/sqlite/sqlite_cgo.go create mode 100644 pkg/util/sqlite/sqlite_nocgo.go diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 33afd0a0ae6..c232ed6d95e 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -79,6 +79,7 @@ require ( github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e // indirect github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/elazarl/goproxy v1.7.2 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect @@ -190,6 +191,7 @@ 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/ncruces/go-strftime v0.1.9 // indirect github.com/nikunjy/rules v1.5.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect @@ -213,6 +215,7 @@ require ( github.com/prometheus/exporter-toolkit v0.14.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // 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 github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -284,6 +287,10 @@ require ( k8s.io/component-base v0.33.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect + modernc.org/libc v1.65.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.10.0 // indirect + modernc.org/sqlite v1.37.0 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 7adf9d1eb21..bc8bd30f88b 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -944,6 +944,8 @@ 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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nikunjy/rules v1.5.0 h1:KJDSLOsFhwt7kcXUyZqwkgrQg5YoUwj+TVu6ItCQShw= github.com/nikunjy/rules v1.5.0/go.mod h1:TlZtZdBChrkqi8Lr2AXocme8Z7EsbxtFdDoKeI6neBQ= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -1056,6 +1058,8 @@ github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2z github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +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= @@ -1910,6 +1914,30 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA= +modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc= +modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY= +modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= +modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y= +modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= +modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index f1bb896a0ff..3a2053ac21d 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -49,6 +49,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/elazarl/goproxy v1.7.2 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect @@ -136,6 +137,7 @@ 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/ncruces/go-strftime v0.1.9 // indirect github.com/nikunjy/rules v1.5.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect @@ -157,6 +159,7 @@ require ( github.com/prometheus/exporter-toolkit v0.14.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect @@ -213,6 +216,10 @@ require ( k8s.io/client-go v0.33.3 // indirect k8s.io/component-base v0.33.3 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect + modernc.org/libc v1.65.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.10.0 // indirect + modernc.org/sqlite v1.37.0 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index b17f4ea40a5..3cc1f3d148a 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -170,6 +170,8 @@ 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-20250410090211-143e6b272ad4 h1:LGTt2LtYX8vaai32d+c9L0sMcP+Dg9w1kO6+lbsxxYg= github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4/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= @@ -540,6 +542,8 @@ 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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nikunjy/rules v1.5.0 h1:KJDSLOsFhwt7kcXUyZqwkgrQg5YoUwj+TVu6ItCQShw= github.com/nikunjy/rules v1.5.0/go.mod h1:TlZtZdBChrkqi8Lr2AXocme8Z7EsbxtFdDoKeI6neBQ= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= @@ -626,6 +630,8 @@ github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlT github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +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= @@ -994,6 +1000,30 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA= +modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc= +modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY= +modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= +modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y= +modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= +modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +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-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= diff --git a/go.mod b/go.mod index e1f5b010a2f..7fe6de3bf42 100644 --- a/go.mod +++ b/go.mod @@ -221,6 +221,7 @@ require ( k8s.io/kube-aggregator v0.33.3 // @grafana/grafana-app-platform-squad k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // @grafana/grafana-app-platform-squad k8s.io/utils v0.0.0-20241210054802-24370beab758 // @grafana/partner-datasources + modernc.org/sqlite v1.37.0 // @grafana/grafana-backend-group pgregory.net/rapid v1.2.0 // @grafana/grafana-operator-experience-squad sigs.k8s.io/randfill v1.0.0 // @grafana/grafana-app-platform-squad sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // @grafana-app-platform-squad @@ -596,7 +597,6 @@ require ( modernc.org/libc v1.65.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.10.0 // indirect - modernc.org/sqlite v1.37.0 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/yaml v1.5.0 // indirect diff --git a/pkg/services/sqlstore/database_wrapper.go b/pkg/services/sqlstore/database_wrapper.go index f691cb0c5a3..6a033b3b248 100644 --- a/pkg/services/sqlstore/database_wrapper.go +++ b/pkg/services/sqlstore/database_wrapper.go @@ -10,9 +10,9 @@ import ( "github.com/gchaincl/sqlhooks" "github.com/go-sql-driver/mysql" + "github.com/grafana/grafana/pkg/util/sqlite" "github.com/grafana/grafana/pkg/util/xorm/core" "github.com/lib/pq" - "github.com/mattn/go-sqlite3" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -42,7 +42,7 @@ func init() { // database queries. It also registers the metrics. func WrapDatabaseDriverWithHooks(dbType string, tracer tracing.Tracer) string { drivers := map[string]driver.Driver{ - migrator.SQLite: &sqlite3.SQLiteDriver{}, + migrator.SQLite: &sqlite.Driver{}, migrator.MySQL: &mysql.MySQLDriver{}, migrator.Postgres: &pq.Driver{}, } diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index d8e55c4ee71..75ebf8e540b 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -2,15 +2,14 @@ package migrator import ( "context" - "errors" "fmt" "strings" "time" _ "github.com/go-sql-driver/mysql" "github.com/golang-migrate/migrate/v4/database" + "github.com/grafana/grafana/pkg/util/sqlite" _ "github.com/lib/pq" - "github.com/mattn/go-sqlite3" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -326,7 +325,7 @@ func (mg *Migrator) doMigration(ctx context.Context, m Migration) error { err := mg.exec(ctx, m, sess) // if we get an sqlite busy/locked error, sleep 100ms and try again cnt := 0 - for cnt < 3 && (errors.Is(err, sqlite3.ErrLocked) || errors.Is(err, sqlite3.ErrBusy)) { + for cnt < 3 && sqlite.IsBusyOrLocked(err) { cnt++ logger.Debug("Database locked, sleeping then retrying", "error", err, "sql", sql) span.AddEvent("Database locked, sleeping then retrying", diff --git a/pkg/services/sqlstore/migrator/sqlite_dialect.go b/pkg/services/sqlstore/migrator/sqlite_dialect.go index 6c0e77972d8..d4302c18181 100644 --- a/pkg/services/sqlstore/migrator/sqlite_dialect.go +++ b/pkg/services/sqlstore/migrator/sqlite_dialect.go @@ -1,12 +1,10 @@ package migrator import ( - "errors" "fmt" "strings" - "github.com/mattn/go-sqlite3" - + "github.com/grafana/grafana/pkg/util/sqlite" "github.com/grafana/grafana/pkg/util/xorm" ) @@ -139,27 +137,12 @@ func (db *SQLite3) TruncateDBTables(engine *xorm.Engine) error { return nil } -func (db *SQLite3) isThisError(err error, errcode int) bool { - var driverErr sqlite3.Error - if errors.As(err, &driverErr) { - if int(driverErr.ExtendedCode) == errcode { - return true - } - } - - return false -} - func (db *SQLite3) ErrorMessage(err error) string { - var driverErr sqlite3.Error - if errors.As(err, &driverErr) { - return driverErr.Error() - } - return "" + return sqlite.ErrorMessage(err) } func (db *SQLite3) IsUniqueConstraintViolation(err error) bool { - return db.isThisError(err, int(sqlite3.ErrConstraintUnique)) || db.isThisError(err, int(sqlite3.ErrConstraintPrimaryKey)) + return sqlite.IsUniqueConstraintViolation(err) } func (db *SQLite3) IsDeadlock(err error) bool { diff --git a/pkg/services/sqlstore/session_test.go b/pkg/services/sqlstore/session_test.go index a30f11e6d11..0f2d0629a47 100644 --- a/pkg/services/sqlstore/session_test.go +++ b/pkg/services/sqlstore/session_test.go @@ -6,10 +6,10 @@ import ( "fmt" "testing" - "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/util/sqlite" ) func TestIntegration_RetryingDisabled(t *testing.T) { @@ -144,7 +144,7 @@ func getRetryErrors(t *testing.T, store *SQLStore) []error { var retryErrors []error switch store.GetDialect().DriverName() { case migrator.SQLite: - retryErrors = []error{sqlite3.Error{Code: sqlite3.ErrBusy}, sqlite3.Error{Code: sqlite3.ErrLocked}} + retryErrors = []error{sqlite.TestErrBusy, sqlite.TestErrLocked} } if len(retryErrors) == 0 { diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index a23cf9469ea..7733de8ff91 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -10,9 +10,9 @@ import ( "time" "github.com/go-sql-driver/mysql" + "github.com/grafana/grafana/pkg/util/sqlite" "github.com/jackc/pgx/v5/pgconn" "github.com/lib/pq" - "github.com/mattn/go-sqlite3" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" @@ -389,9 +389,8 @@ func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64, // IsRowAlreadyExistsError checks if the error is the result of the row inserted already existing. func IsRowAlreadyExistsError(err error) bool { - var sqlite sqlite3.Error - if errors.As(err, &sqlite) { - return sqlite.ExtendedCode == sqlite3.ErrConstraintUnique + if sqlite.IsUniqueConstraintViolation(err) { + return true } var pg *pgconn.PgError diff --git a/pkg/storage/unified/sql/backend_test.go b/pkg/storage/unified/sql/backend_test.go index 92989e8454a..534b393065e 100644 --- a/pkg/storage/unified/sql/backend_test.go +++ b/pkg/storage/unified/sql/backend_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -18,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" "github.com/grafana/grafana/pkg/storage/unified/sql/test" + "github.com/grafana/grafana/pkg/util/sqlite" "github.com/grafana/grafana/pkg/util/testutil" ) @@ -242,7 +242,7 @@ func TestBackend_create(t *testing.T) { ) b.SQLMock.ExpectCommit() b.SQLMock.ExpectBegin() - b.SQLMock.ExpectExec("insert resource").WillReturnError(sqlite3.Error{Code: sqlite3.ErrConstraint, ExtendedCode: sqlite3.ErrConstraintUnique}) + b.SQLMock.ExpectExec("insert resource").WillReturnError(sqlite.TestErrUniqueConstraintViolation) b.SQLMock.ExpectRollback() // First we insert the resource successfully. This is what the happy path test does as well. diff --git a/pkg/util/sqlite/sqlite_cgo.go b/pkg/util/sqlite/sqlite_cgo.go new file mode 100644 index 00000000000..de18511741d --- /dev/null +++ b/pkg/util/sqlite/sqlite_cgo.go @@ -0,0 +1,46 @@ +//go:build cgo + +package sqlite + +import ( + "errors" + + "github.com/mattn/go-sqlite3" +) + +type Driver = sqlite3.SQLiteDriver + +// The errors below are used in tests to simulate specific SQLite errors. It's a temporary solution +// until we rewrite the tests not to depend on the sqlite3 package internals directly. +var ( + TestErrUniqueConstraintViolation = sqlite3.Error{Code: sqlite3.ErrConstraint, ExtendedCode: sqlite3.ErrConstraintUnique} + TestErrBusy = sqlite3.Error{Code: sqlite3.ErrBusy} + TestErrLocked = sqlite3.Error{Code: sqlite3.ErrLocked} +) + +func IsBusyOrLocked(err error) bool { + var sqliteErr sqlite3.Error + if errors.As(err, &sqliteErr) { + return sqliteErr.Code == sqlite3.ErrLocked || sqliteErr.Code == sqlite3.ErrBusy + } + return false +} + +func IsUniqueConstraintViolation(err error) bool { + var sqliteErr sqlite3.Error + if errors.As(err, &sqliteErr) { + return sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique || sqliteErr.ExtendedCode == sqlite3.ErrConstraintPrimaryKey + } + return false +} + +func ErrorMessage(err error) string { + if err == nil { + return "" + } + var sqliteErr sqlite3.Error + if errors.As(err, &sqliteErr) { + return sqliteErr.Error() + } + return err.Error() +} diff --git a/pkg/util/sqlite/sqlite_nocgo.go b/pkg/util/sqlite/sqlite_nocgo.go new file mode 100644 index 00000000000..e3b9a0429d3 --- /dev/null +++ b/pkg/util/sqlite/sqlite_nocgo.go @@ -0,0 +1,24 @@ +//go:build !cgo + +package sqlite + +import "modernc.org/sqlite" + +// +// FIXME (@zserge) +// +// This non-CGo "implementation" is merely a stub to make Grafana compile without CGo. +// Any attempts to actually use this driver are likely to fail at runtime in the most brutal ways. +// + +type Driver = sqlite.Driver + +func IsBusyOrLocked(err error) bool { + return false // FIXME +} +func IsUniqueConstraintViolation(err error) bool { + return false // FIXME +} +func ErrorMessage(err error) string { + return "" // FIXME +} diff --git a/pkg/util/xorm/dialect_sqlite3.go b/pkg/util/xorm/dialect_sqlite3.go index 381a38bb490..4c235ee1c90 100644 --- a/pkg/util/xorm/dialect_sqlite3.go +++ b/pkg/util/xorm/dialect_sqlite3.go @@ -11,8 +11,8 @@ import ( "regexp" "strings" + "github.com/grafana/grafana/pkg/util/sqlite" "github.com/grafana/grafana/pkg/util/xorm/core" - sqlite "github.com/mattn/go-sqlite3" ) var ( @@ -476,11 +476,7 @@ func (db *sqlite3) Filters() []core.Filter { } func (db *sqlite3) RetryOnError(err error) bool { - var sqlError sqlite.Error - if errors.As(err, &sqlError) && (sqlError.Code == sqlite.ErrLocked || sqlError.Code == sqlite.ErrBusy) { - return true - } - return false + return sqlite.IsBusyOrLocked(err) } type sqlite3Driver struct { diff --git a/pkg/util/xorm/xorm_test.go b/pkg/util/xorm/xorm_test.go index f2939fbf9a8..281282a1ef8 100644 --- a/pkg/util/xorm/xorm_test.go +++ b/pkg/util/xorm/xorm_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - _ "github.com/mattn/go-sqlite3" + _ "github.com/grafana/grafana/pkg/util/sqlite" "github.com/stretchr/testify/require" ) From d71924ea7bcf4e830f4b8d9d5ef064d2088f9618 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:39:29 +0100 Subject: [PATCH 083/126] Update dependency stylelint to v16.23.0 (#108963) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 154 +++++++++++++++++++++++++-------------------------- 2 files changed, 77 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index cce32ec8537..3538a0d8b92 100644 --- a/package.json +++ b/package.json @@ -243,7 +243,7 @@ "sass-loader": "16.0.5", "smtp-tester": "^2.1.0", "style-loader": "4.0.0", - "stylelint": "16.14.1", + "stylelint": "16.23.0", "stylelint-config-sass-guidelines": "12.1.0", "terser-webpack-plugin": "5.3.14", "testing-library-selector": "0.3.1", diff --git a/yarn.lock b/yarn.lock index b89f00a6687..1841e4a110e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1713,19 +1713,19 @@ __metadata: languageName: node linkType: hard -"@csstools/css-parser-algorithms@npm:^3.0.1, @csstools/css-parser-algorithms@npm:^3.0.4": - version: 3.0.4 - resolution: "@csstools/css-parser-algorithms@npm:3.0.4" +"@csstools/css-parser-algorithms@npm:^3.0.1, @csstools/css-parser-algorithms@npm:^3.0.5": + version: 3.0.5 + resolution: "@csstools/css-parser-algorithms@npm:3.0.5" peerDependencies: - "@csstools/css-tokenizer": ^3.0.3 - checksum: 10/dfb6926218d9f8ba25d8b43ea46c03863c819481f8c55e4de4925780eaab9e6bcd6bead1d56b4ef82d09fcd9d69a7db2750fa9db08eece9470fd499dc76d0edb + "@csstools/css-tokenizer": ^3.0.4 + checksum: 10/e93083b5cb36a3c1e7a47ce10cf62961d05bd1e4c608bb3ee50186ff740157ab0ec16a3956f7b86251efd10703034d849693201eea858ae904848c68d2d46ada languageName: node linkType: hard -"@csstools/css-tokenizer@npm:^3.0.1, @csstools/css-tokenizer@npm:^3.0.3": - version: 3.0.3 - resolution: "@csstools/css-tokenizer@npm:3.0.3" - checksum: 10/6baa3160e426e1f177b8f10d54ec7a4a596090f65a05f16d7e9e4da049962a404eabc5f885f4867093702c259cd4080ac92a438326e22dea015201b3e71f5bbb +"@csstools/css-tokenizer@npm:^3.0.1, @csstools/css-tokenizer@npm:^3.0.4": + version: 3.0.4 + resolution: "@csstools/css-tokenizer@npm:3.0.4" + checksum: 10/eb6c84c086312f6bb8758dfe2c85addd7475b0927333c5e39a4d59fb210b9810f8c346972046f95e60a721329cffe98895abe451e51de753ad1ca7a8c24ec65f languageName: node linkType: hard @@ -1739,13 +1739,13 @@ __metadata: languageName: node linkType: hard -"@csstools/media-query-list-parser@npm:^4.0.2": - version: 4.0.2 - resolution: "@csstools/media-query-list-parser@npm:4.0.2" +"@csstools/media-query-list-parser@npm:^4.0.3": + version: 4.0.3 + resolution: "@csstools/media-query-list-parser@npm:4.0.3" peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.4 - "@csstools/css-tokenizer": ^3.0.3 - checksum: 10/8aae6337d21255d34e4f6dc6df213566e35bb769fe131006ea4200b643773f3213f8ed0ab011cd85dbe3426766c408d0fe1d04d18e821add9ae7f29cda0a8b26 + "@csstools/css-parser-algorithms": ^3.0.5 + "@csstools/css-tokenizer": ^3.0.4 + checksum: 10/ac4e34c21a1c7fc8b788274f316c29ff2f07e7a08996d27b9beb06454666591be9946362c1b7c4d342558c278c8e7d0e35655043e47a9ffd94c3fdb292fbe020 languageName: node linkType: hard @@ -4805,12 +4805,10 @@ __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 +"@keyv/serialize@npm:^1.1.0": + version: 1.1.0 + resolution: "@keyv/serialize@npm:1.1.0" + checksum: 10/73b88b69e07847b67d443efcd4949955aa7ac0f998f68e6cfe9053307c1ea3caeabe3f6f1c6a99b1b32bb9a6698c8a5c452f96246fe77d98f1cba0c2faae4616 languageName: node linkType: hard @@ -12558,13 +12556,13 @@ __metadata: languageName: node linkType: hard -"cacheable@npm:^1.8.7": - version: 1.8.7 - resolution: "cacheable@npm:1.8.7" +"cacheable@npm:^1.10.3": + version: 1.10.3 + resolution: "cacheable@npm:1.10.3" dependencies: - hookified: "npm:^1.6.0" - keyv: "npm:^5.2.3" - checksum: 10/dce96e947c5b879a58ce024fd2d08a4c44ee328e8406cd6243da2d0e3a17579d63108c80e9272f64190d2d2f00e26a307b3e3ff65b6665abc70956714442897b + hookified: "npm:^1.10.0" + keyv: "npm:^5.4.0" + checksum: 10/1afb8633e0a9c43b0e1a3db43532e2ebff7b7fa6f358925c2ba67abf396dd57c53861e0574bfb1099d6f1a12e9159a4e14f3f993377e8b6ddc32aba3c3dedb8f languageName: node linkType: hard @@ -14843,7 +14841,7 @@ __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.4.0, debug@npm:^4.4.1": +"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.4.0, debug@npm:^4.4.1": version: 4.4.1 resolution: "debug@npm:4.4.1" dependencies: @@ -17054,12 +17052,12 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^10.0.5": - version: 10.0.5 - resolution: "file-entry-cache@npm:10.0.5" +"file-entry-cache@npm:^10.1.3": + version: 10.1.3 + resolution: "file-entry-cache@npm:10.1.3" dependencies: - flat-cache: "npm:^6.1.5" - checksum: 10/57439b39635e75aa900ccdaad9167b85a500a559dd8f0902509189849784e9255d43cd3d8307f918dbd19f48b02562b5e354bcf7bfa23afe3985485504eb7a5e + flat-cache: "npm:^6.1.12" + checksum: 10/dcf7d0b7ca76ebd124e6a5b7fdcab25d18b087da17c4979a680ccc0e88baf5f6727aae0f865d47188da793863f65d6119ce6eb4301e407ea3879d70e97b5c487 languageName: node linkType: hard @@ -17321,14 +17319,14 @@ __metadata: languageName: node linkType: hard -"flat-cache@npm:^6.1.5": - version: 6.1.5 - resolution: "flat-cache@npm:6.1.5" +"flat-cache@npm:^6.1.12": + version: 6.1.12 + resolution: "flat-cache@npm:6.1.12" dependencies: - cacheable: "npm:^1.8.7" - flatted: "npm:^3.3.2" - hookified: "npm:^1.6.0" - checksum: 10/c98d7635316ceee08b74c1dd3f05eda6a5f2f073688af927de68193ca992ab09eb9f0ee7d5fac40f53280844f11cf368b90dc43f73fee358165dc84e13828674 + cacheable: "npm:^1.10.3" + flatted: "npm:^3.3.3" + hookified: "npm:^1.10.0" + checksum: 10/053976363bf2de150c77b4b4c9702b4f7fdebf4fa2f60e5b042606847d4556681da3ee489ed02e9f23cabeafe771d0b3bdf8a3f7fdc1945b55d5e42222d6ffc6 languageName: node linkType: hard @@ -17341,10 +17339,10 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.2.9, flatted@npm:^3.3.2": - version: 3.3.2 - resolution: "flatted@npm:3.3.2" - checksum: 10/ac3c159742e01d0e860a861164bcfd35bb567ccbebb8a0dd041e61cf3c64a435b917dd1e7ed1c380c2ebca85735fb16644485ec33665bc6aafc3b316aa1eed44 +"flatted@npm:^3.2.9, flatted@npm:^3.3.3": + version: 3.3.3 + resolution: "flatted@npm:3.3.3" + checksum: 10/8c96c02fbeadcf4e8ffd0fa24983241e27698b0781295622591fc13585e2f226609d95e422bcf2ef044146ffacb6b68b1f20871454eddf75ab3caa6ee5f4a1fe languageName: node linkType: hard @@ -18617,7 +18615,7 @@ __metadata: slate-react: "npm:0.22.10" smtp-tester: "npm:^2.1.0" style-loader: "npm:4.0.0" - stylelint: "npm:16.14.1" + stylelint: "npm:16.23.0" stylelint-config-sass-guidelines: "npm:12.1.0" swagger-ui-react: "npm:5.27.0" symbol-observable: "npm:4.0.0" @@ -18947,10 +18945,10 @@ __metadata: languageName: node linkType: hard -"hookified@npm:^1.6.0": - version: 1.7.0 - resolution: "hookified@npm:1.7.0" - checksum: 10/87fb8f2ae170f28b1e0b903f2b0b40fb2a92a0364baab7c54121db1c871a2e81c589d778b48e1a35462181b8840764caf6cac3744330e5687a05a4b4d2ad729d +"hookified@npm:^1.10.0": + version: 1.11.0 + resolution: "hookified@npm:1.11.0" + checksum: 10/a9cf288dcfaed9a96aaff9a725f74ea48690df45f7a42b73bef13faf6f5d8be6b3dd10e322b10fce2044e34982422a1efb25d462fc2c0c5d438151070ffee23f languageName: node linkType: hard @@ -19494,7 +19492,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^7.0.0, ignore@npm:^7.0.3": +"ignore@npm:^7.0.0, ignore@npm:^7.0.5": version: 7.0.5 resolution: "ignore@npm:7.0.5" checksum: 10/f134b96a4de0af419196f52c529d5c6120c4456ff8a6b5a14ceaaa399f883e15d58d2ce651c9b69b9388491d4669dda47285d307e827de9304a53a1824801bc6 @@ -21727,12 +21725,12 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^5.2.3": - version: 5.2.3 - resolution: "keyv@npm:5.2.3" +"keyv@npm:^5.4.0": + version: 5.4.0 + resolution: "keyv@npm:5.4.0" dependencies: - "@keyv/serialize": "npm:^1.0.2" - checksum: 10/47b4e9deb33e6a80e5ea79f3022ed3a14bc9fe553b7527ffff0a70b10c7a6c1a5d7e49b9bcfdbd8e8b9fb4632d68baa19d09e82628bcf853103e750e56d49a9e + "@keyv/serialize": "npm:^1.1.0" + checksum: 10/2d0f54505b1650238d3efb547332dc12e5e80c6a6b8e8654c3d4630461a69e9671fef87b5c3df11dc7bfc904cebfba5a74fe56edb9bc8ad994cecfe0dc155b97 languageName: node linkType: hard @@ -21764,10 +21762,10 @@ __metadata: languageName: node linkType: hard -"known-css-properties@npm:^0.35.0": - version: 0.35.0 - resolution: "known-css-properties@npm:0.35.0" - checksum: 10/a6f3f271a94913c72b29e59bd1e96836b0b5427c5dd9969f4673026cd39f7f441b5e8d0b704b0a830c43d745a5f7ca98d41d99961dc4c008ebf756545bada85c +"known-css-properties@npm:^0.37.0": + version: 0.37.0 + resolution: "known-css-properties@npm:0.37.0" + checksum: 10/9bddf2eb980f707567e3fbf8f2daaca27f5d544f4a49eab26d696ae33f54410f82f0a3032097b083d9e889e285586696c7b4324e6393103e7d71a46ca4438bfd languageName: node linkType: hard @@ -26019,7 +26017,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.5.6, postcss@npm:^8.4.33, postcss@npm:^8.4.40, postcss@npm:^8.5.1": +"postcss@npm:8.5.6, postcss@npm:^8.4.33, postcss@npm:^8.4.40, postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" dependencies: @@ -30321,13 +30319,13 @@ __metadata: languageName: node linkType: hard -"stylelint@npm:16.14.1, stylelint@npm:^16.8.2": - version: 16.14.1 - resolution: "stylelint@npm:16.14.1" +"stylelint@npm:16.23.0, stylelint@npm:^16.8.2": + version: 16.23.0 + resolution: "stylelint@npm:16.23.0" dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.4" - "@csstools/css-tokenizer": "npm:^3.0.3" - "@csstools/media-query-list-parser": "npm:^4.0.2" + "@csstools/css-parser-algorithms": "npm:^3.0.5" + "@csstools/css-tokenizer": "npm:^3.0.4" + "@csstools/media-query-list-parser": "npm:^4.0.3" "@csstools/selector-specificity": "npm:^5.0.0" "@dual-bundle/import-meta-resolve": "npm:^4.1.0" balanced-match: "npm:^2.0.0" @@ -30335,37 +30333,37 @@ __metadata: cosmiconfig: "npm:^9.0.0" css-functions-list: "npm:^3.2.3" css-tree: "npm:^3.1.0" - debug: "npm:^4.3.7" + debug: "npm:^4.4.1" fast-glob: "npm:^3.3.3" fastest-levenshtein: "npm:^1.0.16" - file-entry-cache: "npm:^10.0.5" + file-entry-cache: "npm:^10.1.3" global-modules: "npm:^2.0.0" globby: "npm:^11.1.0" globjoin: "npm:^0.1.4" html-tags: "npm:^3.3.1" - ignore: "npm:^7.0.3" + ignore: "npm:^7.0.5" imurmurhash: "npm:^0.1.4" is-plain-object: "npm:^5.0.0" - known-css-properties: "npm:^0.35.0" + known-css-properties: "npm:^0.37.0" mathml-tag-names: "npm:^2.1.3" meow: "npm:^13.2.0" micromatch: "npm:^4.0.8" normalize-path: "npm:^3.0.0" picocolors: "npm:^1.1.1" - postcss: "npm:^8.5.1" + postcss: "npm:^8.5.6" postcss-resolve-nested-selector: "npm:^0.1.6" postcss-safe-parser: "npm:^7.0.1" - postcss-selector-parser: "npm:^7.0.0" + postcss-selector-parser: "npm:^7.1.0" postcss-value-parser: "npm:^4.2.0" resolve-from: "npm:^5.0.0" string-width: "npm:^4.2.3" - supports-hyperlinks: "npm:^3.1.0" + supports-hyperlinks: "npm:^3.2.0" svg-tags: "npm:^1.0.0" table: "npm:^6.9.0" write-file-atomic: "npm:^5.0.1" bin: stylelint: bin/stylelint.mjs - checksum: 10/2c0c95e5ee77d946efdbe58573da75ff4b12bc89c220e78b69f0c2f94251f5e5a1096f5ab583f28f56ae14f298b1335e212d52fe066719d63b8a41d6cc8083b6 + checksum: 10/5a92aaaefbc22aba032d6a08df6d0cc8d72cc36e0cd1210e2ae2b8918b349a12d05d562daa1962b8fe0883835616081a0dd76e3d1440ab0bcda1fffa38cd3312 languageName: node linkType: hard @@ -30410,13 +30408,13 @@ __metadata: languageName: node linkType: hard -"supports-hyperlinks@npm:^3.1.0": - version: 3.1.0 - resolution: "supports-hyperlinks@npm:3.1.0" +"supports-hyperlinks@npm:^3.1.0, supports-hyperlinks@npm:^3.2.0": + version: 3.2.0 + resolution: "supports-hyperlinks@npm:3.2.0" dependencies: has-flag: "npm:^4.0.0" supports-color: "npm:^7.0.0" - checksum: 10/e893fb035ecd86e42c5225dc1cd24db56eb950ed77b2e8f59c7aaf2836b8b2ef276ffd11f0df88b0b12184832aa2333f875eefcb74d3c47ed2633b6b41d4be43 + checksum: 10/f7924de6049fc30bc808f98d3561318c1a4e3d55d786f9fede5e23dc5a7b0f625485bd1143135b496d521ccd0110463f2c077eb71a4ce0cf783b8b31f7909242 languageName: node linkType: hard From e7cfe0c0237394f6c87d01ad4a5e6f29954212fb Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Thu, 31 Jul 2025 11:49:07 +0200 Subject: [PATCH 084/126] Dashboards: Add feature toggle to use dashboard level time macros in scenes (#108670) * Add feature toggle to use dashboard level time macros in scenes * Add test * remove unregister time macro as it was removed from scenes --- .../src/types/featureToggles.gen.ts | 4 + pkg/services/dashboards/store_mock.go | 2 - 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 ++++ public/app/app.ts | 8 +- .../scene/DashboardLevelTimeMacro.test.ts | 74 +++++++++++++++++++ .../scene/DashboardLevelTimeMacro.ts | 33 +++++++++ 9 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/DashboardLevelTimeMacro.test.ts create mode 100644 public/app/features/dashboard-scene/scene/DashboardLevelTimeMacro.ts diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e45d19454a7..f8d6a827c20 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1074,6 +1074,10 @@ export interface FeatureToggles { */ dashboardDsAdHocFiltering?: boolean; /** + * Supports __from and __to macros that always use the dashboard level time range + */ + dashboardLevelTimeMacros?: boolean; + /** * Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications. * @default false */ diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 9185887aec9..74259652fbf 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -90,7 +90,6 @@ func (_m *FakeDashboardStore) CountInOrg(ctx context.Context, orgID int64, isFol return r0, r1 } - // DeleteDashboard provides a mock function with given fields: ctx, cmd func (_m *FakeDashboardStore) DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) error { ret := _m.Called(ctx, cmd) @@ -127,7 +126,6 @@ func (_m *FakeDashboardStore) DeleteDashboardsInFolders(ctx context.Context, req return r0 } - // FindDashboards provides a mock function with given fields: ctx, query func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { ret := _m.Called(ctx, query) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 07bc3cdcc6e..661313c4d30 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1857,6 +1857,13 @@ var ( Owner: grafanaDataProSquad, FrontendOnly: true, }, + { + Name: "dashboardLevelTimeMacros", + Description: "Supports __from and __to macros that always use the dashboard level time range", + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + FrontendOnly: true, + }, { Name: "alertmanagerRemoteSecondaryWithRemoteState", Description: "Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 67f899018d8..0b7c3e381db 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -240,5 +240,6 @@ alertingNotificationHistory,experimental,@grafana/alerting-squad,false,false,fal pluginAssetProvider,experimental,@grafana/plugins-platform-backend,false,true,false unifiedStorageSearchDualReaderEnabled,experimental,@grafana/search-and-storage,false,false,false dashboardDsAdHocFiltering,experimental,@grafana/datapro,false,false,true +dashboardLevelTimeMacros,experimental,@grafana/dashboards-squad,false,false,true alertmanagerRemoteSecondaryWithRemoteState,experimental,@grafana/alerting-squad,false,false,false adhocFiltersInTooltips,experimental,@grafana/datapro,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 236516152f5..9aee8de5873 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -971,6 +971,10 @@ const ( // Enables adhoc filtering support for the dashboard datasource FlagDashboardDsAdHocFiltering = "dashboardDsAdHocFiltering" + // FlagDashboardLevelTimeMacros + // Supports __from and __to macros that always use the dashboard level time range + FlagDashboardLevelTimeMacros = "dashboardLevelTimeMacros" + // FlagAlertmanagerRemoteSecondaryWithRemoteState // Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications. FlagAlertmanagerRemoteSecondaryWithRemoteState = "alertmanagerRemoteSecondaryWithRemoteState" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 2ee75811760..e085dda6279 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -836,6 +836,19 @@ "frontend": true } }, + { + "metadata": { + "name": "dashboardLevelTimeMacros", + "resourceVersion": "1753435849295", + "creationTimestamp": "2025-07-25T09:30:49Z" + }, + "spec": { + "description": "Supports __from and __to macros that always use the dashboard level time range", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, { "metadata": { "name": "dashboardNewLayouts", diff --git a/public/app/app.ts b/public/app/app.ts index 0e3ce53e238..5c48558c30c 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -50,7 +50,7 @@ import { setPanelRenderer, setPluginPage, } from '@grafana/runtime/internal'; -import { loadResources as loadScenesResources } from '@grafana/scenes'; +import { loadResources as loadScenesResources, sceneUtils } from '@grafana/scenes'; import config, { updateConfig } from 'app/core/config'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; @@ -82,6 +82,7 @@ import { initAlerting } from './features/alerting/unified/initAlerting'; import { initAuthConfig } from './features/auth-config'; import { getTimeSrv } from './features/dashboard/services/TimeSrv'; import { EmbeddedDashboardLazy } from './features/dashboard-scene/embedding/EmbeddedDashboardLazy'; +import { DashboardLevelTimeMacro } from './features/dashboard-scene/scene/DashboardLevelTimeMacro'; import { initGrafanaLive } from './features/live'; import { PanelDataErrorView } from './features/panel/components/PanelDataErrorView'; import { PanelRenderer } from './features/panel/components/PanelRenderer'; @@ -284,6 +285,11 @@ export class GrafanaApp { initializeCrashDetection(); } + if (config.featureToggles.dashboardLevelTimeMacros) { + sceneUtils.registerVariableMacro('__from', DashboardLevelTimeMacro, true); + sceneUtils.registerVariableMacro('__to', DashboardLevelTimeMacro, true); + } + const root = createRoot(document.getElementById('reactRoot')!); root.render( createElement(AppWrapper, { diff --git a/public/app/features/dashboard-scene/scene/DashboardLevelTimeMacro.test.ts b/public/app/features/dashboard-scene/scene/DashboardLevelTimeMacro.test.ts new file mode 100644 index 00000000000..d55c4940f52 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/DashboardLevelTimeMacro.test.ts @@ -0,0 +1,74 @@ +import { getPanelPlugin } from '@grafana/data/test'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { SceneTimeRange, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes'; + +import { activateFullSceneTree } from '../utils/test-utils'; + +import { DashboardLevelTimeMacro } from './DashboardLevelTimeMacro'; +import { DashboardScene } from './DashboardScene'; +import { PanelTimeRange } from './PanelTimeRange'; +import { AutoGridItem } from './layout-auto-grid/AutoGridItem'; +import { AutoGridLayout } from './layout-auto-grid/AutoGridLayout'; +import { + AutoGridLayoutManager, + getAutoRowsTemplate, + getTemplateColumnsTemplate, +} from './layout-auto-grid/AutoGridLayoutManager'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), +})); + +setPluginImportUtils({ + importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: (id: string) => undefined, +}); + +describe('dashboardLevelTimeMacros', () => { + it('Can use use $__from and $__to', async () => { + const panel = new VizPanel({ + $timeRange: new PanelTimeRange({ timeShift: '1h' }), + title: 'Test Panel', + key: 'panel-1', + pluginId: 'timeseries', + }); + + const scene = new DashboardScene({ + $timeRange: new SceneTimeRange({ from: '2023-05-23T06:09:57.073Z', to: '2023-05-23T07:09:57.073Z' }), + body: new AutoGridLayoutManager({ + maxColumnCount: 12, + columnWidth: 100, + rowHeight: 100, + fillScreen: true, + layout: new AutoGridLayout({ + isDraggable: true, + templateColumns: getTemplateColumnsTemplate(12, 100), + autoRows: getAutoRowsTemplate(100, true), + children: [ + new AutoGridItem({ + body: panel, + }), + ], + }), + }), + }); + + activateFullSceneTree(scene); + + // Wait for the scene to be activated + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(sceneGraph.interpolate(scene, '$__from')).toBe('1684822197073'); // Dashboard level time range + expect(sceneGraph.interpolate(scene, '$__to')).toBe('1684825797073'); // Dashboard level time range + + expect(sceneGraph.interpolate(panel, '$__from')).toBe('1684818597073'); // Time shifted by 1h + expect(sceneGraph.interpolate(panel, '$__to')).toBe('1684822197073'); // Time shifted by 1h + + sceneUtils.registerVariableMacro('__from', DashboardLevelTimeMacro, true); + sceneUtils.registerVariableMacro('__to', DashboardLevelTimeMacro, true); + + expect(sceneGraph.interpolate(panel, '$__from')).toBe('1684822197073'); // Dashboard level time range even when panel is time shifted + expect(sceneGraph.interpolate(panel, '$__to')).toBe('1684825797073'); // Dashboard level time range even when panel is time shifted + }); +}); diff --git a/public/app/features/dashboard-scene/scene/DashboardLevelTimeMacro.ts b/public/app/features/dashboard-scene/scene/DashboardLevelTimeMacro.ts new file mode 100644 index 00000000000..e6d0fc032fb --- /dev/null +++ b/public/app/features/dashboard-scene/scene/DashboardLevelTimeMacro.ts @@ -0,0 +1,33 @@ +import { dateTimeFormat } from '@grafana/data'; +import { FormatVariable, sceneGraph, SceneObject } from '@grafana/scenes'; + +/** + * This macro is used to support the old __to and __from macros that always used the dashboard level time range. + **/ +export class DashboardLevelTimeMacro implements FormatVariable { + public state: { name: string; type: string }; + private _sceneObject: SceneObject; + + public constructor(name: string, sceneObject: SceneObject) { + this.state = { name: name, type: 'time_macro' }; + this._sceneObject = sceneObject.getRoot(); + } + + public getValue() { + const timeRange = sceneGraph.getTimeRange(this._sceneObject); + if (this.state.name === '__from') { + return timeRange.state.value.from.valueOf(); + } else { + return timeRange.state.value.to.valueOf(); + } + } + + public getValueText?(): string { + const timeRange = sceneGraph.getTimeRange(this._sceneObject); + if (this.state.name === '__from') { + return dateTimeFormat(timeRange.state.value.from, { timeZone: timeRange.getTimeZone() }); + } else { + return dateTimeFormat(timeRange.state.value.to, { timeZone: timeRange.getTimeZone() }); + } + } +} From 1ad042b4e09b265e38def434d0dcdefba4f45dba Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 31 Jul 2025 11:56:15 +0200 Subject: [PATCH 085/126] Alerting: Mark Prometheus to Grafana conversion API as stable (#103499) Alerting: Make Prometheus conversion API stable --- pkg/services/ngalert/api/tooling/api.json | 580 ++++++++++++++ .../definitions/convert_prometheus_api.go | 28 +- pkg/services/ngalert/api/tooling/spec.json | 42 +- public/api-merged.json | 580 ++++++++++++++ public/openapi3.json | 718 ++++++++++++++++++ 5 files changed, 1920 insertions(+), 28 deletions(-) diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index bf2a859fd61..7eae1a55974 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -5523,6 +5523,586 @@ "version": "1.1.0" }, "paths": { + "/convert/api/prom/rules": { + "get": { + "operationId": "RouteConvertPrometheusCortexGetRules", + "produces": [ + "application/yaml" + ], + "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 Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "post": { + "consumes": [ + "application/json", + "application/yaml" + ], + "operationId": "RouteConvertPrometheusCortexPostRuleGroups", + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Converts the submitted rule groups into Grafana-Managed Rules.", + "tags": [ + "convert_prometheus" + ] + } + }, + "/convert/api/prom/rules/{NamespaceTitle}": { + "delete": { + "operationId": "RouteConvertPrometheusCortexDeleteNamespace", + "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 that were imported from Prometheus-compatible sources within the specified namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusCortexGetNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/yaml" + ], + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", + "tags": [ + "convert_prometheus" + ] + }, + "post": { + "consumes": [ + "application/yaml" + ], + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", + "operationId": "RouteConvertPrometheusCortexPostRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-datasource-uid", + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-recording-rules-paused", + "type": "boolean" + }, + { + "in": "header", + "name": "x-grafana-alerting-alert-rules-paused", + "type": "boolean" + }, + { + "in": "header", + "name": "x-grafana-alerting-target-datasource-uid", + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-folder-uid", + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-notification-receiver", + "type": "string" + }, + { + "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": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", + "tags": [ + "convert_prometheus" + ], + "x-raw-request": "true" + } + }, + "/convert/api/prom/rules/{NamespaceTitle}/{Group}": { + "delete": { + "operationId": "RouteConvertPrometheusCortexDeleteRuleGroup", + "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 specific rule group if it was imported from a Prometheus-compatible source.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusCortexGetRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/yaml" + ], + "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 single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", + "tags": [ + "convert_prometheus" + ] + } + }, + "/convert/prometheus/config/v1/rules": { + "get": { + "operationId": "RouteConvertPrometheusGetRules", + "produces": [ + "application/yaml" + ], + "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 Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "post": { + "consumes": [ + "application/json", + "application/yaml" + ], + "operationId": "RouteConvertPrometheusPostRuleGroups", + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Converts the submitted rule groups into Grafana-Managed Rules.", + "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 that were imported from Prometheus-compatible sources within the specified namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusGetNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/yaml" + ], + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", + "tags": [ + "convert_prometheus" + ] + }, + "post": { + "consumes": [ + "application/yaml" + ], + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", + "operationId": "RouteConvertPrometheusPostRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-datasource-uid", + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-recording-rules-paused", + "type": "boolean" + }, + { + "in": "header", + "name": "x-grafana-alerting-alert-rules-paused", + "type": "boolean" + }, + { + "in": "header", + "name": "x-grafana-alerting-target-datasource-uid", + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-folder-uid", + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-notification-receiver", + "type": "string" + }, + { + "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": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", + "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 specific rule group if it was imported from a Prometheus-compatible source.", + "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/yaml" + ], + "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 single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", + "tags": [ + "convert_prometheus" + ] + } + }, "/v1/provisioning/alert-rules": { "get": { "operationId": "RouteGetAlertRules", diff --git a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go index 98b17d19ed3..b3c2db6fe7a 100644 --- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -5,7 +5,7 @@ import ( ) // Route for mimirtool -// swagger:route GET /convert/prometheus/config/v1/rules convert_prometheus RouteConvertPrometheusGetRules +// swagger:route GET /convert/prometheus/config/v1/rules convert_prometheus stable RouteConvertPrometheusGetRules // // Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace. // @@ -18,7 +18,7 @@ import ( // 404: NotFound // Route for cortextool -// swagger:route GET /convert/api/prom/rules convert_prometheus RouteConvertPrometheusCortexGetRules +// swagger:route GET /convert/api/prom/rules convert_prometheus stable RouteConvertPrometheusCortexGetRules // // Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace. // @@ -31,7 +31,7 @@ import ( // 404: NotFound // Route for mimirtool -// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusGetNamespace +// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus stable RouteConvertPrometheusGetNamespace // // Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder). // @@ -44,7 +44,7 @@ import ( // 404: NotFound // Route for cortextool -// swagger:route GET /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexGetNamespace +// swagger:route GET /convert/api/prom/rules/{NamespaceTitle} convert_prometheus stable RouteConvertPrometheusCortexGetNamespace // // Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder). // @@ -57,7 +57,7 @@ import ( // 404: NotFound // Route for mimirtool -// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusGetRuleGroup +// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus stable RouteConvertPrometheusGetRuleGroup // // Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source. // @@ -70,7 +70,7 @@ import ( // 404: NotFound // Route for cortextool -// swagger:route GET /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusCortexGetRuleGroup +// swagger:route GET /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus stable RouteConvertPrometheusCortexGetRuleGroup // // Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source. // @@ -82,7 +82,7 @@ import ( // 403: ForbiddenError // 404: NotFound -// swagger:route POST /convert/prometheus/config/v1/rules convert_prometheus RouteConvertPrometheusPostRuleGroups +// swagger:route POST /convert/prometheus/config/v1/rules convert_prometheus stable RouteConvertPrometheusPostRuleGroups // // Converts the submitted rule groups into Grafana-Managed Rules. // @@ -97,7 +97,7 @@ import ( // 202: ConvertPrometheusResponse // 403: ForbiddenError -// swagger:route POST /convert/api/prom/rules convert_prometheus RouteConvertPrometheusCortexPostRuleGroups +// swagger:route POST /convert/api/prom/rules convert_prometheus stable RouteConvertPrometheusCortexPostRuleGroups // // Converts the submitted rule groups into Grafana-Managed Rules. // @@ -113,7 +113,7 @@ import ( // 403: ForbiddenError // Route for mimirtool -// swagger:route POST /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusPostRuleGroup +// swagger:route POST /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus stable RouteConvertPrometheusPostRuleGroup // // Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace. // If the group already exists and was not imported from a Prometheus-compatible source initially, @@ -133,7 +133,7 @@ import ( // x-raw-request: true // Route for cortextool -// swagger:route POST /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexPostRuleGroup +// swagger:route POST /convert/api/prom/rules/{NamespaceTitle} convert_prometheus stable RouteConvertPrometheusCortexPostRuleGroup // // Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace. // If the group already exists and was not imported from a Prometheus-compatible source initially, @@ -153,7 +153,7 @@ import ( // x-raw-request: true // Route for mimirtool -// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusDeleteNamespace +// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus stable RouteConvertPrometheusDeleteNamespace // // Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace. // @@ -165,7 +165,7 @@ import ( // 403: ForbiddenError // Route for cortextool -// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexDeleteNamespace +// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle} convert_prometheus stable RouteConvertPrometheusCortexDeleteNamespace // // Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace. // @@ -177,7 +177,7 @@ import ( // 403: ForbiddenError // Route for mimirtool -// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusDeleteRuleGroup +// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus stable RouteConvertPrometheusDeleteRuleGroup // // Deletes a specific rule group if it was imported from a Prometheus-compatible source. // @@ -189,7 +189,7 @@ import ( // 403: ForbiddenError // Route for cortextool -// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusCortexDeleteRuleGroup +// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus stable RouteConvertPrometheusCortexDeleteRuleGroup // // Deletes a specific rule group if it was imported from a Prometheus-compatible source. // diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 1b2744b976d..0617865b8d2 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1076,7 +1076,8 @@ "application/yaml" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", "operationId": "RouteConvertPrometheusCortexGetRules", @@ -1110,7 +1111,8 @@ "application/json" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Converts the submitted rule groups into Grafana-Managed Rules.", "operationId": "RouteConvertPrometheusCortexPostRuleGroups", @@ -1136,7 +1138,8 @@ "application/yaml" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", "operationId": "RouteConvertPrometheusCortexGetNamespace", @@ -1178,7 +1181,8 @@ "application/json" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", "operationId": "RouteConvertPrometheusCortexPostRuleGroup", @@ -1248,7 +1252,8 @@ "application/json" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.", "operationId": "RouteConvertPrometheusCortexDeleteNamespace", @@ -1282,7 +1287,8 @@ "application/yaml" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", "operationId": "RouteConvertPrometheusCortexGetRuleGroup", @@ -1326,7 +1332,8 @@ "application/json" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.", "operationId": "RouteConvertPrometheusCortexDeleteRuleGroup", @@ -1483,7 +1490,8 @@ "application/yaml" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", "operationId": "RouteConvertPrometheusGetRules", @@ -1517,7 +1525,8 @@ "application/json" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Converts the submitted rule groups into Grafana-Managed Rules.", "operationId": "RouteConvertPrometheusPostRuleGroups", @@ -1543,7 +1552,8 @@ "application/yaml" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", "operationId": "RouteConvertPrometheusGetNamespace", @@ -1585,7 +1595,8 @@ "application/json" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", "operationId": "RouteConvertPrometheusPostRuleGroup", @@ -1655,7 +1666,8 @@ "application/json" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.", "operationId": "RouteConvertPrometheusDeleteNamespace", @@ -1689,7 +1701,8 @@ "application/yaml" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", "operationId": "RouteConvertPrometheusGetRuleGroup", @@ -1733,7 +1746,8 @@ "application/json" ], "tags": [ - "convert_prometheus" + "convert_prometheus", + "stable" ], "summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.", "operationId": "RouteConvertPrometheusDeleteRuleGroup", diff --git a/public/api-merged.json b/public/api-merged.json index 2f2eed4fca5..07e2a9726be 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -2671,6 +2671,586 @@ } } }, + "/convert/api/prom/rules": { + "get": { + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", + "operationId": "RouteConvertPrometheusCortexGetRules", + "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/json", + "application/yaml" + ], + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Converts the submitted rule groups into Grafana-Managed Rules.", + "operationId": "RouteConvertPrometheusCortexPostRuleGroups", + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + } + } + }, + "/convert/api/prom/rules/{NamespaceTitle}": { + "get": { + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", + "operationId": "RouteConvertPrometheusCortexGetNamespace", + "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": { + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", + "consumes": [ + "application/yaml" + ], + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", + "operationId": "RouteConvertPrometheusCortexPostRuleGroup", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "x-grafana-alerting-datasource-uid", + "in": "header" + }, + { + "type": "boolean", + "name": "x-grafana-alerting-recording-rules-paused", + "in": "header" + }, + { + "type": "boolean", + "name": "x-grafana-alerting-alert-rules-paused", + "in": "header" + }, + { + "type": "string", + "name": "x-grafana-alerting-target-datasource-uid", + "in": "header" + }, + { + "type": "string", + "name": "x-grafana-alerting-folder-uid", + "in": "header" + }, + { + "type": "string", + "name": "x-grafana-alerting-notification-receiver", + "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 that were imported from Prometheus-compatible sources within the specified namespace.", + "operationId": "RouteConvertPrometheusCortexDeleteNamespace", + "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/api/prom/rules/{NamespaceTitle}/{Group}": { + "get": { + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", + "operationId": "RouteConvertPrometheusCortexGetRuleGroup", + "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 specific rule group if it was imported from a Prometheus-compatible source.", + "operationId": "RouteConvertPrometheusCortexDeleteRuleGroup", + "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" + } + } + } + } + }, + "/convert/prometheus/config/v1/rules": { + "get": { + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", + "operationId": "RouteConvertPrometheusGetRules", + "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/json", + "application/yaml" + ], + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Converts the submitted rule groups into Grafana-Managed Rules.", + "operationId": "RouteConvertPrometheusPostRuleGroups", + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + } + } + }, + "/convert/prometheus/config/v1/rules/{NamespaceTitle}": { + "get": { + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", + "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": { + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", + "consumes": [ + "application/yaml" + ], + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", + "operationId": "RouteConvertPrometheusPostRuleGroup", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "x-grafana-alerting-datasource-uid", + "in": "header" + }, + { + "type": "boolean", + "name": "x-grafana-alerting-recording-rules-paused", + "in": "header" + }, + { + "type": "boolean", + "name": "x-grafana-alerting-alert-rules-paused", + "in": "header" + }, + { + "type": "string", + "name": "x-grafana-alerting-target-datasource-uid", + "in": "header" + }, + { + "type": "string", + "name": "x-grafana-alerting-folder-uid", + "in": "header" + }, + { + "type": "string", + "name": "x-grafana-alerting-notification-receiver", + "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 that were imported from Prometheus-compatible sources within the specified 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/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", + "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 specific rule group if it was imported from a Prometheus-compatible source.", + "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" + } + } + } + } + }, "/dashboard/snapshots": { "get": { "tags": [ diff --git a/public/openapi3.json b/public/openapi3.json index 3a1e1c93c5b..8928ac0429f 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -16590,6 +16590,724 @@ ] } }, + "/convert/api/prom/rules": { + "get": { + "operationId": "RouteConvertPrometheusCortexGetRules", + "responses": { + "200": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/PrometheusNamespace" + } + } + }, + "description": "PrometheusNamespace" + }, + "403": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + }, + "404": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/NotFound" + } + } + }, + "description": "NotFound" + } + }, + "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "post": { + "operationId": "RouteConvertPrometheusCortexPostRuleGroups", + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConvertPrometheusResponse" + } + } + }, + "description": "ConvertPrometheusResponse" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + } + }, + "summary": "Converts the submitted rule groups into Grafana-Managed Rules.", + "tags": [ + "convert_prometheus" + ] + } + }, + "/convert/api/prom/rules/{NamespaceTitle}": { + "delete": { + "operationId": "RouteConvertPrometheusCortexDeleteNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConvertPrometheusResponse" + } + } + }, + "description": "ConvertPrometheusResponse" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + } + }, + "summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusCortexGetNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/PrometheusNamespace" + } + } + }, + "description": "PrometheusNamespace" + }, + "403": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + }, + "404": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/NotFound" + } + } + }, + "description": "NotFound" + } + }, + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", + "tags": [ + "convert_prometheus" + ] + }, + "post": { + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", + "operationId": "RouteConvertPrometheusCortexPostRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-datasource-uid", + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-recording-rules-paused", + "schema": { + "type": "boolean" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-alert-rules-paused", + "schema": { + "type": "boolean" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-target-datasource-uid", + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-folder-uid", + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-notification-receiver", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/PrometheusRuleGroup" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConvertPrometheusResponse" + } + } + }, + "description": "ConvertPrometheusResponse" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + } + }, + "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", + "tags": [ + "convert_prometheus" + ], + "x-raw-request": "true" + } + }, + "/convert/api/prom/rules/{NamespaceTitle}/{Group}": { + "delete": { + "operationId": "RouteConvertPrometheusCortexDeleteRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "Group", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConvertPrometheusResponse" + } + } + }, + "description": "ConvertPrometheusResponse" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + } + }, + "summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusCortexGetRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "Group", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/PrometheusRuleGroup" + } + } + }, + "description": "PrometheusRuleGroup" + }, + "403": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + }, + "404": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/NotFound" + } + } + }, + "description": "NotFound" + } + }, + "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", + "tags": [ + "convert_prometheus" + ] + } + }, + "/convert/prometheus/config/v1/rules": { + "get": { + "operationId": "RouteConvertPrometheusGetRules", + "responses": { + "200": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/PrometheusNamespace" + } + } + }, + "description": "PrometheusNamespace" + }, + "403": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + }, + "404": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/NotFound" + } + } + }, + "description": "NotFound" + } + }, + "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "post": { + "operationId": "RouteConvertPrometheusPostRuleGroups", + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConvertPrometheusResponse" + } + } + }, + "description": "ConvertPrometheusResponse" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + } + }, + "summary": "Converts the submitted rule groups into Grafana-Managed Rules.", + "tags": [ + "convert_prometheus" + ] + } + }, + "/convert/prometheus/config/v1/rules/{NamespaceTitle}": { + "delete": { + "operationId": "RouteConvertPrometheusDeleteNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConvertPrometheusResponse" + } + } + }, + "description": "ConvertPrometheusResponse" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + } + }, + "summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusGetNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/PrometheusNamespace" + } + } + }, + "description": "PrometheusNamespace" + }, + "403": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + }, + "404": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/NotFound" + } + } + }, + "description": "NotFound" + } + }, + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", + "tags": [ + "convert_prometheus" + ] + }, + "post": { + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", + "operationId": "RouteConvertPrometheusPostRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-datasource-uid", + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-recording-rules-paused", + "schema": { + "type": "boolean" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-alert-rules-paused", + "schema": { + "type": "boolean" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-target-datasource-uid", + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-folder-uid", + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "x-grafana-alerting-notification-receiver", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/PrometheusRuleGroup" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConvertPrometheusResponse" + } + } + }, + "description": "ConvertPrometheusResponse" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + } + }, + "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", + "tags": [ + "convert_prometheus" + ], + "x-raw-request": "true" + } + }, + "/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}": { + "delete": { + "operationId": "RouteConvertPrometheusDeleteRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "Group", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConvertPrometheusResponse" + } + } + }, + "description": "ConvertPrometheusResponse" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + } + }, + "summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusGetRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "Group", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/PrometheusRuleGroup" + } + } + }, + "description": "PrometheusRuleGroup" + }, + "403": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + }, + "404": { + "content": { + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/NotFound" + } + } + }, + "description": "NotFound" + } + }, + "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", + "tags": [ + "convert_prometheus" + ] + } + }, "/dashboard/snapshots": { "get": { "operationId": "searchDashboardSnapshots", From a3e19209839d4bdfb0341c77b4a54583cf8c9eb5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:14:02 +0100 Subject: [PATCH 086/126] Update dependency ts-jest to v29.4.0 (#108964) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-sql/package.json | 2 +- yarn.lock | 56 ++++++++++++++++--- 5 files changed, 52 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 3538a0d8b92..de65edb4836 100644 --- a/package.json +++ b/package.json @@ -248,7 +248,7 @@ "terser-webpack-plugin": "5.3.14", "testing-library-selector": "0.3.1", "tracelib": "1.0.1", - "ts-jest": "29.2.5", + "ts-jest": "29.4.0", "ts-node": "10.9.2", "typescript": "5.8.3", "webpack": "5.97.1", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index cfb6e77e230..3152966e799 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -79,7 +79,7 @@ "rollup": "^4.22.4", "rollup-plugin-esbuild": "6.2.1", "rollup-plugin-node-externals": "^8.0.0", - "ts-jest": "29.2.5", + "ts-jest": "29.4.0", "ts-node": "10.9.2", "typescript": "5.8.3" }, diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 87b73a02a08..11ef5bdce98 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -41,7 +41,7 @@ "@types/systemjs": "6.15.3", "jest": "^29.6.4", "react": "18.3.1", - "ts-jest": "29.2.5", + "ts-jest": "29.4.0", "ts-node": "10.9.2", "typescript": "5.8.3" }, diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index a3e9c784261..e99c2f9a737 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -51,7 +51,7 @@ "@types/uuid": "10.0.0", "i18next-parser": "9.3.0", "jest": "^29.6.4", - "ts-jest": "29.2.5", + "ts-jest": "29.4.0", "ts-node": "10.9.2", "typescript": "5.8.3" }, diff --git a/yarn.lock b/yarn.lock index 1841e4a110e..94cb5b420db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3293,7 +3293,7 @@ __metadata: rollup-plugin-esbuild: "npm:6.2.1" rollup-plugin-node-externals: "npm:^8.0.0" tinycolor2: "npm:1.6.0" - ts-jest: "npm:29.2.5" + ts-jest: "npm:29.4.0" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" typescript: "npm:5.8.3" @@ -3406,7 +3406,7 @@ __metadata: react-select: "npm:5.10.2" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" - ts-jest: "npm:29.2.5" + ts-jest: "npm:29.4.0" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" typescript: "npm:5.8.3" @@ -3700,7 +3700,7 @@ __metadata: react-virtualized-auto-sizer: "npm:1.0.26" rxjs: "npm:7.8.2" sql-formatter-plus: "npm:^1.3.6" - ts-jest: "npm:29.2.5" + ts-jest: "npm:29.4.0" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" typescript: "npm:5.8.3" @@ -18624,7 +18624,7 @@ __metadata: testing-library-selector: "npm:0.3.1" tinycolor2: "npm:1.6.0" tracelib: "npm:1.0.1" - ts-jest: "npm:29.2.5" + ts-jest: "npm:29.4.0" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" tween-functions: "npm:^1.2.0" @@ -31219,6 +31219,46 @@ __metadata: languageName: node linkType: hard +"ts-jest@npm:29.4.0": + version: 29.4.0 + resolution: "ts-jest@npm:29.4.0" + dependencies: + bs-logger: "npm:^0.2.6" + ejs: "npm:^3.1.10" + fast-json-stable-stringify: "npm:^2.1.0" + json5: "npm:^2.2.3" + lodash.memoize: "npm:^4.1.2" + make-error: "npm:^1.3.6" + semver: "npm:^7.7.2" + type-fest: "npm:^4.41.0" + yargs-parser: "npm:^21.1.1" + peerDependencies: + "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/transform": ^29.0.0 || ^30.0.0 + "@jest/types": ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: ">=4.3 <6" + peerDependenciesMeta: + "@babel/core": + optional: true + "@jest/transform": + optional: true + "@jest/types": + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + bin: + ts-jest: cli.js + checksum: 10/fe501f3d9946ec52db78ae0ac6cfd72942b1c1f5d657c12db321c9d570f0f499e83eb6c7e26074cd11dfe534a6a09c676947e7a63ee08fcda552aabcdeb6c592 + languageName: node + linkType: hard + "ts-mixer@npm:^6.0.3, ts-mixer@npm:^6.0.4": version: 6.0.4 resolution: "ts-mixer@npm:6.0.4" @@ -31439,10 +31479,10 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.18.2, type-fest@npm:^4.26.1, type-fest@npm:^4.40.0": - version: 4.40.0 - resolution: "type-fest@npm:4.40.0" - checksum: 10/dbca20979d18c6b8c87ca28cd999d9ae6b34e0c54c3a87ac65530a32f7a178d38d3788044a589f47c9fde3f3c81422e7b021ec1455f7242b724a2d9c642ce8b8 +"type-fest@npm:^4.18.2, type-fest@npm:^4.26.1, type-fest@npm:^4.40.0, type-fest@npm:^4.41.0": + version: 4.41.0 + resolution: "type-fest@npm:4.41.0" + checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212 languageName: node linkType: hard From 04a563cb09388a630d22bea8d8bb1b051ca8b953 Mon Sep 17 00:00:00 2001 From: Gareth Date: Thu, 31 Jul 2025 11:30:15 +0100 Subject: [PATCH 087/126] Jaeger: remove unused code (#108612) * remove isSearchFormValid * remove getTimeRange * remove convertTagsLogfmt * remove createTableFrame * remove transformToLogfmt * remove types * remove mapJaegerDependenciesResponse * trigger workflow --- .betterer.results | 6 - .../jaeger/_importedDependencies/README.md | 3 - .../model/trace-viewer.ts | 57 ----- .../model/transform-trace-data.tsx | 201 ------------------ .../_importedDependencies/selectors/trace.ts | 65 ------ .../_importedDependencies/types/index.tsx | 15 -- .../_importedDependencies/types/trace.ts | 102 --------- .../_importedDependencies/utils/TreeNode.ts | 139 ------------ .../utils/config/default-config.ts | 40 ---- .../utils/config/get-config.tsx | 29 --- .../jaeger/components/SearchForm.tsx | 3 +- .../datasource/jaeger/datasource.test.ts | 10 - .../plugins/datasource/jaeger/datasource.ts | 21 -- .../jaeger/dependencyGraphTransform.test.ts | 106 --------- .../jaeger/dependencyGraphTransform.ts | 125 ----------- .../datasource/jaeger/responseTransform.ts | 66 +----- public/app/plugins/datasource/jaeger/types.ts | 8 +- public/app/plugins/datasource/jaeger/util.ts | 26 --- 18 files changed, 4 insertions(+), 1018 deletions(-) delete mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/README.md delete mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/model/trace-viewer.ts delete mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx delete mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/selectors/trace.ts delete mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx delete mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts delete mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/utils/TreeNode.ts delete mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/default-config.ts delete mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/get-config.tsx delete mode 100644 public/app/plugins/datasource/jaeger/dependencyGraphTransform.test.ts delete mode 100644 public/app/plugins/datasource/jaeger/dependencyGraphTransform.ts delete mode 100644 public/app/plugins/datasource/jaeger/util.ts diff --git a/.betterer.results b/.betterer.results index 37fb0e90b15..4482c94acc5 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3498,12 +3498,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/influxdb/response_parser.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./trace\`)", "0"] - ], "public/app/plugins/datasource/jaeger/datasource.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/README.md b/public/app/plugins/datasource/jaeger/_importedDependencies/README.md deleted file mode 100644 index 6a32e35129c..00000000000 --- a/public/app/plugins/datasource/jaeger/_importedDependencies/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This directory contains dependencies that we duplicated from Grafana core while working on the decoupling of Jaeger from such core. -The long-term goal is to move these files away from here by replacing them with packages. -As such, they are only temporary and meant to be used internally to this package, please avoid using them for example as dependencies (imports) in other data source plugins. diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/model/trace-viewer.ts b/public/app/plugins/datasource/jaeger/_importedDependencies/model/trace-viewer.ts deleted file mode 100644 index d3978acc247..00000000000 --- a/public/app/plugins/datasource/jaeger/_importedDependencies/model/trace-viewer.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2020 The Jaeger Authors -// -// 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. - -import { memoize } from 'lodash'; - -import { TraceSpan } from '../types'; - -function _getTraceNameImpl(spans: TraceSpan[]) { - // Use a span with no references to another span in given array - // prefering the span with the fewest references - // using start time as a tie breaker - let candidateSpan: TraceSpan | undefined; - const allIDs: Set = new Set(spans.map(({ spanID }) => spanID)); - - for (let i = 0; i < spans.length; i++) { - const hasInternalRef = - spans[i].references && - spans[i].references.some(({ traceID, spanID }) => traceID === spans[i].traceID && allIDs.has(spanID)); - if (hasInternalRef) { - continue; - } - - if (!candidateSpan) { - candidateSpan = spans[i]; - continue; - } - - const thisRefLength = (spans[i].references && spans[i].references.length) || 0; - const candidateRefLength = (candidateSpan.references && candidateSpan.references.length) || 0; - - if ( - thisRefLength < candidateRefLength || - (thisRefLength === candidateRefLength && spans[i].startTime < candidateSpan.startTime) - ) { - candidateSpan = spans[i]; - } - } - return candidateSpan ? `${candidateSpan.process.serviceName}: ${candidateSpan.operationName}` : ''; -} - -export const getTraceName = memoize(_getTraceNameImpl, (spans: TraceSpan[]) => { - if (!spans.length) { - return 0; - } - return spans[0].traceID; -}); diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx b/public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx deleted file mode 100644 index f095fe3d8c1..00000000000 --- a/public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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. - -import { isEqual as _isEqual } from 'lodash'; - -import { TraceKeyValuePair } from '@grafana/data'; - -import { getTraceSpanIdsAsTree } from '../selectors/trace'; -import { TraceSpan, Trace, TraceResponse, TraceProcess } from '../types'; -import TreeNode from '../utils/TreeNode'; -import { getConfigValue } from '../utils/config/get-config'; - -import { getTraceName } from './trace-viewer'; - -function deduplicateTags(tags: TraceKeyValuePair[]) { - const warningsHash: Map = new Map(); - const dedupedTags: TraceKeyValuePair[] = tags.reduce((uniqueTags, tag) => { - if (!uniqueTags.some((t) => t.key === tag.key && t.value === tag.value)) { - uniqueTags.push(tag); - } else { - warningsHash.set(`${tag.key}:${tag.value}`, `Duplicate tag "${tag.key}:${tag.value}"`); - } - return uniqueTags; - }, []); - const warnings = Array.from(warningsHash.values()); - return { dedupedTags, warnings }; -} - -function orderTags(tags: TraceKeyValuePair[], topPrefixes?: string[]) { - const orderedTags: TraceKeyValuePair[] = tags?.slice() ?? []; - const tp = (topPrefixes || []).map((p: string) => p.toLowerCase()); - - orderedTags.sort((a, b) => { - const aKey = a.key.toLowerCase(); - const bKey = b.key.toLowerCase(); - - for (let i = 0; i < tp.length; i++) { - const p = tp[i]; - if (aKey.startsWith(p) && !bKey.startsWith(p)) { - return -1; - } - if (!aKey.startsWith(p) && bKey.startsWith(p)) { - return 1; - } - } - - if (aKey > bKey) { - return 1; - } - if (aKey < bKey) { - return -1; - } - return 0; - }); - - return orderedTags; -} - -/** - * NOTE: Mutates `data` - Transform the HTTP response data into the form the app - * generally requires. - */ -export default function transformTraceData(data: TraceResponse | undefined): Trace | null { - if (!data?.traceID) { - return null; - } - const traceID = data.traceID.toLowerCase(); - - let traceEndTime = 0; - let traceStartTime = Number.MAX_SAFE_INTEGER; - const spanIdCounts = new Map(); - const spanMap = new Map(); - // filter out spans with empty start times - // eslint-disable-next-line no-param-reassign - data.spans = data.spans.filter((span) => Boolean(span.startTime)); - - // Sort process tags - data.processes = Object.entries(data.processes).reduce>((processes, [id, process]) => { - processes[id] = { - ...process, - tags: orderTags(process.tags), - }; - return processes; - }, {}); - - const max = data.spans.length; - for (let i = 0; i < max; i++) { - const span: TraceSpan = data.spans[i] as TraceSpan; - const { startTime, duration, processID } = span; - - let spanID = span.spanID; - // check for start / end time for the trace - if (startTime < traceStartTime) { - traceStartTime = startTime; - } - if (startTime + duration > traceEndTime) { - traceEndTime = startTime + duration; - } - // make sure span IDs are unique - const idCount = spanIdCounts.get(spanID); - if (idCount != null) { - // eslint-disable-next-line no-console - console.warn(`Dupe spanID, ${idCount + 1} x ${spanID}`, span, spanMap.get(spanID)); - if (_isEqual(span, spanMap.get(spanID))) { - // eslint-disable-next-line no-console - console.warn('\t two spans with same ID have `isEqual(...) === true`'); - } - spanIdCounts.set(spanID, idCount + 1); - spanID = `${spanID}_${idCount}`; - span.spanID = spanID; - } else { - spanIdCounts.set(spanID, 1); - } - span.process = data.processes[processID]; - spanMap.set(spanID, span); - } - // tree is necessary to sort the spans, so children follow parents, and - // siblings are sorted by start time - const tree = getTraceSpanIdsAsTree(data, spanMap); - const spans: TraceSpan[] = []; - const svcCounts: Record = {}; - - tree.walk((spanID: string, node: TreeNode, depth = 0) => { - if (spanID === '__root__') { - return; - } - if (typeof spanID !== 'string') { - return; - } - const span = spanMap.get(spanID); - if (!span) { - return; - } - const { serviceName } = span.process; - svcCounts[serviceName] = (svcCounts[serviceName] || 0) + 1; - span.relativeStartTime = span.startTime - traceStartTime; - span.depth = depth - 1; - span.hasChildren = node.children.length > 0; - span.childSpanCount = node.children.length; - span.warnings = span.warnings || []; - span.tags = span.tags || []; - span.references = span.references || []; - - span.childSpanIds = node.children - .slice() - .sort((a, b) => { - const spanA = spanMap.get(a.value)!; - const spanB = spanMap.get(b.value)!; - return spanB.startTime + spanB.duration - (spanA.startTime + spanA.duration); - }) - .map((each) => each.value); - - const tagsInfo = deduplicateTags(span.tags); - span.tags = orderTags(tagsInfo.dedupedTags, getConfigValue('topTagPrefixes')); - span.warnings = span.warnings.concat(tagsInfo.warnings); - span.references.forEach((ref, index) => { - const refSpan = spanMap.get(ref.spanID); - if (refSpan) { - // eslint-disable-next-line no-param-reassign - ref.span = refSpan; - if (index > 0) { - // Don't take into account the parent, just other references. - refSpan.subsidiarilyReferencedBy = refSpan.subsidiarilyReferencedBy || []; - refSpan.subsidiarilyReferencedBy.push({ - spanID, - traceID, - span, - refType: ref.refType, - }); - } - } - }); - spans.push(span); - }); - const traceName = getTraceName(spans); - const services = Object.keys(svcCounts).map((name) => ({ name, numberOfSpans: svcCounts[name] })); - return { - services, - spans, - traceID, - traceName, - // can't use spread operator for intersection types - // repl: https://goo.gl/4Z23MJ - // issue: https://github.com/facebook/flow/issues/1511 - processes: data.processes, - duration: traceEndTime - traceStartTime, - startTime: traceStartTime, - endTime: traceEndTime, - }; -} diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/selectors/trace.ts b/public/app/plugins/datasource/jaeger/_importedDependencies/selectors/trace.ts deleted file mode 100644 index 9fb695b3321..00000000000 --- a/public/app/plugins/datasource/jaeger/_importedDependencies/selectors/trace.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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. - -import { TraceResponse, TraceSpanData } from '../types/trace'; -import TreeNode from '../utils/TreeNode'; - -const TREE_ROOT_ID = '__root__'; - -/** - * Build a tree of { value: spanID, children } items derived from the - * `span.references` information. The tree represents the grouping of parent / - * child relationships. The root-most node is nominal in that - * `.value === TREE_ROOT_ID`. This is done because a root span (the main trace - * span) is not always included with the trace data. Thus, there can be - * multiple top-level spans, and the root node acts as their common parent. - * - * The children are sorted by `span.startTime` after the tree is built. - * - * @param {Trace} trace The trace to build the tree of spanIDs. - * @return {TreeNode} A tree of spanIDs derived from the relationships - * between spans in the trace. - */ -export function getTraceSpanIdsAsTree(trace: TraceResponse, spanMap: Map | null = null) { - const nodesById = new Map(trace.spans.map((span: TraceSpanData) => [span.spanID, new TreeNode(span.spanID)])); - const spansById = spanMap ?? new Map(trace.spans.map((span: TraceSpanData) => [span.spanID, span])); - const root = new TreeNode(TREE_ROOT_ID); - trace.spans.forEach((span: TraceSpanData) => { - const node = nodesById.get(span.spanID)!; - if (Array.isArray(span.references) && span.references.length) { - const { refType, spanID: parentID } = span.references[0]; - if (refType === 'CHILD_OF' || refType === 'FOLLOWS_FROM') { - const parent = nodesById.get(parentID) || root; - parent.children?.push(node); - } else { - throw new Error(`Unrecognized ref type: ${refType}`); - } - } else { - root.children.push(node); - } - }); - const comparator = (nodeA: TreeNode, nodeB: TreeNode) => { - const a: TraceSpanData | undefined = nodeA?.value ? spansById.get(nodeA.value.toString()) : undefined; - const b: TraceSpanData | undefined = nodeB?.value ? spansById.get(nodeB.value.toString()) : undefined; - return +(a?.startTime! > b?.startTime!) || +(a?.startTime === b?.startTime) - 1; - }; - trace.spans.forEach((span: TraceSpanData) => { - const node = nodesById.get(span.spanID); - if (node!.children.length > 1) { - node?.children.sort(comparator); - } - }); - root.children.sort(comparator); - return root; -} diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx b/public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx deleted file mode 100644 index f777aee8566..00000000000 --- a/public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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. - -export type { TraceSpan, TraceResponse, Trace, TraceProcess, TraceLink, CriticalPathSection } from './trace'; diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts b/public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts deleted file mode 100644 index 0c38b7ba584..00000000000 --- a/public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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. - -import { TraceKeyValuePair, TraceLog } from '@grafana/data'; - -/** - * All timestamps are in microseconds - */ - -export type TraceLink = { - url: string; - text: string; -}; - -export type TraceProcess = { - serviceName: string; - tags: TraceKeyValuePair[]; -}; - -export type TraceSpanReference = { - refType: 'CHILD_OF' | 'FOLLOWS_FROM'; - // eslint-disable-next-line no-use-before-define - span?: TraceSpan | null | undefined; - spanID: string; - traceID: string; - tags?: TraceKeyValuePair[]; -}; - -export type TraceSpanData = { - spanID: string; - traceID: string; - processID: string; - operationName: string; - // Times are in microseconds - startTime: number; - duration: number; - logs: TraceLog[]; - tags?: TraceKeyValuePair[]; - kind?: string; - statusCode?: number; - statusMessage?: string; - instrumentationLibraryName?: string; - instrumentationLibraryVersion?: string; - traceState?: string; - references?: TraceSpanReference[]; - warnings?: string[] | null; - stackTraces?: string[]; - flags: number; - errorIconColor?: string; - dataFrameRowIndex?: number; - childSpanIds?: string[]; -}; - -export type TraceSpan = TraceSpanData & { - depth: number; - hasChildren: boolean; - childSpanCount: number; - process: TraceProcess; - relativeStartTime: number; - tags: NonNullable; - references: NonNullable; - warnings: NonNullable; - childSpanIds: NonNullable; - subsidiarilyReferencedBy: TraceSpanReference[]; -}; - -export type TraceData = { - processes: Record; - traceID: string; - warnings?: string[] | null; -}; - -export type TraceResponse = TraceData & { - spans: TraceSpanData[]; -}; - -export type Trace = TraceData & { - duration: number; - endTime: number; - spans: TraceSpan[]; - startTime: number; - traceName: string; - services: Array<{ name: string; numberOfSpans: number }>; -}; - -// It is a section of span that lies on critical path -export type CriticalPathSection = { - spanId: string; - section_start: number; - section_end: number; -}; diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/utils/TreeNode.ts b/public/app/plugins/datasource/jaeger/_importedDependencies/utils/TreeNode.ts deleted file mode 100644 index f6b123f2d50..00000000000 --- a/public/app/plugins/datasource/jaeger/_importedDependencies/utils/TreeNode.ts +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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 - -export default class TreeNode { - value: TValue; - children: Array>; - - static iterFunction( - fn: ((value: TValue, node: TreeNode, depth: number) => TreeNode | null) | Function, - depth = 0 - ) { - return (node: TreeNode) => fn(node.value, node, depth); - } - - static searchFunction(search: Function | TreeNode) { - if (typeof search === 'function') { - return search; - } - - return (value: TValue, node: TreeNode) => (search instanceof TreeNode ? node === search : value === search); - } - - constructor(value: TValue, children: Array> = []) { - this.value = value; - this.children = children; - } - - get depth(): number { - return this.children.reduce((depth, child) => Math.max(child.depth + 1, depth), 1); - } - - get size() { - let i = 0; - this.walk(() => i++); - return i; - } - - addChild(child: TreeNode | TValue) { - this.children.push(child instanceof TreeNode ? child : new TreeNode(child)); - return this; - } - - find(search: Function | TreeNode): TreeNode | null { - const searchFn = TreeNode.iterFunction(TreeNode.searchFunction(search)); - if (searchFn(this)) { - return this; - } - for (let i = 0; i < this.children.length; i++) { - const result = this.children[i].find(search); - if (result) { - return result; - } - } - return null; - } - - getPath(search: Function | TreeNode) { - const searchFn = TreeNode.iterFunction(TreeNode.searchFunction(search)); - - const findPath = ( - currentNode: TreeNode, - currentPath: Array> - ): Array> | null => { - // skip if we already found the result - const attempt = currentPath.concat([currentNode]); - // base case: return the array when there is a match - if (searchFn(currentNode)) { - return attempt; - } - for (let i = 0; i < currentNode.children.length; i++) { - const child = currentNode.children[i]; - const match = findPath(child, attempt); - if (match) { - return match; - } - } - return null; - }; - - return findPath(this, []); - } - - walk(fn: (spanID: TValue, node: TreeNode, depth: number) => void, startDepth = 0) { - type StackEntry = { - node: TreeNode; - depth: number; - }; - const nodeStack: StackEntry[] = []; - let actualDepth = startDepth; - nodeStack.push({ node: this, depth: actualDepth }); - while (nodeStack.length) { - const entry: StackEntry = nodeStack[nodeStack.length - 1]; - nodeStack.pop(); - const { node, depth } = entry; - fn(node.value, node, depth); - actualDepth = depth + 1; - let i = node.children.length - 1; - while (i >= 0) { - nodeStack.push({ node: node.children[i], depth: actualDepth }); - i--; - } - } - } - - paths(fn: (pathIds: TValue[]) => void) { - type StackEntry = { - node: TreeNode; - childIndex: number; - }; - const stack: StackEntry[] = []; - stack.push({ node: this, childIndex: 0 }); - const paths: TValue[] = []; - while (stack.length) { - const { node, childIndex } = stack[stack.length - 1]; - if (node.children.length >= childIndex + 1) { - stack[stack.length - 1].childIndex++; - stack.push({ node: node.children[childIndex], childIndex: 0 }); - } else { - if (node.children.length === 0) { - const path = stack.map((item) => item.node.value); - fn(path); - } - stack.pop(); - } - } - return paths; - } -} diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/default-config.ts b/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/default-config.ts deleted file mode 100644 index 1e8215a230d..00000000000 --- a/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/default-config.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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. - -const FALLBACK_DAG_MAX_NUM_SERVICES = 100; - -export default Object.defineProperty( - { - archiveEnabled: false, - dependencies: { - dagMaxNumServices: FALLBACK_DAG_MAX_NUM_SERVICES, - menuEnabled: true, - }, - linkPatterns: [], - search: { - maxLookback: { - label: '2 Days', - value: '2d', - }, - maxLimit: 1500, - }, - tracking: { - gaID: null, - trackErrors: true, - }, - }, - // fields that should be individually merged vs wholesale replaced - '__mergeFields', - { value: ['dependencies', 'search', 'tracking'] } -); diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/get-config.tsx b/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/get-config.tsx deleted file mode 100644 index 0a1ec89870c..00000000000 --- a/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/get-config.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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. - -import { get as _get } from 'lodash'; - -import defaultConfig from './default-config'; - -/** - * Merge the embedded config from the query service (if present) with the - * default config from `../../constants/default-config`. - */ -export default function getConfig() { - return defaultConfig; -} - -export function getConfigValue(path: string) { - return _get(getConfig(), path); -} diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx index c5840a60bab..a81c64d72a0 100644 --- a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx @@ -8,7 +8,6 @@ import { fuzzyMatch, InlineField, InlineFieldRow, Input, Select } from '@grafana import { JaegerDatasource } from '../datasource'; import { JaegerQuery } from '../types'; -import { transformToLogfmt } from '../util'; const durationPlaceholder = 'e.g. 1.2s, 100ms, 500us'; @@ -150,7 +149,7 @@ export function SearchForm({ datasource, query, onChange }: Props) { onChange({ diff --git a/public/app/plugins/datasource/jaeger/datasource.test.ts b/public/app/plugins/datasource/jaeger/datasource.test.ts index 5c0961d4ac7..2beeeb09eb8 100644 --- a/public/app/plugins/datasource/jaeger/datasource.test.ts +++ b/public/app/plugins/datasource/jaeger/datasource.test.ts @@ -136,16 +136,6 @@ describe('node graph functionality', () => { }); }); -describe('time range', () => { - it('should calculate correct time range', async () => { - const ds = new JaegerDatasource(defaultSettings); - const timeRange = ds.getTimeRange(); - const now = Date.now(); - expect(timeRange.end).toBeCloseTo(now * 1000, -4); - expect(timeRange.start).toBeCloseTo((now - 6 * 3600 * 1000) * 1000, -4); - }); -}); - function setupQueryMock(type: 'trace' | 'search') { return jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => { if (type === 'search') { diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts index 11c6a8a3f3b..ea4eb2c4d43 100644 --- a/public/app/plugins/datasource/jaeger/datasource.ts +++ b/public/app/plugins/datasource/jaeger/datasource.ts @@ -6,10 +6,7 @@ import { DataQueryResponse, DataSourceInstanceSettings, DataSourceJsonData, - dateMath, - DateTime, FieldType, - getDefaultTimeRange, MutableDataFrame, ScopedVars, toDataFrame, @@ -45,10 +42,6 @@ export class JaegerDatasource extends DataSourceWithBackend): Observable { // At this moment we expect only one target. In case we somehow change the UI to be able to show multiple // traces at one we need to change this. @@ -122,25 +115,11 @@ export class JaegerDatasource extends DataSourceWithBackend { - it('should transform Jaeger dependencies API response', () => { - const data = { - data: [ - { - parent: 'serviceA', - child: 'serviceB', - callCount: 1, - }, - { - parent: 'serviceA', - child: 'serviceC', - callCount: 2, - }, - { - parent: 'serviceB', - child: 'serviceC', - callCount: 3, - }, - ], - total: 0, - limit: 0, - offset: 0, - }; - - const res = mapJaegerDependenciesResponse({ data }); - expect(res).toMatchObject({ - data: [ - { - fields: [ - { - config: {}, - name: 'id', - type: 'string', - values: ['serviceA', 'serviceB', 'serviceC'], - }, - { - config: {}, - name: 'title', - type: 'string', - values: ['serviceA', 'serviceB', 'serviceC'], - }, - ], - meta: { preferredVisualisationType: 'nodeGraph' }, - }, - { - fields: [ - { - config: {}, - name: 'id', - type: 'string', - values: ['serviceA--serviceB', 'serviceA--serviceC', 'serviceB--serviceC'], - }, - { - config: {}, - name: 'target', - type: 'string', - values: ['serviceB', 'serviceC', 'serviceC'], - }, - { - config: {}, - name: 'source', - type: 'string', - values: ['serviceA', 'serviceA', 'serviceB'], - }, - { - config: { displayName: 'Call count' }, - name: 'mainstat', - type: 'string', - values: [1, 2, 3], - }, - ], - meta: { preferredVisualisationType: 'nodeGraph' }, - }, - ], - }); - }); - - it('should transform Jaeger API error', () => { - const data = { - total: 0, - limit: 0, - offset: 0, - errors: [ - { - code: 400, - msg: 'unable to parse param \'endTs\': strconv.ParseInt: parsing "foo": invalid syntax', - }, - ], - }; - - const res = mapJaegerDependenciesResponse({ data }); - - expect(res).toEqual({ - data: [], - errors: [ - { - message: 'unable to parse param \'endTs\': strconv.ParseInt: parsing "foo": invalid syntax', - status: 400, - }, - ], - }); - }); -}); diff --git a/public/app/plugins/datasource/jaeger/dependencyGraphTransform.ts b/public/app/plugins/datasource/jaeger/dependencyGraphTransform.ts deleted file mode 100644 index 0e8fe6dce46..00000000000 --- a/public/app/plugins/datasource/jaeger/dependencyGraphTransform.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { - DataFrame, - DataQueryResponse, - FieldType, - MutableDataFrame, - NodeGraphDataFrameFieldNames as Fields, -} from '@grafana/data'; - -import { JaegerServiceDependency } from './types'; - -interface Node { - [Fields.id]: string; - [Fields.title]: string; -} - -interface Edge { - [Fields.id]: string; - [Fields.target]: string; - [Fields.source]: string; - [Fields.mainStat]: number; -} - -/** - * Error schema used by the Jaeger dependencies API. - */ -interface JaegerDependenciesResponseError { - code: number; - msg: string; -} - -interface JaegerDependenciesResponse { - data?: { - errors?: JaegerDependenciesResponseError[]; - data?: JaegerServiceDependency[]; - }; -} - -/** - * Transforms a Jaeger dependencies API response to a Grafana {@link DataQueryResponse}. - * @param response Raw response data from the API proxy. - */ -export function mapJaegerDependenciesResponse(response: JaegerDependenciesResponse): DataQueryResponse { - const errors = response?.data?.errors; - if (errors) { - return { - data: [], - errors: errors.map((e: JaegerDependenciesResponseError) => ({ message: e.msg, status: e.code })), - }; - } - const dependencies = response?.data?.data; - if (dependencies) { - return { - data: convertDependenciesToGraph(dependencies), - }; - } - - return { data: [] }; -} - -/** - * Converts a list of Jaeger service dependencies to a Grafana {@link DataFrame} array suitable for the node graph panel. - * @param dependencies List of Jaeger service dependencies as returned by the Jaeger dependencies API. - */ -function convertDependenciesToGraph(dependencies: JaegerServiceDependency[]): DataFrame[] { - const servicesByName = new Map(); - const edges: Edge[] = []; - - for (const dependency of dependencies) { - addServiceNode(dependency.parent, servicesByName); - addServiceNode(dependency.child, servicesByName); - - edges.push({ - [Fields.id]: dependency.parent + '--' + dependency.child, - [Fields.target]: dependency.child, - [Fields.source]: dependency.parent, - [Fields.mainStat]: dependency.callCount, - }); - } - - const nodesFrame = new MutableDataFrame({ - fields: [ - { name: Fields.id, type: FieldType.string }, - { name: Fields.title, type: FieldType.string }, - ], - meta: { - preferredVisualisationType: 'nodeGraph', - }, - }); - - const edgesFrame = new MutableDataFrame({ - fields: [ - { name: Fields.id, type: FieldType.string }, - { name: Fields.target, type: FieldType.string }, - { name: Fields.source, type: FieldType.string }, - { name: Fields.mainStat, type: FieldType.string, config: { displayName: 'Call count' } }, - ], - meta: { - preferredVisualisationType: 'nodeGraph', - }, - }); - - for (const node of servicesByName.values()) { - nodesFrame.add(node); - } - - for (const edge of edges) { - edgesFrame.add(edge); - } - - return [nodesFrame, edgesFrame]; -} - -/** - * Convenience function to register a service node in the dependency graph. - * @param service Name of the service to register. - * @param servicesByName Map of service nodes keyed name. - */ -function addServiceNode(service: string, servicesByName: Map) { - if (!servicesByName.has(service)) { - servicesByName.set(service, { - [Fields.id]: service, - [Fields.title]: service, - }); - } -} diff --git a/public/app/plugins/datasource/jaeger/responseTransform.ts b/public/app/plugins/datasource/jaeger/responseTransform.ts index 35446e221b8..cd56f08e61f 100644 --- a/public/app/plugins/datasource/jaeger/responseTransform.ts +++ b/public/app/plugins/datasource/jaeger/responseTransform.ts @@ -1,13 +1,5 @@ -import { - DataFrame, - DataSourceInstanceSettings, - FieldType, - MutableDataFrame, - TraceLog, - TraceSpanRow, -} from '@grafana/data'; +import { DataFrame, FieldType, MutableDataFrame, TraceLog, TraceSpanRow } from '@grafana/data'; -import transformTraceData from './_importedDependencies/model/transform-trace-data'; import { JaegerResponse, Span, TraceProcess, TraceResponse } from './types'; export function createTraceFrame(data: TraceResponse): DataFrame { @@ -68,62 +60,6 @@ function toSpanRow(span: Span, processes: Record): TraceSp }; } -export function createTableFrame(data: TraceResponse[], instanceSettings: DataSourceInstanceSettings): DataFrame { - const frame = new MutableDataFrame({ - fields: [ - { - name: 'traceID', - type: FieldType.string, - config: { - unit: 'string', - displayNameFromDS: 'Trace ID', - links: [ - { - title: 'Trace: ${__value.raw}', - url: '', - internal: { - datasourceUid: instanceSettings.uid, - datasourceName: instanceSettings.name, - query: { - query: '${__value.raw}', - }, - }, - }, - ], - }, - }, - { name: 'traceName', type: FieldType.string, config: { displayNameFromDS: 'Trace name' } }, - { name: 'startTime', type: FieldType.time, config: { displayNameFromDS: 'Start time' } }, - { name: 'duration', type: FieldType.number, config: { displayNameFromDS: 'Duration', unit: 'µs' } }, - ], - meta: { - preferredVisualisationType: 'table', - }, - }); - // Show the most recent traces - const traceData = data.map(transformToTraceData).sort((a, b) => b?.startTime! - a?.startTime!); - - for (const trace of traceData) { - frame.add(trace); - } - - return frame; -} - -function transformToTraceData(data: TraceResponse) { - const traceData = transformTraceData(data); - if (!traceData) { - return; - } - - return { - traceID: traceData.traceID, - startTime: traceData.startTime / 1000, - duration: traceData.duration, - traceName: traceData.traceName, - }; -} - export function transformToJaeger(data: MutableDataFrame): JaegerResponse { let traceResponse: TraceResponse = { traceID: '', diff --git a/public/app/plugins/datasource/jaeger/types.ts b/public/app/plugins/datasource/jaeger/types.ts index 7173be50a1f..0ddf78bc2b4 100644 --- a/public/app/plugins/datasource/jaeger/types.ts +++ b/public/app/plugins/datasource/jaeger/types.ts @@ -1,9 +1,5 @@ -import { DataQuery, TraceKeyValuePair, TraceLog } from '@grafana/data'; - -export type TraceLink = { - url: string; - text: string; -}; +import { TraceKeyValuePair, TraceLog } from '@grafana/data'; +import { DataQuery } from '@grafana/schema'; export type TraceProcess = { serviceName: string; diff --git a/public/app/plugins/datasource/jaeger/util.ts b/public/app/plugins/datasource/jaeger/util.ts deleted file mode 100644 index f025ed9df55..00000000000 --- a/public/app/plugins/datasource/jaeger/util.ts +++ /dev/null @@ -1,26 +0,0 @@ -import logfmt from 'logfmt'; - -export function convertTagsLogfmt(tags: string | undefined) { - if (!tags) { - return ''; - } - const data = logfmt.parse(tags); - Object.keys(data).forEach((key) => { - const value = data[key]; - if (typeof value !== 'string') { - data[key] = String(value); - } - }); - return JSON.stringify(data); -} - -export function transformToLogfmt(tags: string | undefined) { - if (!tags) { - return ''; - } - try { - return logfmt.stringify(JSON.parse(tags)); - } catch { - return tags; - } -} From 6cd85eb1b3d42f2401a1f98387761c6abc639a94 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 31 Jul 2025 13:33:03 +0300 Subject: [PATCH 088/126] Dashboard: Fix for viewing a repeated panel that is out of view (#108912) --- .../features/dashboard-scene/scene/DashboardSceneUrlSync.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 3d9acceed0b..e19a7331d5c 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -208,8 +208,11 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { if (panel) { this._viewEventSub?.unsubscribe(); this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: panel.getRef() }) }); + this._viewEventSub = undefined; } }); + + this._scene.state.body.activateRepeaters?.(); } } From 372f115db956f02dcb36535eb290bae4321f624a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Thu, 31 Jul 2025 13:45:55 +0200 Subject: [PATCH 089/126] Finish building index even if original context is canceled. (#108969) --- pkg/storage/unified/resource/search.go | 4 ++ pkg/storage/unified/resource/search_test.go | 49 ++++++++++++++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index 721f7105b30..4752fa35cec 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -644,6 +644,10 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso } ch := s.buildIndex.DoChan(key.String(), func() (interface{}, error) { + // We want to finish building of the index even if original context is canceled. + // We reuse original context without cancel to keep the tracing spans correct. + ctx := context.WithoutCancel(ctx) + // Recheck if some other goroutine managed to build an index in the meantime. // (That is, it finished running this function and stored the index into the cache) idx, err := s.search.GetIndex(ctx, key) diff --git a/pkg/storage/unified/resource/search_test.go b/pkg/storage/unified/resource/search_test.go index a05c74c8a4b..677e24b8b8b 100644 --- a/pkg/storage/unified/resource/search_test.go +++ b/pkg/storage/unified/resource/search_test.go @@ -340,7 +340,7 @@ func TestSearchGetOrCreateIndexWithCancellation(t *testing.T) { {NamespacedResource: NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, Count: 50, ResourceVersion: 11111111}, }, } - search := &slowSearchBackend{ + search := &slowSearchBackendWithCache{ mockSearchBackend: mockSearchBackend{}, } supplier := &TestDocumentBuilderSupplier{ @@ -362,10 +362,12 @@ func TestSearchGetOrCreateIndexWithCancellation(t *testing.T) { require.NoError(t, err) require.NotNil(t, support) + key := NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"} + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() - _, err = support.getOrCreateIndex(ctx, NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, "test") + _, err = support.getOrCreateIndex(ctx, key, "test") // Make sure we get context deadline error require.ErrorIs(t, err, context.DeadlineExceeded) @@ -373,16 +375,53 @@ func TestSearchGetOrCreateIndexWithCancellation(t *testing.T) { search.wg.Wait() require.NotEmpty(t, search.buildIndexCalls) + + // Wait until new index is put into cache. + require.Eventually(t, func() bool { + idx, err := support.search.GetIndex(ctx, key) + return err == nil && idx != nil + }, 1*time.Second, 100*time.Millisecond, "Indexing finishes despite context cancellation") + + // Second call to getOrCreateIndex returns index immediately, even if context is canceled, as the index is now ready and cached. + _, err = support.getOrCreateIndex(ctx, key, "test") + require.NoError(t, err) } -type slowSearchBackend struct { +type slowSearchBackendWithCache struct { mockSearchBackend wg sync.WaitGroup + + mu sync.Mutex + cache map[NamespacedResource]ResourceIndex } -func (m *slowSearchBackend) BuildIndex(ctx context.Context, key NamespacedResource, size int64, resourceVersion int64, fields SearchableDocumentFields, reason string, builder func(index ResourceIndex) (int64, error)) (ResourceIndex, error) { +func (m *slowSearchBackendWithCache) GetIndex(ctx context.Context, key NamespacedResource) (ResourceIndex, error) { + m.mu.Lock() + defer m.mu.Unlock() + return m.cache[key], nil +} + +func (m *slowSearchBackendWithCache) BuildIndex(ctx context.Context, key NamespacedResource, size int64, resourceVersion int64, fields SearchableDocumentFields, reason string, builder func(index ResourceIndex) (int64, error)) (ResourceIndex, error) { m.wg.Add(1) defer m.wg.Done() + time.Sleep(1 * time.Second) - return m.mockSearchBackend.BuildIndex(ctx, key, size, resourceVersion, fields, reason, builder) + + // Simulate erroring out when context is cancelled. + if ctx.Err() != nil { + return nil, ctx.Err() + } + idx, err := m.mockSearchBackend.BuildIndex(ctx, key, size, resourceVersion, fields, reason, builder) + if err != nil { + return nil, err + } + + m.mu.Lock() + defer m.mu.Unlock() + + if m.cache == nil { + m.cache = make(map[NamespacedResource]ResourceIndex) + } + m.cache[key] = idx + return idx, nil } From e022b6593b58f288a71e210d5161e545a3fa46b7 Mon Sep 17 00:00:00 2001 From: "lean.dev" <34773040+leandro-deveikis@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:51:04 +0100 Subject: [PATCH 090/126] SecretsManager: fix log (#108970) --- pkg/storage/secret/metadata/secure_value_store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/storage/secret/metadata/secure_value_store.go b/pkg/storage/secret/metadata/secure_value_store.go index a9c2d0664d6..5782ee9a3e4 100644 --- a/pkg/storage/secret/metadata/secure_value_store.go +++ b/pkg/storage/secret/metadata/secure_value_store.go @@ -265,7 +265,7 @@ func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.N defer span.End() defer func() { - logging.FromContext(ctx).Info("SecureValueMetadataStorage.Read", "namespace", namespace, "name", name, "success", readErr != nil, "error", readErr) + logging.FromContext(ctx).Info("SecureValueMetadataStorage.Read", "namespace", namespace, "name", name, "success", readErr == nil, "error", readErr) s.metrics.SecureValueMetadataGetDuration.Observe(time.Since(start).Seconds()) s.metrics.SecureValueMetadataGetCount.Inc() From 3e752c2e35e20e30f52504aa8225cb2fc0e17dad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 13:11:46 +0100 Subject: [PATCH 091/126] Update dependency typescript to v5.8.3 (#108966) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../app/plugins/datasource/loki/package.json | 2 +- yarn.lock | 22 +------------------ 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/datasource/loki/package.json b/public/app/plugins/datasource/loki/package.json index 75a465d608c..cffc2dc7dcf 100644 --- a/public/app/plugins/datasource/loki/package.json +++ b/public/app/plugins/datasource/loki/package.json @@ -39,7 +39,7 @@ "@types/uuid": "10.0.0", "jest": "29.7.0", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index 94cb5b420db..d609c509945 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2781,7 +2781,7 @@ __metadata: rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" uuid: "npm:11.1.0" webpack: "npm:5.97.1" peerDependencies: @@ -31626,16 +31626,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:5.7.3": - version: 5.7.3 - resolution: "typescript@npm:5.7.3" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10/6a7e556de91db3d34dc51cd2600e8e91f4c312acd8e52792f243c7818dfadb27bae677175fad6947f9c81efb6c57eb6b2d0c736f196a6ee2f1f7d57b74fc92fa - languageName: node - linkType: hard - "typescript@npm:5.8.3, typescript@npm:>=2.7, typescript@npm:>=3 < 6, typescript@npm:^5.0.4, typescript@npm:^5.4.5, typescript@npm:^5.5.4": version: 5.8.3 resolution: "typescript@npm:5.8.3" @@ -31666,16 +31656,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A5.7.3#optional!builtin": - version: 5.7.3 - resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin::version=5.7.3&hash=5786d5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10/dc58d777eb4c01973f7fbf1fd808aad49a0efdf545528dab9b07d94fdcb65b8751742804c3057e9619a4627f2d9cc85547fdd49d9f4326992ad0181b49e61d81 - languageName: node - linkType: hard - "typescript@patch:typescript@npm%3A5.8.3#optional!builtin, typescript@patch:typescript@npm%3A>=2.7#optional!builtin, typescript@patch:typescript@npm%3A>=3 < 6#optional!builtin, typescript@patch:typescript@npm%3A^5.0.4#optional!builtin, typescript@patch:typescript@npm%3A^5.4.5#optional!builtin, typescript@patch:typescript@npm%3A^5.5.4#optional!builtin": version: 5.8.3 resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" From 41df2e9d26128f2948d1f6c58e73bbf6095bdc8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 13:27:36 +0100 Subject: [PATCH 092/126] Bump actions/setup-go from 4.2.1 to 5.5.0 (#108286) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/alerting-swagger-gen.yml | 2 +- .github/workflows/alerting-update-module.yml | 2 +- .github/workflows/backend-code-checks.yml | 2 +- .github/workflows/backend-unit-tests.yml | 4 ++-- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/core-plugins-build-and-release.yml | 2 +- .github/workflows/feature-toggles-ci.yml | 2 +- .github/workflows/go-lint.yml | 2 +- .github/workflows/pr-dependabot-update-go-workspace.yml | 2 +- .github/workflows/pr-e2e-tests.yml | 2 +- .github/workflows/pr-go-workspace-check.yml | 4 ++-- .github/workflows/pr-k8s-codegen-check.yml | 2 +- .github/workflows/pr-test-integration.yml | 6 +++--- .github/workflows/publish-kinds-next.yml | 2 +- .github/workflows/publish-kinds-release.yml | 2 +- .github/workflows/release-pr.yml | 2 +- .github/workflows/run-dashboard-search-e2e.yml | 2 +- .github/workflows/run-schema-v2-e2e.yml | 2 +- .github/workflows/swagger-gen.yml | 2 +- .github/workflows/verify-kinds.yml | 2 +- 20 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/alerting-swagger-gen.yml b/.github/workflows/alerting-swagger-gen.yml index c06304b38a3..19476764b69 100644 --- a/.github/workflows/alerting-swagger-gen.yml +++ b/.github/workflows/alerting-swagger-gen.yml @@ -15,7 +15,7 @@ jobs: fetch-depth: 2 persist-credentials: false - name: Set go version - uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: go-version-file: go.mod - name: Build swagger diff --git a/.github/workflows/alerting-update-module.yml b/.github/workflows/alerting-update-module.yml index dce80d065e4..0b8d5730618 100644 --- a/.github/workflows/alerting-update-module.yml +++ b/.github/workflows/alerting-update-module.yml @@ -29,7 +29,7 @@ jobs: fi - name: Setup Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # 5.3.0 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # 5.5.0 with: "go-version-file": "go.mod" diff --git a/.github/workflows/backend-code-checks.yml b/.github/workflows/backend-code-checks.yml index b95257d99c3..bf7252a95f5 100644 --- a/.github/workflows/backend-code-checks.yml +++ b/.github/workflows/backend-code-checks.yml @@ -27,7 +27,7 @@ jobs: with: persist-credentials: false - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v5.5.0 with: # Explicitly set Go version to 1.24.1 to ensure consistent OpenAPI spec generation # The crypto/x509 package has additional fields in Go 1.24.1 that affect the generated specs diff --git a/.github/workflows/backend-unit-tests.yml b/.github/workflows/backend-unit-tests.yml index 1c839432829..0d3c468b21f 100644 --- a/.github/workflows/backend-unit-tests.yml +++ b/.github/workflows/backend-unit-tests.yml @@ -57,7 +57,7 @@ jobs: with: persist-credentials: false - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v5.5.0 with: go-version-file: go.mod - name: Run unit tests @@ -92,7 +92,7 @@ jobs: with: persist-credentials: false - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v5.5.0 with: go-version-file: go.mod - name: Setup Enterprise diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a07cb222898..eea0caa319c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -50,7 +50,7 @@ jobs: - if: matrix.language == 'go' name: Set go version - uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: go-version-file: go.mod diff --git a/.github/workflows/core-plugins-build-and-release.yml b/.github/workflows/core-plugins-build-and-release.yml index c3c934ea2b6..ec8b1db511f 100644 --- a/.github/workflows/core-plugins-build-and-release.yml +++ b/.github/workflows/core-plugins-build-and-release.yml @@ -101,7 +101,7 @@ jobs: echo "has_backend=false" >> "$GITHUB_OUTPUT" fi - name: Setup golang environment - uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 if: steps.check_backend.outputs.has_backend == 'true' with: go-version-file: go.mod diff --git a/.github/workflows/feature-toggles-ci.yml b/.github/workflows/feature-toggles-ci.yml index ab1aa9b2dca..880810a333a 100644 --- a/.github/workflows/feature-toggles-ci.yml +++ b/.github/workflows/feature-toggles-ci.yml @@ -22,7 +22,7 @@ jobs: persist-credentials: false - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v5.5.0 with: go-version-file: 'go.mod' cache: true diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml index 78788f3570c..f870c16a847 100644 --- a/.github/workflows/go-lint.yml +++ b/.github/workflows/go-lint.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v5.5.0 with: go-version-file: ./go.mod - name: golangci-lint diff --git a/.github/workflows/pr-dependabot-update-go-workspace.yml b/.github/workflows/pr-dependabot-update-go-workspace.yml index 82169bd69ac..b0dbc68c266 100644 --- a/.github/workflows/pr-dependabot-update-go-workspace.yml +++ b/.github/workflows/pr-dependabot-update-go-workspace.yml @@ -45,7 +45,7 @@ jobs: persist-credentials: false - name: Set go version - uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: go-version-file: go.mod diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index bbae7d86c65..6ceff2b9d45 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -113,7 +113,7 @@ jobs: with: persist-credentials: false - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v5.5.0 with: go-version-file: go.mod cache: ${{ !github.event.pull_request.head.repo.fork }} diff --git a/.github/workflows/pr-go-workspace-check.yml b/.github/workflows/pr-go-workspace-check.yml index 91dc3037f11..0fa96a1da3f 100644 --- a/.github/workflows/pr-go-workspace-check.yml +++ b/.github/workflows/pr-go-workspace-check.yml @@ -39,7 +39,7 @@ jobs: persist-credentials: false - name: Set go version - uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: cache: false go-version-file: go.mod @@ -76,7 +76,7 @@ jobs: persist-credentials: false - name: Set go version - uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: cache: false go-version-file: go.mod diff --git a/.github/workflows/pr-k8s-codegen-check.yml b/.github/workflows/pr-k8s-codegen-check.yml index 6c34674e5c9..5441b4780b5 100644 --- a/.github/workflows/pr-k8s-codegen-check.yml +++ b/.github/workflows/pr-k8s-codegen-check.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Set go version - uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: go-version-file: go.mod diff --git a/.github/workflows/pr-test-integration.yml b/.github/workflows/pr-test-integration.yml index 76f2068172c..314ca335979 100644 --- a/.github/workflows/pr-test-integration.yml +++ b/.github/workflows/pr-test-integration.yml @@ -37,7 +37,7 @@ jobs: with: persist-credentials: false - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v5.5.0 with: go-version-file: go.mod cache: true @@ -81,7 +81,7 @@ jobs: with: persist-credentials: false - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v5.5.0 with: go-version-file: go.mod cache: true @@ -126,7 +126,7 @@ jobs: with: persist-credentials: false - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v5.5.0 with: go-version-file: go.mod cache: true diff --git a/.github/workflows/publish-kinds-next.yml b/.github/workflows/publish-kinds-next.yml index 495cb35abae..901d6fc3587 100644 --- a/.github/workflows/publish-kinds-next.yml +++ b/.github/workflows/publish-kinds-next.yml @@ -27,7 +27,7 @@ jobs: persist-credentials: false - name: "Setup Go" - uses: "actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639" + uses: "actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5" with: go-version-file: go.mod diff --git a/.github/workflows/publish-kinds-release.yml b/.github/workflows/publish-kinds-release.yml index 03873c27641..0fd991d66f3 100644 --- a/.github/workflows/publish-kinds-release.yml +++ b/.github/workflows/publish-kinds-release.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: "Setup Go" - uses: "actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639" + uses: "actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5" with: go-version-file: go.mod diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 2c43ae43de4..8018aaca34d 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -136,7 +136,7 @@ jobs: uses: actions/setup-node@v4 with: node-version-file: .nvmrc - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v5.5.0 with: go-version-file: go.mod - name: Configure git user diff --git a/.github/workflows/run-dashboard-search-e2e.yml b/.github/workflows/run-dashboard-search-e2e.yml index 6eebaf9320a..3988a4390c9 100644 --- a/.github/workflows/run-dashboard-search-e2e.yml +++ b/.github/workflows/run-dashboard-search-e2e.yml @@ -29,7 +29,7 @@ jobs: with: persist-credentials: false - name: Pin Go version to mod file - uses: actions/setup-go@v5 + uses: actions/setup-go@v5.5.0 with: go-version-file: 'go.mod' cache: true diff --git a/.github/workflows/run-schema-v2-e2e.yml b/.github/workflows/run-schema-v2-e2e.yml index a13b953648b..f1ce54a24ff 100644 --- a/.github/workflows/run-schema-v2-e2e.yml +++ b/.github/workflows/run-schema-v2-e2e.yml @@ -22,7 +22,7 @@ jobs: with: persist-credentials: false - name: Pin Go version to mod file - uses: actions/setup-go@v5 + uses: actions/setup-go@v5.5.0 with: go-version-file: 'go.mod' - run: go version diff --git a/.github/workflows/swagger-gen.yml b/.github/workflows/swagger-gen.yml index ee73adc1c28..0e42e0e7441 100644 --- a/.github/workflows/swagger-gen.yml +++ b/.github/workflows/swagger-gen.yml @@ -28,7 +28,7 @@ jobs: with: persist-credentials: false - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v5.5.0 with: go-version-file: go.mod - name: Setup Enterprise diff --git a/.github/workflows/verify-kinds.yml b/.github/workflows/verify-kinds.yml index c793dcb2895..1d6e8e80b38 100644 --- a/.github/workflows/verify-kinds.yml +++ b/.github/workflows/verify-kinds.yml @@ -17,7 +17,7 @@ jobs: persist-credentials: false - name: "Setup Go" - uses: "actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639" + uses: "actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5" with: go-version-file: go.mod From 454fae17867635c37f14794f7d9cf3189155bdd6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:37:33 +0000 Subject: [PATCH 093/126] Update dependency webpack to v5.101.0 (#108972) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-plugin-configs/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/loki/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 | 91 +++++++++++-------- 16 files changed, 66 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index de65edb4836..32091e8b962 100644 --- a/package.json +++ b/package.json @@ -251,7 +251,7 @@ "ts-jest": "29.4.0", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1", + "webpack": "5.101.0", "webpack-assets-manifest": "^5.1.0", "webpack-cli": "6.0.1", "webpack-livereload-plugin": "3.0.2", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index 3ca7029e86d..42233bea781 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -26,7 +26,7 @@ "replace-in-file-webpack-plugin": "1.0.6", "swc-loader": "0.2.6", "typescript": "5.8.3", - "webpack": "5.97.1", + "webpack": "5.101.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-virtual-modules": "^0.6.2" }, diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 48bd1e11c66..2193c02d0e8 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -204,7 +204,7 @@ "storybook": "^8.6.2", "style-loader": "4.0.0", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "react": "^18.0.0", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index a7b771654d3..056b8688f35 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -43,7 +43,7 @@ "react-select-event": "5.5.1", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 6063cb8f0a1..7181c557951 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -43,7 +43,7 @@ "react-select-event": "5.5.1", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index bb91cdc6cc6..40ca80efa0b 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -28,7 +28,7 @@ "jest": "29.7.0", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 5ed1bd369a5..612b26365b0 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -36,7 +36,7 @@ "style-loader": "4.0.0", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index d8a2c573280..aafaa01cfb5 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -37,7 +37,7 @@ "jest": "29.7.0", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index b4c8fefdc08..7733e2817ef 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -39,7 +39,7 @@ "jest": "29.7.0", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/loki/package.json b/public/app/plugins/datasource/loki/package.json index cffc2dc7dcf..937fba60812 100644 --- a/public/app/plugins/datasource/loki/package.json +++ b/public/app/plugins/datasource/loki/package.json @@ -40,7 +40,7 @@ "jest": "29.7.0", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index 46084541319..48940aedbdd 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -29,7 +29,7 @@ "i18next-parser": "9.3.0", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index cb07deb595a..b8bcf1e9e14 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -28,7 +28,7 @@ "jest": "29.7.0", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index 999694c2dac..a3c421b7a16 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -29,7 +29,7 @@ "jest": "29.7.0", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index e889d8c30df..7afedbaff53 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -56,7 +56,7 @@ "react-select-event": "5.5.1", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 4e1a7530854..bb336e2173d 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -32,7 +32,7 @@ "jest": "29.7.0", "ts-node": "10.9.2", "typescript": "5.8.3", - "webpack": "5.97.1" + "webpack": "5.101.0" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/yarn.lock b/yarn.lock index d609c509945..159ca0b7a83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2583,7 +2583,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.8.1" typescript: "npm:5.8.3" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -2615,7 +2615,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.8.1" typescript: "npm:5.8.3" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -2655,7 +2655,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.8.1" typescript: "npm:5.8.3" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -2696,7 +2696,7 @@ __metadata: tslib: "npm:2.8.1" typescript: "npm:5.8.3" uuid: "npm:11.1.0" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -2739,7 +2739,7 @@ __metadata: tslib: "npm:2.8.1" typescript: "npm:5.8.3" uuid: "npm:11.1.0" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -2783,7 +2783,7 @@ __metadata: tslib: "npm:2.8.1" typescript: "npm:5.8.3" uuid: "npm:11.1.0" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -2816,7 +2816,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.8.1" typescript: "npm:5.8.3" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -2848,7 +2848,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.8.1" typescript: "npm:5.8.3" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -2881,7 +2881,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.8.1" typescript: "npm:5.8.3" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -2928,7 +2928,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.8.1" typescript: "npm:5.8.3" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -2988,7 +2988,7 @@ __metadata: tslib: "npm:2.8.1" typescript: "npm:5.8.3" uuid: "npm:11.1.0" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -3024,7 +3024,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.8.1" typescript: "npm:5.8.3" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -3439,7 +3439,7 @@ __metadata: swc-loader: "npm:0.2.6" tslib: "npm:2.8.1" typescript: "npm:5.8.3" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" webpack-bundle-analyzer: "npm:^4.10.2" webpack-virtual-modules: "npm:^0.6.2" languageName: unknown @@ -3875,7 +3875,7 @@ __metadata: uplot: "npm:1.6.32" uuid: "npm:11.1.0" uwrap: "npm:0.1.2" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 @@ -9476,7 +9476,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5, @types/estree@npm:^1.0.6": +"@types/estree@npm:*, @types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" checksum: 10/25a4c16a6752538ffde2826c2cc0c6491d90e69cd6187bef4a006dd2c3c45469f049e643d7e516c515f21484dc3d48fd5c870be158a5beb72f5baf3dc43e4099 @@ -11102,6 +11102,15 @@ __metadata: languageName: node linkType: hard +"acorn-import-phases@npm:^1.0.3": + version: 1.0.4 + resolution: "acorn-import-phases@npm:1.0.4" + peerDependencies: + acorn: ^8.14.0 + checksum: 10/471050ac7d9b61909c837b426de9eeef2958997f6277ad7dea88d5894fd9b3245d8ed4a225c2ca44f814dbb20688009db7a80e525e8196fc9e98c5285b66161d + languageName: node + linkType: hard + "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -11120,7 +11129,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.15.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.15.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2": version: 8.15.0 resolution: "acorn@npm:8.15.0" bin: @@ -15669,13 +15678,13 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.17.1": - version: 5.17.1 - resolution: "enhanced-resolve@npm:5.17.1" +"enhanced-resolve@npm:^5.17.1, enhanced-resolve@npm:^5.17.2": + version: 5.18.2 + resolution: "enhanced-resolve@npm:5.18.2" dependencies: graceful-fs: "npm:^4.2.4" tapable: "npm:^2.2.0" - checksum: 10/e8e03cb7a4bf3c0250a89afbd29e5ec20e90ba5fcd026066232a0754864d7d0a393fa6fc0e5379314a6529165a1834b36731147080714459d98924520410d8f5 + checksum: 10/d1b517c908b69d1afbf87b476bbe7dd8d1daf11070127b9ec4f8553f0c6020d30f79103c938776645d569e954e4e04c326f408d2ea3820ade71e72798fb7d36f languageName: node linkType: hard @@ -18634,7 +18643,7 @@ __metadata: uuid: "npm:11.1.0" vis-data: "npm:^8.0.0" vis-network: "npm:10.0.1" - webpack: "npm:5.97.1" + webpack: "npm:5.101.0" webpack-assets-manifest: "npm:^5.1.0" webpack-cli: "npm:6.0.1" webpack-livereload-plugin: "npm:3.0.2" @@ -28789,15 +28798,15 @@ __metadata: languageName: node linkType: hard -"schema-utils@npm:>1.0.0, schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0, schema-utils@npm:^4.3.0": - version: 4.3.0 - resolution: "schema-utils@npm:4.3.0" +"schema-utils@npm:>1.0.0, schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0, schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.2": + version: 4.3.2 + resolution: "schema-utils@npm:4.3.2" dependencies: "@types/json-schema": "npm:^7.0.9" ajv: "npm:^8.9.0" ajv-formats: "npm:^2.1.1" ajv-keywords: "npm:^5.1.0" - checksum: 10/86c5a7c72a275c56f140bc3cdd832d56efb11428c88ad588127db12cb9b2c83ccaa9540e115d7baa9c6175b5e360094457e29c44e6fb76787c9498c2eb6df5d6 + checksum: 10/02c32c34aae762d48468f98465a96a167fede637772871c7c7d8923671ddb9f20b2cc6f6e8448ae6bef5363e3597493c655212c8b06a4ee73aa099d9452fbd8b languageName: node linkType: hard @@ -30702,7 +30711,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:5.3.14, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10": +"terser-webpack-plugin@npm:5.3.14, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.11": version: 5.3.14 resolution: "terser-webpack-plugin@npm:5.3.14" dependencies: @@ -32573,10 +32582,10 @@ __metadata: languageName: node linkType: hard -"webpack-sources@npm:^3.2.3": - version: 3.2.3 - resolution: "webpack-sources@npm:3.2.3" - checksum: 10/a661f41795d678b7526ae8a88cd1b3d8ce71a7d19b6503da8149b2e667fc7a12f9b899041c1665d39e38245ed3a59ab68de648ea31040c3829aa695a5a45211d +"webpack-sources@npm:^3.2.3, webpack-sources@npm:^3.3.3": + version: 3.3.3 + resolution: "webpack-sources@npm:3.3.3" + checksum: 10/ec5d72607e8068467370abccbfff855c596c098baedbe9d198a557ccf198e8546a322836a6f74241492576adba06100286592993a62b63196832cdb53c8bae91 languageName: node linkType: hard @@ -32607,19 +32616,21 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5, webpack@npm:5.97.1, webpack@npm:^5": - version: 5.97.1 - resolution: "webpack@npm:5.97.1" +"webpack@npm:5, webpack@npm:5.101.0, webpack@npm:^5": + version: 5.101.0 + resolution: "webpack@npm:5.101.0" dependencies: "@types/eslint-scope": "npm:^3.7.7" - "@types/estree": "npm:^1.0.6" + "@types/estree": "npm:^1.0.8" + "@types/json-schema": "npm:^7.0.15" "@webassemblyjs/ast": "npm:^1.14.1" "@webassemblyjs/wasm-edit": "npm:^1.14.1" "@webassemblyjs/wasm-parser": "npm:^1.14.1" - acorn: "npm:^8.14.0" + acorn: "npm:^8.15.0" + acorn-import-phases: "npm:^1.0.3" browserslist: "npm:^4.24.0" chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.17.1" + enhanced-resolve: "npm:^5.17.2" es-module-lexer: "npm:^1.2.1" eslint-scope: "npm:5.1.1" events: "npm:^3.2.0" @@ -32629,17 +32640,17 @@ __metadata: loader-runner: "npm:^4.2.0" mime-types: "npm:^2.1.27" neo-async: "npm:^2.6.2" - schema-utils: "npm:^3.2.0" + schema-utils: "npm:^4.3.2" tapable: "npm:^2.1.1" - terser-webpack-plugin: "npm:^5.3.10" + terser-webpack-plugin: "npm:^5.3.11" watchpack: "npm:^2.4.1" - webpack-sources: "npm:^3.2.3" + webpack-sources: "npm:^3.3.3" peerDependenciesMeta: webpack-cli: optional: true bin: webpack: bin/webpack.js - checksum: 10/665bd3b8c84b20f0b1f250159865e4d3e9b76c682030313d49124d5f8e96357ccdcc799dd9fe0ebf010fdb33dbc59d9863d79676a308e868e360ac98f7c09987 + checksum: 10/edeb24a88cbe96902c601bc1292cd344d27c89c5eceb187f4e9936c8a17356c62b23a5dc441c12b16e62bb6c40804bd10d691135c638398f921760f2dba6a761 languageName: node linkType: hard From 9d2cb8ddf1e692062a03031e727b448932065972 Mon Sep 17 00:00:00 2001 From: Alexa Vargas <239999+axelavargas@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:52:12 +0200 Subject: [PATCH 094/126] Query Library: Add `highlightQuery` to QueryLibraryContext (#108617) * add hightlightedQuery to QueryLibraryContext * revert go changes --- .../features/explore/QueryLibrary/QueryLibraryContext.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx index 9037182c156..eeb7c3d9b1d 100644 --- a/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx +++ b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx @@ -24,7 +24,11 @@ export type QueryLibraryContextType = { openDrawer: ( datasourceFilters: string[], onSelectQuery: OnSelectQueryType, - options?: { isReplacingQuery?: boolean; context?: string } + options?: { + isReplacingQuery?: boolean; + context?: string; + highlightQuery?: string; + } ) => void; closeDrawer: () => void; isDrawerOpen: boolean; From d469a32491950e91207e2089aa8347e88d37ef61 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 31 Jul 2025 14:55:02 +0200 Subject: [PATCH 095/126] K8s/DataQuery: Register explicit equality function (#108907) --- pkg/services/apiserver/builder/openapi.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pkg/services/apiserver/builder/openapi.go b/pkg/services/apiserver/builder/openapi.go index 9489ac38b95..cde3548a136 100644 --- a/pkg/services/apiserver/builder/openapi.go +++ b/pkg/services/apiserver/builder/openapi.go @@ -1,21 +1,42 @@ package builder import ( + "bytes" + "encoding/json" "maps" "strings" + "sync" + apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime/schema" openapi "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" spec "k8s.io/kube-openapi/pkg/validation/spec" + "github.com/grafana/grafana-app-sdk/logging" data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" secret "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) +var ( + equalityInit sync.Once +) + // This should eventually live in grafana-app-sdk func GetOpenAPIDefinitions(builders []APIGroupBuilder, additionalGetters ...openapi.GetOpenAPIDefinitions) openapi.GetOpenAPIDefinitions { + equalityInit.Do(func() { + // DataQuery has private variables, so it needs an explicit equality helper + err := apiequality.Semantic.AddFunc( + func(a, b data.DataQuery) bool { + aa, _ := json.Marshal(a) + bb, _ := json.Marshal(b) + return bytes.Equal(aa, bb) + }, + ) + logging.DefaultLogger.Error("error initializing DataQuery apiequality", "err", err) + }) + return func(ref openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition { defs := common.GetOpenAPIDefinitions(ref) // common grafana apis maps.Copy(defs, data.GetOpenAPIDefinitions(ref)) From 9e0f09e6c35c0ba992c9e2110695be68a7c5efca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 13:03:27 +0000 Subject: [PATCH 096/126] Update dependency yaml to v2.8.0 (#108975) 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 159ca0b7a83..637dd342ea9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33189,11 +33189,11 @@ __metadata: linkType: hard "yaml@npm:^2.0.0, yaml@npm:^2.3.4, yaml@npm:^2.6.0": - version: 2.7.1 - resolution: "yaml@npm:2.7.1" + version: 2.8.0 + resolution: "yaml@npm:2.8.0" bin: yaml: bin.mjs - checksum: 10/af57658d37c5efae4bac7204589b742ae01878a278554d632f01012868cf7fa66cba09b39140f12e7f6ceecc693ae52bcfb737596c4827e6e233338cb3a9528e + checksum: 10/7d4bd9c10d0e467601f496193f2ac254140f8e36f96f5ff7f852b9ce37974168eb7354f4c36dc8837dde527a2043d004b6aff48818ec24a69ab2dd3c6b6c381c languageName: node linkType: hard From 5a67e9d6402d5a1c50531e1770813a3abc3ebb5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 31 Jul 2025 15:26:43 +0200 Subject: [PATCH 097/126] query: add unit test (#108958) --- pkg/services/query/query_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pkg/services/query/query_test.go b/pkg/services/query/query_test.go index 8b0d17a381c..66c4ff17f85 100644 --- a/pkg/services/query/query_test.go +++ b/pkg/services/query/query_test.go @@ -78,6 +78,38 @@ func TestIntegrationParseMetricRequest(t *testing.T) { assert.Len(t, parsedReq.getFlattenedQueries(), 2) }) + t.Run("Test a simple single datasource query with missing time range", func(t *testing.T) { + tc := setup(t, false, nil) + mr := metricRequestWithQueries(t, `{ + "refId": "A", + "datasource": { + "uid": "gIEkMvIVz", + "type": "postgres" + } + }`, `{ + "refId": "B", + "datasource": { + "uid": "gIEkMvIVz", + "type": "postgres" + } + }`) + mr.From = "" + mr.To = "" + parsedReq, err := tc.queryService.parseMetricRequest(context.Background(), tc.signedInUser, true, mr) + require.NoError(t, err) + require.NotNil(t, parsedReq) + assert.False(t, parsedReq.hasExpression) + assert.Len(t, parsedReq.parsedQueries, 1) + assert.Contains(t, parsedReq.parsedQueries, "gIEkMvIVz") + queries := parsedReq.getFlattenedQueries() + assert.Len(t, queries, 2) + + for _, q := range queries { + require.Equal(t, int64(0), q.query.TimeRange.From.UnixMilli()) + require.Equal(t, int64(0), q.query.TimeRange.To.UnixMilli()) + } + }) + t.Run("Test a single datasource query with expressions", func(t *testing.T) { tc := setup(t, false, nil) mr := metricRequestWithQueries(t, `{ From 37ccbd77a9aa17c5304b3c1725ced8f13ea8d056 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:30:17 +0100 Subject: [PATCH 098/126] Update react-spectrum monorepo (#108977) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 8 ++++---- yarn.lock | 57 ++++++++++++---------------------------------------- 2 files changed, 17 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 32091e8b962..169c2f41f6c 100644 --- a/package.json +++ b/package.json @@ -97,10 +97,10 @@ "@npmcli/package-json": "^5.2.0", "@playwright/test": "1.54.1", "@pmmmwh/react-refresh-webpack-plugin": "0.6.1", - "@react-types/button": "3.10.2", - "@react-types/menu": "3.9.14", - "@react-types/overlays": "3.8.12", - "@react-types/shared": "3.27.0", + "@react-types/button": "3.13.0", + "@react-types/menu": "3.10.3", + "@react-types/overlays": "3.9.0", + "@react-types/shared": "3.31.0", "@rsdoctor/webpack-plugin": "^0.4.6", "@rtk-query/codegen-openapi": "^2.0.0", "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", diff --git a/yarn.lock b/yarn.lock index 637dd342ea9..c031af16035 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6660,18 +6660,7 @@ __metadata: languageName: node linkType: hard -"@react-types/button@npm:3.10.2": - version: 3.10.2 - resolution: "@react-types/button@npm:3.10.2" - dependencies: - "@react-types/shared": "npm:^3.27.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/fbc3ac3c11d6ba0f509fa8b26f7de96d77d3ef769fd474d56f1a7f430f727698dd4eb29f0f1dd75de4eb3a3cf9a805a2966e5509c60d1e5ed530f37d1a3ceb58 - languageName: node - linkType: hard - -"@react-types/button@npm:^3.13.0": +"@react-types/button@npm:3.13.0, @react-types/button@npm:^3.13.0": version: 3.13.0 resolution: "@react-types/button@npm:3.13.0" dependencies: @@ -6694,30 +6683,19 @@ __metadata: languageName: node linkType: hard -"@react-types/menu@npm:3.9.14": - version: 3.9.14 - resolution: "@react-types/menu@npm:3.9.14" +"@react-types/menu@npm:3.10.3": + version: 3.10.3 + resolution: "@react-types/menu@npm:3.10.3" dependencies: - "@react-types/overlays": "npm:^3.8.12" - "@react-types/shared": "npm:^3.27.0" + "@react-types/overlays": "npm:^3.9.0" + "@react-types/shared": "npm:^3.31.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/81ca0e57bd5affb4f53381334a876070c9c072119853541bd54f8ada91d584c717f6d604c01a648524c61b2b0e9cb2d51870ab5022f470961947f31ada01fd9c + checksum: 10/06944ecd950225642174fa17607560650dfbc67a3d34c68f400701229131c3c7c1f362ea6d50bf2b703a31899ca90a8d9dbf92609f3b76d4e55a9612d404dde6 languageName: node linkType: hard -"@react-types/overlays@npm:3.8.12": - version: 3.8.12 - resolution: "@react-types/overlays@npm:3.8.12" - dependencies: - "@react-types/shared": "npm:^3.27.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/3510add3f70c99a56b3dc160a51f5f71e60e958fd3753f2379eb904ff5442fe30d17841dee79ae77ae3b872a003757995074f45d701453186518dd357d204901 - languageName: node - linkType: hard - -"@react-types/overlays@npm:^3.8.12, @react-types/overlays@npm:^3.9.0": +"@react-types/overlays@npm:3.9.0, @react-types/overlays@npm:^3.9.0": version: 3.9.0 resolution: "@react-types/overlays@npm:3.9.0" dependencies: @@ -6728,16 +6706,7 @@ __metadata: languageName: node linkType: hard -"@react-types/shared@npm:3.27.0": - version: 3.27.0 - resolution: "@react-types/shared@npm:3.27.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/359be934dda6404824479d0dfdf9e694414252da4946b4e486f3cc546a080844a7d4f20507041d3f8f6f5a316c05b28785ca3f4377a7f4ea397ed1f21c2646af - languageName: node - linkType: hard - -"@react-types/shared@npm:^3.27.0, @react-types/shared@npm:^3.31.0": +"@react-types/shared@npm:3.31.0, @react-types/shared@npm:^3.31.0": version: 3.31.0 resolution: "@react-types/shared@npm:3.31.0" peerDependencies: @@ -18375,10 +18344,10 @@ __metadata: "@react-aria/overlays": "npm:3.28.0" "@react-aria/utils": "npm:3.30.0" "@react-awesome-query-builder/ui": "npm:6.6.15" - "@react-types/button": "npm:3.10.2" - "@react-types/menu": "npm:3.9.14" - "@react-types/overlays": "npm:3.8.12" - "@react-types/shared": "npm:3.27.0" + "@react-types/button": "npm:3.13.0" + "@react-types/menu": "npm:3.10.3" + "@react-types/overlays": "npm:3.9.0" + "@react-types/shared": "npm:3.31.0" "@reduxjs/toolkit": "npm:2.8.2" "@rsdoctor/webpack-plugin": "npm:^0.4.6" "@rtk-query/codegen-openapi": "npm:^2.0.0" From 9907191a7b23cac57b340c6f7e4207fe5ae7ad89 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 31 Jul 2025 15:32:33 +0200 Subject: [PATCH 099/126] Alerting: List V2 - Add a group link to the rule list item (#108960) * Add group link to the aler rule breadcrumbs in the new list view * Remove redundant comments --- .../rule-list/DataSourceRuleListItem.tsx | 4 ++ .../unified/rule-list/FilterView.test.tsx | 24 +++++++ .../unified/rule-list/GrafanaRuleListItem.tsx | 10 ++- .../components/AlertRuleListItem.tsx | 65 ++++++++----------- .../rule-list/components/RuleLocation.tsx | 43 ++++++++++++ 5 files changed, 108 insertions(+), 38 deletions(-) create mode 100644 public/app/features/alerting/unified/rule-list/components/RuleLocation.tsx diff --git a/public/app/features/alerting/unified/rule-list/DataSourceRuleListItem.tsx b/public/app/features/alerting/unified/rule-list/DataSourceRuleListItem.tsx index 3fc7eb8a808..3850092b40d 100644 --- a/public/app/features/alerting/unified/rule-list/DataSourceRuleListItem.tsx +++ b/public/app/features/alerting/unified/rule-list/DataSourceRuleListItem.tsx @@ -5,6 +5,7 @@ import { PromRuleType, RulerRuleDTO, RulesSourceApplication } from 'app/types/un import { createReturnTo } from '../hooks/useReturnTo'; import { Annotation } from '../utils/constants'; +import { groups } from '../utils/navigation'; import { fromRule, fromRulerRule, stringifyIdentifier } from '../utils/rule-id'; import { getRuleName, getRulePluginOrigin, rulerRuleType } from '../utils/rules'; import { createRelativeUrl } from '../utils/url'; @@ -46,11 +47,14 @@ export function DataSourceRuleListItem({ const ruleName = rulerRule ? getRuleName(rulerRule) : rule.name; const labels = rulerRule ? rulerRule.labels : rule.labels; + const groupUrl = groups.detailsPageLink(rulesSource.uid, namespace.name, groupName); + const commonProps: RuleListItemCommonProps = { name: ruleName, rulesSource: rulesSource, application: application, group: groupName, + groupUrl, namespace: namespace.name, href, health: rule.health, 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 908e5978d88..3a7348048da 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx @@ -107,6 +107,30 @@ describe('RuleList - FilterView', () => { expect(await screen.findByText(/No matching rules found/)).toBeInTheDocument(); }); + + it('should render group names as clickable links', async () => { + render( + + ); + + await loadMoreResults(); + + const groupLink = await screen.findByRole('link', { + name: 'test-group-4501', + }); + + expect(groupLink).toBeInTheDocument(); + expect(groupLink).toHaveAttribute( + 'href', + '/alerting/mimir/namespaces/test-mimir-namespace/groups/test-group-4501/view' + ); + }); }); async function loadMoreResults() { diff --git a/public/app/features/alerting/unified/rule-list/GrafanaRuleListItem.tsx b/public/app/features/alerting/unified/rule-list/GrafanaRuleListItem.tsx index 3113c991e09..5667069082b 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaRuleListItem.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaRuleListItem.tsx @@ -1,7 +1,8 @@ import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; import { GrafanaPromRuleDTO, PromRuleType } from 'app/types/unified-alerting-dto'; -import { GrafanaRulesSource } from '../utils/datasource'; +import { GRAFANA_RULES_SOURCE_NAME, GrafanaRulesSource } from '../utils/datasource'; +import { groups } from '../utils/navigation'; import { totalFromStats } from '../utils/ruleStats'; import { prometheusRuleType } from '../utils/rules'; import { createRelativeUrl } from '../utils/url'; @@ -32,10 +33,17 @@ export function GrafanaRuleListItem({ }: GrafanaRuleListItemProps) { const { name, uid, labels, provenance } = rule; + const groupUrl = groups.detailsPageLink( + GRAFANA_RULES_SOURCE_NAME, + groupIdentifier.namespace.uid, + groupIdentifier.groupName + ); + const commonProps: RuleListItemCommonProps = { name, rulesSource: GrafanaRulesSource, group: groupIdentifier.groupName, + groupUrl, namespace: namespaceName, href: createRelativeUrl(`/alerting/grafana/${uid}/view`), health: rule?.health, diff --git a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx index 3f8c01831e6..5117a8516bd 100644 --- a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx +++ b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx @@ -4,7 +4,7 @@ import { ReactNode, useEffect, useId } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Alert, Icon, Stack, Text, TextLink, Tooltip, useStyles2 } from '@grafana/ui'; +import { Alert, Stack, Text, TextLink, Tooltip, useStyles2 } from '@grafana/ui'; import { Rule, RuleGroupIdentifierV2, RuleHealth, RulesSourceIdentifier } from 'app/types/unified-alerting'; import { Labels, PromAlertingRuleState, RulerRuleDTO, RulesSourceApplication } from 'app/types/unified-alerting-dto'; @@ -20,8 +20,8 @@ import { createContactPointSearchLink } from '../../utils/misc'; import { RulePluginOrigin } from '../../utils/rules'; import { ListItem } from './ListItem'; -import { DataSourceIcon } from './Namespace'; import { RuleListIcon, RuleOperation } from './RuleListIcon'; +import { RuleLocation } from './RuleLocation'; import { calculateNextEvaluationEstimate } from './util'; export interface AlertRuleListItemProps { @@ -39,6 +39,7 @@ export interface AlertRuleListItemProps { instancesCount?: number; namespace?: string; group?: string; + groupUrl?: string; rulesSource?: RulesSourceIdentifier; application?: RulesSourceApplication; // used for alert rules that use simplified routing @@ -65,6 +66,7 @@ export const AlertRuleListItem = (props: AlertRuleListItemProps) => { instancesCount = 0, namespace, group, + groupUrl, rulesSource, application, contactPoint, @@ -81,7 +83,13 @@ export const AlertRuleListItem = (props: AlertRuleListItemProps) => { if (namespace && group && showLocation) { metadata.push( - + ); } @@ -160,6 +168,7 @@ export function RecordingRuleListItem({ name, namespace, group, + groupUrl, rulesSource, application, href, @@ -175,7 +184,13 @@ export function RecordingRuleListItem({ if (namespace && group && showLocation) { metadata.push( - + ); } @@ -206,6 +221,7 @@ interface RuleOperationListItemProps { name: string; namespace: string; group: string; + groupUrl?: string; rulesSource?: RulesSourceIdentifier; application?: RulesSourceApplication; operation: RuleOperation; @@ -216,6 +232,7 @@ export function RuleOperationListItem({ name, namespace, group, + groupUrl, rulesSource, application, operation, @@ -227,7 +244,13 @@ export function RuleOperationListItem({ if (namespace && group && showLocation) { metadata.push( - + ); } @@ -368,38 +391,6 @@ export const UnknownRuleListItem = ({ ruleName, groupIdentifier, ruleDefinition ); }; -interface RuleLocationProps { - namespace: string; - group: string; - rulesSource?: RulesSourceIdentifier; - application?: RulesSourceApplication; -} - -// @TODO make the datasource / namespace / group click-able to allow further filtering of the list -export const RuleLocation = ({ namespace, group, rulesSource, application }: RuleLocationProps) => { - const isGrafanaApp = application === 'grafana'; - const isDataSourceApp = !!rulesSource && !!application && !isGrafanaApp; - - return ( - - {isGrafanaApp && } - {isDataSourceApp && ( - - - - - - )} - - - {namespace} - - {group} - - - ); -}; - const getStyles = (theme: GrafanaTheme2) => ({ alertListItemContainer: css({ position: 'relative', diff --git a/public/app/features/alerting/unified/rule-list/components/RuleLocation.tsx b/public/app/features/alerting/unified/rule-list/components/RuleLocation.tsx new file mode 100644 index 00000000000..b0d4b31de5d --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/components/RuleLocation.tsx @@ -0,0 +1,43 @@ +import { Icon, Stack, TextLink, Tooltip } from '@grafana/ui'; +import { RulesSourceIdentifier } from 'app/types/unified-alerting'; +import { RulesSourceApplication } from 'app/types/unified-alerting-dto'; + +import { DataSourceIcon } from './Namespace'; + +interface RuleLocationProps { + namespace: string; + group: string; + groupUrl?: string; + rulesSource?: RulesSourceIdentifier; + application?: RulesSourceApplication; +} + +export function RuleLocation({ namespace, group, groupUrl, rulesSource, application }: RuleLocationProps) { + const isGrafanaApp = application === 'grafana'; + const isDataSourceApp = !!rulesSource && !!application && !isGrafanaApp; + + return ( + + {isGrafanaApp && } + {isDataSourceApp && ( + + + + + + )} + + + {namespace} + + {groupUrl ? ( + + {group} + + ) : ( + group + )} + + + ); +} From a3cdfce25a43526fdf6688f701f025bced4559ca Mon Sep 17 00:00:00 2001 From: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:45:59 +0100 Subject: [PATCH 100/126] SecretsManager: Consolidation service and ability to run via cli (#108774) * list all encrypted values and count * separate interfaces * add time filter to global queries * initial secrets consolidation * Revert defaults * More verbose description of the operation * Add consolidation tests and tracing * Fix lint * Revert debug log --- pkg/cmd/grafana-cli/commands/commands.go | 12 + .../secretsconsolidation.go | 13 + .../apis/secret/contracts/encryption.go | 4 + .../apis/secret/service/consolidation.go | 87 ++++++ .../apis/secret/service/consolidation_test.go | 281 ++++++++++++++++++ .../apis/secret/testutils/testutils.go | 11 + pkg/server/runner.go | 38 +-- pkg/server/wire.go | 2 + pkg/server/wire_gen.go | 36 ++- pkg/server/wireexts_oss.go | 2 + 10 files changed, 467 insertions(+), 19 deletions(-) create mode 100644 pkg/cmd/grafana-cli/commands/secretsconsolidation/secretsconsolidation.go create mode 100644 pkg/registry/apis/secret/service/consolidation.go create mode 100644 pkg/registry/apis/secret/service/consolidation_test.go diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index f55209ac1d6..ae7f5f93f67 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -7,6 +7,7 @@ import ( "github.com/urfave/cli/v2" "github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/datamigrations" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/secretsconsolidation" "github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/secretsmigrations" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" @@ -184,6 +185,17 @@ var adminCommands = []*cli.Command{ }, }, }, + { + Name: "secrets-consolidation", + Usage: "Runs an operation that re-encrypts all encrypted values in your database with new data keys", + Subcommands: []*cli.Command{ + { + Name: "consolidate", + Usage: "Re-encrypts all encrypted values with new data keys and deletes the old deactivated data keys. Returns ok unless there is an error. Safe to execute multiple times.", + Action: runRunnerCommand(secretsconsolidation.ConsolidateSecrets), + }, + }, + }, } var Commands = []*cli.Command{ diff --git a/pkg/cmd/grafana-cli/commands/secretsconsolidation/secretsconsolidation.go b/pkg/cmd/grafana-cli/commands/secretsconsolidation/secretsconsolidation.go new file mode 100644 index 00000000000..9be8e5f5a2a --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/secretsconsolidation/secretsconsolidation.go @@ -0,0 +1,13 @@ +package secretsconsolidation + +import ( + "context" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" + "github.com/grafana/grafana/pkg/server" +) + +func ConsolidateSecrets(_ utils.CommandLine, runner server.Runner) error { + err := runner.SecretsConsolidationService.Consolidate(context.Background()) + return err +} diff --git a/pkg/registry/apis/secret/contracts/encryption.go b/pkg/registry/apis/secret/contracts/encryption.go index f0596de9034..f24fe73a544 100644 --- a/pkg/registry/apis/secret/contracts/encryption.go +++ b/pkg/registry/apis/secret/contracts/encryption.go @@ -38,3 +38,7 @@ type GlobalEncryptedValueStorage interface { ListAll(ctx context.Context, opts ListOpts, untilTime *int64) ([]*EncryptedValue, error) CountAll(ctx context.Context, untilTime *int64) (int64, error) } + +type ConsolidationService interface { + Consolidate(ctx context.Context) error +} diff --git a/pkg/registry/apis/secret/service/consolidation.go b/pkg/registry/apis/secret/service/consolidation.go new file mode 100644 index 00000000000..b0baea659c4 --- /dev/null +++ b/pkg/registry/apis/secret/service/consolidation.go @@ -0,0 +1,87 @@ +package service + +import ( + "context" + "fmt" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + otelcodes "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +type ConsolidationService struct { + tracer trace.Tracer + globalDataKeyStore contracts.GlobalDataKeyStorage + encryptedValueStore contracts.EncryptedValueStorage + globalEncryptedValueStore contracts.GlobalEncryptedValueStorage + encryptionManager contracts.EncryptionManager +} + +func ProvideConsolidationService( + tracer trace.Tracer, + globalDataKeyStore contracts.GlobalDataKeyStorage, + encryptedValueStore contracts.EncryptedValueStorage, + globalEncryptedValueStore contracts.GlobalEncryptedValueStorage, + encryptionManager contracts.EncryptionManager, +) contracts.ConsolidationService { + return &ConsolidationService{ + tracer: tracer, + globalDataKeyStore: globalDataKeyStore, + encryptedValueStore: encryptedValueStore, + globalEncryptedValueStore: globalEncryptedValueStore, + encryptionManager: encryptionManager, + } +} + +func (s *ConsolidationService) Consolidate(ctx context.Context) (err error) { + ctx, span := s.tracer.Start(ctx, "ConsolidationService.Consolidate") + defer span.End() + + defer func() { + if err != nil { + span.SetStatus(otelcodes.Error, err.Error()) + span.RecordError(err) + } + }() + + // Disable all active data keys. + // This will ensure that no new data can be encrypted with the old keys. + err = s.globalDataKeyStore.DisableAllDataKeys(ctx) + if err != nil { + return fmt.Errorf("disabling all data keys: %w", err) + } + + // List all encrypted values. + encryptedValues, err := s.globalEncryptedValueStore.ListAll(ctx, contracts.ListOpts{}, nil) + if err != nil { + return fmt.Errorf("listing all encrypted values: %w", err) + } + + for _, ev := range encryptedValues { + // Decrypt the value using its old data key. + decryptedValue, err := s.encryptionManager.Decrypt(ctx, ev.Namespace, ev.EncryptedData) + if err != nil { + logging.FromContext(ctx).Error("Failed to decrypt value", "namespace", ev.Namespace, "name", ev.Name, "error", err) + continue + } + + // Re-encrypt the value using a new data key. + reEncryptedValue, err := s.encryptionManager.Encrypt(ctx, ev.Namespace, decryptedValue) + if err != nil { + logging.FromContext(ctx).Error("Failed to re-encrypt value", "namespace", ev.Namespace, "name", ev.Name, "error", err) + continue + } + + // Update the encrypted value in the store. + err = s.encryptedValueStore.Update(ctx, ev.Namespace, ev.Name, ev.Version, reEncryptedValue) + if err != nil { + logging.FromContext(ctx).Error("Failed to update encrypted value", "namespace", ev.Namespace, "name", ev.Name, "error", err) + continue + } + } + + // TODO: After all values are re-encrypted, we can safely remove the old data keys. + + return nil +} diff --git a/pkg/registry/apis/secret/service/consolidation_test.go b/pkg/registry/apis/secret/service/consolidation_test.go new file mode 100644 index 00000000000..57ac94253c9 --- /dev/null +++ b/pkg/registry/apis/secret/service/consolidation_test.go @@ -0,0 +1,281 @@ +package service_test + +import ( + "context" + "testing" + + "github.com/grafana/authlib/authn" + "github.com/grafana/authlib/types" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +// mockGlobalEncryptedValueStorage wraps the real storage and allows injecting behavior during ListAll +type mockGlobalEncryptedValueStorage struct { + real contracts.GlobalEncryptedValueStorage + sut *testutils.Sut + ctx context.Context + onListAll func() +} + +func (m *mockGlobalEncryptedValueStorage) ListAll(ctx context.Context, opts contracts.ListOpts, untilTime *int64) ([]*contracts.EncryptedValue, error) { + if m.onListAll != nil { + m.onListAll() + } + return m.real.ListAll(ctx, opts, untilTime) +} + +func (m *mockGlobalEncryptedValueStorage) CountAll(ctx context.Context, untilTime *int64) (int64, error) { + return m.real.CountAll(ctx, untilTime) +} + +func TestConsolidation(t *testing.T) { + t.Parallel() + + t.Run("consolidation re-encrypts values but preserves decrypted content", func(t *testing.T) { + t.Parallel() + sut := testutils.Setup(t) + + ctx := context.Background() + createAuthContext := func(ctx context.Context, namespace string, identityType types.IdentityType) context.Context { + return types.WithAuthInfo(ctx, &identity.StaticRequester{ + Type: identityType, + Namespace: namespace, + AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{ + Rest: authn.AccessTokenClaims{ + Permissions: []string{"secret.grafana.app/securevalues:decrypt"}, + ServiceIdentity: "decrypter1", + }, + }, + }) + } + + // Create several secure values in different namespaces + testCases := []struct { + name string + namespace string + value string + }{ + {"test-secret-1", "namespace1", "test-value-1"}, + {"test-secret-2", "namespace1", "test-value-2"}, + {"test-secret-3", "namespace2", "test-value-3"}, + {"test-secret-4", "namespace2", "test-value-4"}, + } + + var originalDecryptedValues []string + var originalEncryptedData [][]byte + + // Create secure values and store their original decrypted values and encrypted data + for _, tc := range testCases { + sv := &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.name, + Namespace: tc.namespace, + }, + Spec: secretv1beta1.SecureValueSpec{ + Value: ptr.To(secretv1beta1.NewExposedSecureValue(tc.value)), + Decrypters: []string{"decrypter1"}, + }, + } + + createdSv, err := sut.CreateSv(ctx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + require.NotNil(t, createdSv) + + // Store the original decrypted data and encrypted data + authCtx := createAuthContext(ctx, tc.namespace, types.TypeAccessPolicy) + decryptedValue, err := sut.DecryptStorage.Decrypt(authCtx, xkube.Namespace(tc.namespace), tc.name) + require.NoError(t, err) + originalDecryptedValues = append(originalDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) + + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + require.NoError(t, err) + require.NotNil(t, encryptedValue) + originalEncryptedData = append(originalEncryptedData, encryptedValue.EncryptedData) + } + + // Run consolidation + err := sut.ConsolidationService.Consolidate(ctx) + require.NoError(t, err) + + for i, tc := range testCases { + // Verify that the decrypted values are still the same + authCtx := createAuthContext(ctx, tc.namespace, types.TypeAccessPolicy) + decryptedValue, err := sut.DecryptStorage.Decrypt(authCtx, xkube.Namespace(tc.namespace), tc.name) + require.NoError(t, err) + require.Equal(t, originalDecryptedValues[i], decryptedValue.DangerouslyExposeAndConsumeValue()) + + // Verify that the encrypted data has changed (indicating re-encryption) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + require.NoError(t, err) + require.NotEqual(t, originalEncryptedData[i], encryptedValue.EncryptedData) + } + }) + + t.Run("consolidation handles secrets created during the process", func(t *testing.T) { + t.Parallel() + sut := testutils.Setup(t) + + ctx := context.Background() + createAuthContext := func(ctx context.Context, namespace string, identityType types.IdentityType) context.Context { + return types.WithAuthInfo(ctx, &identity.StaticRequester{ + Type: identityType, + Namespace: namespace, + AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{ + Rest: authn.AccessTokenClaims{ + Permissions: []string{"secret.grafana.app/securevalues:decrypt"}, + ServiceIdentity: "decrypter1", + }, + }, + }) + } + + // Create initial secure values + initialSecrets := []struct { + name string + namespace string + value string + }{ + {"initial-secret-1", "namespace1", "initial-value-1"}, + {"initial-secret-2", "namespace2", "initial-value-2"}, + } + + var initialDecryptedValues []string + var initialEncryptedData [][]byte + + for _, tc := range initialSecrets { + sv := &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.name, + Namespace: tc.namespace, + }, + Spec: secretv1beta1.SecureValueSpec{ + Value: ptr.To(secretv1beta1.NewExposedSecureValue(tc.value)), + Decrypters: []string{"decrypter1"}, + }, + } + + _, err := sut.CreateSv(ctx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + + // Store original decrypted values and encrypted data + authCtx := createAuthContext(ctx, tc.namespace, types.TypeAccessPolicy) + decryptedValue, err := sut.DecryptStorage.Decrypt(authCtx, xkube.Namespace(tc.namespace), tc.name) + require.NoError(t, err) + initialDecryptedValues = append(initialDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) + + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + require.NoError(t, err) + initialEncryptedData = append(initialEncryptedData, encryptedValue.EncryptedData) + } + + // Secrets to be created during consolidation (after data keys are disabled) + var newSecretDecryptedValues []string + var newSecretEncryptedData [][]byte + + // Create a mock GlobalEncryptedValueStorage that will create new secrets when ListAll is called + mockStorage := &mockGlobalEncryptedValueStorage{ + real: sut.GlobalEncryptedValueStorage, + sut: &sut, + ctx: ctx, + onListAll: func() { + // This function is called during consolidation, after data keys are disabled + // but before the re-encryption loop begins + newSecrets := []struct { + name string + namespace string + value string + desc string + }{ + {"new-secret-1", "namespace1", "new-value-1", "New secret created during consolidation"}, + {"new-secret-2", "namespace3", "new-value-2", "Another new secret during consolidation"}, + } + + for _, tc := range newSecrets { + sv := &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.name, + Namespace: tc.namespace, + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: tc.desc, + Value: ptr.To(secretv1beta1.NewExposedSecureValue(tc.value)), + Decrypters: []string{"decrypter1"}, + }, + } + + _, err := sut.CreateSv(ctx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + + // Store their decrypted values and original encrypted data + authCtx := createAuthContext(ctx, tc.namespace, types.TypeAccessPolicy) + decryptedValue, err := sut.DecryptStorage.Decrypt(authCtx, xkube.Namespace(tc.namespace), tc.name) + require.NoError(t, err) + newSecretDecryptedValues = append(newSecretDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) + + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + require.NoError(t, err) + newSecretEncryptedData = append(newSecretEncryptedData, encryptedValue.EncryptedData) + } + }, + } + + // Create a custom consolidation service that uses the mocked storage + tracer := noop.NewTracerProvider().Tracer("test") + customConsolidationService := service.ProvideConsolidationService( + tracer, + sut.GlobalDataKeyStore, + sut.EncryptedValueStorage, + mockStorage, + sut.EncryptionManager, + ) + + // Run consolidation + err := customConsolidationService.Consolidate(ctx) + require.NoError(t, err) + + for i, tc := range initialSecrets { + // Verify that all initial secrets still decrypt to the same values + authCtx := createAuthContext(ctx, tc.namespace, types.TypeAccessPolicy) + decryptedValue, err := sut.DecryptStorage.Decrypt(authCtx, xkube.Namespace(tc.namespace), tc.name) + require.NoError(t, err) + require.Equal(t, initialDecryptedValues[i], decryptedValue.DangerouslyExposeAndConsumeValue()) + + // Verify that the encrypted data has changed (indicating re-encryption) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + require.NoError(t, err) + require.NotEqual(t, initialEncryptedData[i], encryptedValue.EncryptedData) + } + + // Verify that the new secrets (created during consolidation) also decrypt correctly + // These secrets should have been re-encrypted as well during the consolidation process + newSecrets := []struct { + name string + namespace string + }{ + {"new-secret-1", "namespace1"}, + {"new-secret-2", "namespace3"}, + } + + for i, tc := range newSecrets { + authCtx := createAuthContext(ctx, tc.namespace, types.TypeAccessPolicy) + decryptedValue, err := sut.DecryptStorage.Decrypt(authCtx, xkube.Namespace(tc.namespace), tc.name) + require.NoError(t, err) + require.Equal(t, newSecretDecryptedValues[i], decryptedValue.DangerouslyExposeAndConsumeValue()) + + // Verify that the encrypted data has changed from what it was when first created + // (indicating it was re-encrypted during consolidation) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + require.NoError(t, err) + require.NotEqual(t, newSecretEncryptedData[i], encryptedValue.EncryptedData) + } + }) +} diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 8b6765ba642..ebe8b109b81 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -87,6 +87,9 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { store, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, nil) require.NoError(t, err) + globalDataKeyStore, err := encryptionstorage.ProvideGlobalDataKeyStorage(database, tracer, nil) + require.NoError(t, err) + usageStats := &usagestats.UsageStatsMock{T: t} enc, err := cipher.ProvideAESGCMCipherService(tracer, usageStats) @@ -135,6 +138,8 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { decryptService, err := decrypt.ProvideDecryptService(testCfg, tracer, decryptStorage) require.NoError(t, err) + consolidationService := service.ProvideConsolidationService(tracer, globalDataKeyStore, encryptedValueStorage, globalEncryptedValueStorage, encryptionManager) + return Sut{ SecureValueService: secureValueService, SecureValueMetadataStorage: secureValueMetadataStorage, @@ -145,6 +150,9 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { SQLKeeper: sqlKeeper, Database: database, AccessClient: accessClient, + ConsolidationService: consolidationService, + EncryptionManager: encryptionManager, + GlobalDataKeyStore: globalDataKeyStore, } } @@ -158,6 +166,9 @@ type Sut struct { SQLKeeper *sqlkeeper.SQLKeeper Database *database.Database AccessClient types.AccessClient + ConsolidationService contracts.ConsolidationService + EncryptionManager contracts.EncryptionManager + GlobalDataKeyStore contracts.GlobalDataKeyStorage } type CreateSvConfig struct { diff --git a/pkg/server/runner.go b/pkg/server/runner.go index ae9ccf28469..cc31af9711b 100644 --- a/pkg/server/runner.go +++ b/pkg/server/runner.go @@ -8,32 +8,36 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" ) type Runner struct { - Cfg *setting.Cfg - SQLStore db.DB - SettingsProvider setting.Provider - Features featuremgmt.FeatureToggles - EncryptionService encryption.Internal - SecretsService *manager.SecretsService - SecretsMigrator secrets.Migrator - UserService user.Service + Cfg *setting.Cfg + SQLStore db.DB + SettingsProvider setting.Provider + Features featuremgmt.FeatureToggles + EncryptionService encryption.Internal + SecretsService *manager.SecretsService + SecretsMigrator secrets.Migrator + UserService user.Service + SecretsConsolidationService contracts.ConsolidationService } func NewRunner(cfg *setting.Cfg, sqlStore db.DB, settingsProvider setting.Provider, encryptionService encryption.Internal, features featuremgmt.FeatureToggles, secretsService *manager.SecretsService, secretsMigrator secrets.Migrator, - userService user.Service, + userService user.Service, secretsConsolidationService contracts.ConsolidationService, ) Runner { return Runner{ - Cfg: cfg, - SQLStore: sqlStore, - SettingsProvider: settingsProvider, - EncryptionService: encryptionService, - SecretsService: secretsService, - SecretsMigrator: secretsMigrator, - Features: features, - UserService: userService, + Cfg: cfg, + SQLStore: sqlStore, + SettingsProvider: settingsProvider, + EncryptionService: encryptionService, + SecretsService: secretsService, + SecretsMigrator: secretsMigrator, + Features: features, + UserService: userService, + SecretsConsolidationService: secretsConsolidationService, } } diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 741ec53ede8..a56c1bff1a6 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -429,7 +429,9 @@ var wireBasicSet = wire.NewSet( secretdecrypt.ProvideDecryptAuthorizer, secretdecrypt.ProvideDecryptService, secretencryption.ProvideDataKeyStorage, + secretencryption.ProvideGlobalDataKeyStorage, secretencryption.ProvideEncryptedValueStorage, + secretencryption.ProvideGlobalEncryptedValueStorage, secretsecurevalueservice.ProvideSecureValueService, secretvalidator.ProvideKeeperValidator, secretvalidator.ProvideSecureValueValidator, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 3d055c451e5..cf76c869512 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -1453,7 +1453,39 @@ func InitializeForCLI(cfg *setting.Cfg) (Runner, error) { if err != nil { return Runner{}, err } - runner := NewRunner(cfg, sqlStore, ossImpl, serviceService, featureToggles, secretsService, secretsMigrator, userService) + tracer := otelTracer() + databaseDatabase := database5.ProvideDatabase(sqlStore, tracer) + registerer := metrics.ProvideRegisterer() + globalDataKeyStorage, err := encryption.ProvideGlobalDataKeyStorage(databaseDatabase, tracer, registerer) + if err != nil { + return Runner{}, err + } + encryptedValueStorage, err := encryption.ProvideEncryptedValueStorage(databaseDatabase, tracer) + if err != nil { + return Runner{}, err + } + globalEncryptedValueStorage, err := encryption.ProvideGlobalEncryptedValueStorage(databaseDatabase, tracer) + if err != nil { + return Runner{}, err + } + dataKeyStorage, err := encryption.ProvideDataKeyStorage(databaseDatabase, tracer, registerer) + if err != nil { + return Runner{}, err + } + cipher, err := service11.ProvideAESGCMCipherService(tracer, usageStats) + if err != nil { + return Runner{}, err + } + providerConfig, err := kmsproviders.ProvideOSSKMSProviders(cfg, cipher) + if err != nil { + return Runner{}, err + } + encryptionManager, err := manager4.ProvideEncryptionManager(tracer, dataKeyStorage, usageStats, cipher, providerConfig) + if err != nil { + return Runner{}, err + } + consolidationService := service12.ProvideConsolidationService(tracer, globalDataKeyStorage, encryptedValueStorage, globalEncryptedValueStorage, encryptionManager) + runner := NewRunner(cfg, sqlStore, ossImpl, serviceService, featureToggles, secretsService, secretsMigrator, userService, consolidationService) return runner, nil } @@ -1540,7 +1572,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets2.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets2.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), mtdsclient.NewNullMTDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptService, encryption.ProvideDataKeyStorage, encryption.ProvideEncryptedValueStorage, service12.ProvideSecureValueService, validator3.ProvideKeeperValidator, validator3.ProvideSecureValueValidator, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), manager4.ProvideEncryptionManager, service11.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets2.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets2.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), mtdsclient.NewNullMTDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, service12.ProvideSecureValueService, validator3.ProvideKeeperValidator, validator3.ProvideSecureValueValidator, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), manager4.ProvideEncryptionManager, service11.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index bb715ed7dea..61a05716b47 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" gsmKMSProviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper" + secretService "github.com/grafana/grafana/pkg/registry/apis/secret/service" "github.com/grafana/grafana/pkg/registry/backgroundsvcs" "github.com/grafana/grafana/pkg/registry/usagestatssvcs" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -108,6 +109,7 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(kmsproviders.Service), new(osskmsproviders.Service)), secretkeeper.ProvideService, wire.Bind(new(contracts.KeeperService), new(*secretkeeper.OSSKeeperService)), + secretService.ProvideConsolidationService, ldap.ProvideGroupsService, wire.Bind(new(ldap.Groups), new(*ldap.OSSGroups)), guardian.ProvideGuardian, From f3cb8aa903250c02e703f31bbb856f27219fe3e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:07:55 +0100 Subject: [PATCH 101/126] Update swc monorepo (#108981) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +- packages/grafana-plugin-configs/package.json | 2 +- packages/grafana-test-utils/package.json | 2 +- yarn.lock | 222 +++++-------------- 4 files changed, 57 insertions(+), 173 deletions(-) diff --git a/package.json b/package.json index 169c2f41f6c..cbc51156684 100644 --- a/package.json +++ b/package.json @@ -105,8 +105,8 @@ "@rtk-query/codegen-openapi": "^2.0.0", "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", "@stylistic/eslint-plugin-ts": "^3.0.0", - "@swc/core": "1.10.12", - "@swc/helpers": "0.5.15", + "@swc/core": "1.13.3", + "@swc/helpers": "0.5.17", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.3.0", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index 42233bea781..a2edd65dd30 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -9,7 +9,7 @@ "type": "module", "devDependencies": { "@grafana/tsconfig": "^2.0.0", - "@swc/core": "1.10.12", + "@swc/core": "1.13.3", "@swc/helpers": "^0.5.0", "@swc/jest": "^0.2.26", "@types/eslint": "9.6.1", diff --git a/packages/grafana-test-utils/package.json b/packages/grafana-test-utils/package.json index c20fad715d2..fe04697966f 100644 --- a/packages/grafana-test-utils/package.json +++ b/packages/grafana-test-utils/package.json @@ -57,7 +57,7 @@ }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", - "@swc/core": "1.10.12", + "@swc/core": "1.13.3", "@swc/jest": "^0.2.26", "@types/jest": "29.5.14", "@types/node": "22.17.0", diff --git a/yarn.lock b/yarn.lock index c031af16035..c6ee7419101 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3421,7 +3421,7 @@ __metadata: resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs" dependencies: "@grafana/tsconfig": "npm:^2.0.0" - "@swc/core": "npm:1.10.12" + "@swc/core": "npm:1.13.3" "@swc/helpers": "npm:^0.5.0" "@swc/jest": "npm:^0.2.26" "@types/eslint": "npm:9.6.1" @@ -3715,7 +3715,7 @@ __metadata: resolution: "@grafana/test-utils@workspace:packages/grafana-test-utils" dependencies: "@grafana/tsconfig": "npm:^2.0.0" - "@swc/core": "npm:1.10.12" + "@swc/core": "npm:1.13.3" "@swc/jest": "npm:^0.2.26" "@types/jest": "npm:29.5.14" "@types/node": "npm:22.17.0" @@ -8447,206 +8447,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.10.12": - version: 1.10.12 - resolution: "@swc/core-darwin-arm64@npm:1.10.12" +"@swc/core-darwin-arm64@npm:1.13.3": + version: 1.13.3 + resolution: "@swc/core-darwin-arm64@npm:1.13.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.13.2": - version: 1.13.2 - resolution: "@swc/core-darwin-arm64@npm:1.13.2" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@swc/core-darwin-x64@npm:1.10.12": - version: 1.10.12 - resolution: "@swc/core-darwin-x64@npm:1.10.12" +"@swc/core-darwin-x64@npm:1.13.3": + version: 1.13.3 + resolution: "@swc/core-darwin-x64@npm:1.13.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.13.2": - version: 1.13.2 - resolution: "@swc/core-darwin-x64@npm:1.13.2" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@swc/core-linux-arm-gnueabihf@npm:1.10.12": - version: 1.10.12 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.10.12" +"@swc/core-linux-arm-gnueabihf@npm:1.13.3": + version: 1.13.3 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.3" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.13.2": - version: 1.13.2 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.2" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@swc/core-linux-arm64-gnu@npm:1.10.12": - version: 1.10.12 - resolution: "@swc/core-linux-arm64-gnu@npm:1.10.12" +"@swc/core-linux-arm64-gnu@npm:1.13.3": + version: 1.13.3 + resolution: "@swc/core-linux-arm64-gnu@npm:1.13.3" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.13.2": - version: 1.13.2 - resolution: "@swc/core-linux-arm64-gnu@npm:1.13.2" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@swc/core-linux-arm64-musl@npm:1.10.12": - version: 1.10.12 - resolution: "@swc/core-linux-arm64-musl@npm:1.10.12" +"@swc/core-linux-arm64-musl@npm:1.13.3": + version: 1.13.3 + resolution: "@swc/core-linux-arm64-musl@npm:1.13.3" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.13.2": - version: 1.13.2 - resolution: "@swc/core-linux-arm64-musl@npm:1.13.2" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@swc/core-linux-x64-gnu@npm:1.10.12": - version: 1.10.12 - resolution: "@swc/core-linux-x64-gnu@npm:1.10.12" +"@swc/core-linux-x64-gnu@npm:1.13.3": + version: 1.13.3 + resolution: "@swc/core-linux-x64-gnu@npm:1.13.3" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.13.2": - version: 1.13.2 - resolution: "@swc/core-linux-x64-gnu@npm:1.13.2" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@swc/core-linux-x64-musl@npm:1.10.12": - version: 1.10.12 - resolution: "@swc/core-linux-x64-musl@npm:1.10.12" +"@swc/core-linux-x64-musl@npm:1.13.3": + version: 1.13.3 + resolution: "@swc/core-linux-x64-musl@npm:1.13.3" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.13.2": - version: 1.13.2 - resolution: "@swc/core-linux-x64-musl@npm:1.13.2" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@swc/core-win32-arm64-msvc@npm:1.10.12": - version: 1.10.12 - resolution: "@swc/core-win32-arm64-msvc@npm:1.10.12" +"@swc/core-win32-arm64-msvc@npm:1.13.3": + version: 1.13.3 + resolution: "@swc/core-win32-arm64-msvc@npm:1.13.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.13.2": - version: 1.13.2 - resolution: "@swc/core-win32-arm64-msvc@npm:1.13.2" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@swc/core-win32-ia32-msvc@npm:1.10.12": - version: 1.10.12 - resolution: "@swc/core-win32-ia32-msvc@npm:1.10.12" +"@swc/core-win32-ia32-msvc@npm:1.13.3": + version: 1.13.3 + resolution: "@swc/core-win32-ia32-msvc@npm:1.13.3" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.13.2": - version: 1.13.2 - resolution: "@swc/core-win32-ia32-msvc@npm:1.13.2" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"@swc/core-win32-x64-msvc@npm:1.10.12": - version: 1.10.12 - resolution: "@swc/core-win32-x64-msvc@npm:1.10.12" +"@swc/core-win32-x64-msvc@npm:1.13.3": + version: 1.13.3 + resolution: "@swc/core-win32-x64-msvc@npm:1.13.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.13.2": - version: 1.13.2 - resolution: "@swc/core-win32-x64-msvc@npm:1.13.2" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@swc/core@npm:1.10.12": - version: 1.10.12 - resolution: "@swc/core@npm:1.10.12" +"@swc/core@npm:1.13.3, @swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": + version: 1.13.3 + resolution: "@swc/core@npm:1.13.3" dependencies: - "@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: - "@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/62b77009f267f28a486ded1de9ac250b65dc618a4c551a86ad8e438ee54d96b0d44f52332fb242eaa9a6e00db499436753ef99747e6c1a14bd6894554c10e799 - languageName: node - linkType: hard - -"@swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": - version: 1.13.2 - resolution: "@swc/core@npm:1.13.2" - dependencies: - "@swc/core-darwin-arm64": "npm:1.13.2" - "@swc/core-darwin-x64": "npm:1.13.2" - "@swc/core-linux-arm-gnueabihf": "npm:1.13.2" - "@swc/core-linux-arm64-gnu": "npm:1.13.2" - "@swc/core-linux-arm64-musl": "npm:1.13.2" - "@swc/core-linux-x64-gnu": "npm:1.13.2" - "@swc/core-linux-x64-musl": "npm:1.13.2" - "@swc/core-win32-arm64-msvc": "npm:1.13.2" - "@swc/core-win32-ia32-msvc": "npm:1.13.2" - "@swc/core-win32-x64-msvc": "npm:1.13.2" + "@swc/core-darwin-arm64": "npm:1.13.3" + "@swc/core-darwin-x64": "npm:1.13.3" + "@swc/core-linux-arm-gnueabihf": "npm:1.13.3" + "@swc/core-linux-arm64-gnu": "npm:1.13.3" + "@swc/core-linux-arm64-musl": "npm:1.13.3" + "@swc/core-linux-x64-gnu": "npm:1.13.3" + "@swc/core-linux-x64-musl": "npm:1.13.3" + "@swc/core-win32-arm64-msvc": "npm:1.13.3" + "@swc/core-win32-ia32-msvc": "npm:1.13.3" + "@swc/core-win32-x64-msvc": "npm:1.13.3" "@swc/counter": "npm:^0.1.3" "@swc/types": "npm:^0.1.23" peerDependencies: @@ -8675,7 +8559,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10/ca4d05c44c3e36f7d3f68dda9b53eb1e7e1c9f07701d526c8484605bf4a744cbcb83e1cadb0b4b57723d98ba799d3a75508b2219751ad5fdb29491ca325c680d + checksum: 10/d9f933b69b6872b69a40a926eafd98a6088e545027e031f32717f7112ce39c58ba6714a44cd894b31a619a2999b94756582735eb3ec69a35ff434da2b537e868 languageName: node linkType: hard @@ -8686,12 +8570,12 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.5.15, @swc/helpers@npm:^0.5.0": - version: 0.5.15 - resolution: "@swc/helpers@npm:0.5.15" +"@swc/helpers@npm:0.5.17, @swc/helpers@npm:^0.5.0": + version: 0.5.17 + resolution: "@swc/helpers@npm:0.5.17" dependencies: tslib: "npm:^2.8.0" - checksum: 10/e3f32c6deeecfb0fa3f22edff03a7b358e7ce16d27b0f1c8b5cdc3042c5c4ce4da6eac0b781ab7cc4f54696ece657467d56734fb26883439fb00017385364c4c + checksum: 10/1fc8312a78f1f99c8ec838585445e99763eeebff2356100738cdfdb8ad47d2d38df678ee6edd93a90fe319ac52da67adc14ac00eb82b606c5fb8ebc5d06ec2a2 languageName: node linkType: hard @@ -8708,7 +8592,7 @@ __metadata: languageName: node linkType: hard -"@swc/types@npm:^0.1.17, @swc/types@npm:^0.1.23": +"@swc/types@npm:^0.1.23": version: 0.1.23 resolution: "@swc/types@npm:0.1.23" dependencies: @@ -18353,8 +18237,8 @@ __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:^3.0.0" - "@swc/core": "npm:1.10.12" - "@swc/helpers": "npm:0.5.15" + "@swc/core": "npm:1.13.3" + "@swc/helpers": "npm:0.5.17" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.3.0" From 7d8c2fbb82051a2b2ef10b69e29fd881e683e19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Thu, 31 Jul 2025 16:18:53 +0200 Subject: [PATCH 102/126] TraceView: Refine UI visual hierarchy inside details section (#108929) * TraceView: Span details content redesign * Unify displaying various sections in span details * Revert i18n key change * Update tests --- .../SpanDetail/AccordianKeyValues.test.tsx | 2 +- .../SpanDetail/AccordianKeyValues.tsx | 20 +++- .../SpanDetail/AccordianLogs.test.tsx | 4 +- .../SpanDetail/AccordianLogs.tsx | 2 - .../SpanDetail/AccordianReferences.test.tsx | 4 +- .../SpanDetail/AccordianReferences.tsx | 8 +- .../SpanDetail/AccordianText.test.tsx | 49 ---------- .../SpanDetail/AccordianText.tsx | 97 ------------------- .../SpanDetail/KeyValuesTable.tsx | 36 ++++--- .../SpanDetail/TextList.tsx | 5 +- .../TraceTimelineViewer/SpanDetail/index.tsx | 65 ++++++------- .../components/TraceTimelineViewer/index.tsx | 1 - .../TraceView/components/common/CopyIcon.tsx | 1 - .../grafana-testdata-datasource/datasource.ts | 48 +++++++++ 14 files changed, 128 insertions(+), 214 deletions(-) delete mode 100644 public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.test.tsx delete mode 100644 public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx index 597b6d125c6..908126b649b 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx @@ -96,7 +96,7 @@ describe('AccordianKeyValues test', () => { it('renders the summary instead of the table when it is not expanded', () => { setupAccordian({ isOpen: false } as AccordianKeyValuesProps); - expect(screen.getByRole('switch', { name: 'test accordian: span.kind client omg mos-def' })).toBeInTheDocument(); + expect(screen.getByRole('switch', { name: 'test accordian span.kind client omg mos-def' })).toBeInTheDocument(); expect(screen.queryByRole('table')).not.toBeInTheDocument(); expect(screen.queryAllByRole('cell')).toHaveLength(0); }); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx index d8d621cf218..93822458ac7 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx @@ -17,7 +17,7 @@ import cx from 'classnames'; import * as React from 'react'; import { GrafanaTheme2, TraceKeyValuePair } from '@grafana/data'; -import { Icon, useStyles2 } from '@grafana/ui'; +import { Counter, Icon, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; import TNil from '../../types/TNil'; @@ -43,6 +43,10 @@ export const getStyles = (theme: GrafanaTheme2) => { background: autoColor(theme, '#e8e8e8'), }, }), + headerLabel: css({ + width: '120px', + display: 'inline-block', + }), headerEmpty: css({ label: 'headerEmpty', background: 'none', @@ -87,6 +91,9 @@ export type AccordianKeyValuesProps = { logName?: string; highContrast?: boolean; interactive?: boolean; + onlyValues?: boolean; + showSummary?: boolean; + showCountBadge?: boolean; isOpen: boolean; label: string | React.ReactNode; linksGetter?: ((pairs: TraceKeyValuePair[], index: number) => KeyValuesTableLink[]) | TNil; @@ -127,6 +134,9 @@ export default function AccordianKeyValues({ isOpen, label, linksGetter, + onlyValues = false, + showSummary = true, + showCountBadge = false, onToggle = null, }: AccordianKeyValuesProps) { const isEmpty = (!Array.isArray(data) || !data.length) && !logName; @@ -148,7 +158,7 @@ export default function AccordianKeyValues({ }; } - const showDataSummaryFields = data.length > 0 && !isOpen; + const showDataSummaryFields = showSummary && data.length > 0 && !isOpen; return (
    @@ -161,9 +171,9 @@ export default function AccordianKeyValues({ data-testid="AccordianKeyValues--header" > {arrow} - + {label} - {showDataSummaryFields && ':'} + {showCountBadge ? : null} {showDataSummaryFields && ( @@ -171,7 +181,7 @@ export default function AccordianKeyValues({ )}
    - {isOpen && } + {isOpen && }
    ); } diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx index 18085f3fdcb..19ff85b2dd7 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx @@ -103,10 +103,10 @@ describe('AccordianLogs tests', () => { setup({ isOpen: true, openedItems: new Set() } as AccordianLogsProps); expect( screen.getByRole('switch', { - name: '15μs (foo event name) : message oh the next log message more stuff', + name: '15μs (foo event name) message oh the next log message more stuff', }) ).toBeInTheDocument(); - expect(screen.getByRole('switch', { name: '5μs: message oh the log message something else' })).toBeInTheDocument(); + expect(screen.getByRole('switch', { name: '5μs message oh the log message something else' })).toBeInTheDocument(); }); it('renders event name and duration when events list is open', () => { diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx index 5584760b04b..b708b7f3ec8 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx @@ -32,14 +32,12 @@ const getStyles = (theme: GrafanaTheme2) => { AccordianLogs: css({ label: 'AccordianLogs', position: 'relative', - marginBottom: '0.25rem', }), AccordianLogsHeader: css({ label: 'AccordianLogsHeader', color: 'inherit', display: 'flex', alignItems: 'center', - padding: '0.25rem 0.1em', '&:hover': { background: autoColor(theme, '#e8e8e8'), }, diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx index d7296055bfc..14fa5187396 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx @@ -75,7 +75,7 @@ describe('AccordianReferences tests', () => { it('renders the correct number of references', () => { setup(); - expect(screen.getByRole('switch', { name: 'References (3)' })).toBeInTheDocument(); + expect(screen.getByRole('switch', { name: 'References 3' })).toBeInTheDocument(); }); it('content doesnt show when not expanded', () => { @@ -88,7 +88,7 @@ describe('AccordianReferences tests', () => { it('renders the content when it is expanded', () => { setup({ isOpen: true } as AccordianReferencesProps); - expect(screen.getByRole('switch', { name: 'References (3)' })).toBeInTheDocument(); + expect(screen.getByRole('switch', { name: 'References 3' })).toBeInTheDocument(); expect(screen.getAllByRole('link', { name: /^service\d\sop\d/ })).toHaveLength(2); expect(screen.getByRole('link', { name: /^View\sLinked/ })).toBeInTheDocument(); }); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx index de60443707e..10c31897ec6 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx @@ -17,7 +17,7 @@ import * as React from 'react'; import { Field, GrafanaTheme2, LinkModel } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Icon, useStyles2 } from '@grafana/ui'; +import { Counter, Icon, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; import { TraceSpanReference } from '../../types/trace'; @@ -36,16 +36,14 @@ const getStyles = (theme: GrafanaTheme2) => ({ }), AccordianReferences: css({ label: 'AccordianReferences', - border: `1px solid ${autoColor(theme, '#d8d8d8')}`, position: 'relative', marginBottom: '0.25rem', }), AccordianReferencesHeader: css({ label: 'AccordianReferencesHeader', - background: autoColor(theme, '#e4e4e4'), color: 'inherit', display: 'block', - padding: '0.25rem 0.5rem', + padding: '0.25rem 0', '&:hover': { background: autoColor(theme, '#dadada'), }, @@ -223,7 +221,7 @@ const AccordianReferences = ({ References {' '} - ({data.length}) + {isOpen && ( ', () => { - const props = { - compact: false, - data: warnings, - highContrast: false, - isOpen: false, - label: 'le-label', - onToggle: jest.fn(), - }; - - it('renders without exploding', () => { - render(); - expect(() => render()).not.toThrow(); - }); - - it('renders the label', () => { - render(); - const { getByText } = within(screen.getByTestId('AccordianText--header')); - expect(getByText(props.label)).toBeInTheDocument(); - }); - - it('renders the content when it is expanded', () => { - props.isOpen = true; - render(); - warnings.forEach((warning) => { - expect(screen.getByText(warning)).toBeInTheDocument(); - }); - }); -}); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx deleted file mode 100644 index f0efc64b10a..00000000000 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2019 Uber Technologies, Inc. -// -// 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. - -import { css } from '@emotion/css'; -import cx from 'classnames'; -import * as React from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Icon, useStyles2 } from '@grafana/ui'; - -import { autoColor } from '../../Theme'; -import TNil from '../../types/TNil'; - -import { getStyles as getAccordianKeyValuesStyles } from './AccordianKeyValues'; -import TextList from './TextList'; - -import { alignIcon } from '.'; - -const getStyles = (theme: GrafanaTheme2) => ({ - header: css({ - cursor: 'pointer', - overflow: 'hidden', - padding: '0.25em 0.1em', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - '&:hover': { - background: autoColor(theme, '#e8e8e8'), - }, - }), -}); - -type AccordianTextProps = { - className?: string | TNil; - headerClassName?: string | TNil; - data: string[]; - highContrast?: boolean; - interactive?: boolean; - isOpen: boolean; - label: React.ReactNode | string; - onToggle?: null | (() => void); - TextComponent?: React.ElementType<{ data: string[] }>; -}; - -function DefaultTextComponent({ data }: { data: string[] }) { - return ; -} - -export default function AccordianText({ - className = null, - data, - headerClassName, - highContrast = false, - interactive = true, - isOpen, - label, - onToggle = null, - TextComponent = DefaultTextComponent, -}: AccordianTextProps) { - const isEmpty = !Array.isArray(data) || !data.length; - const accordianKeyValuesStyles = useStyles2(getAccordianKeyValuesStyles); - const iconCls = cx(alignIcon, { [accordianKeyValuesStyles.emptyIcon]: isEmpty }); - let arrow: React.ReactNode | null = null; - let headerProps: {} | null = null; - if (interactive) { - arrow = isOpen ? ( - - ) : ( - - ); - headerProps = { - 'aria-checked': isOpen, - onClick: isEmpty ? null : onToggle, - role: 'switch', - }; - } - const styles = useStyles2(getStyles); - return ( -
    -
    - {arrow} - {label} ({data.length}) -
    - {isOpen && } -
    - ); -} diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx index 83632c9351d..a6c79b2d2cf 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx @@ -47,7 +47,7 @@ export const getStyles = (theme: GrafanaTheme2) => { row: css({ label: 'row', '& > td': { - padding: '0.5rem 0.5rem', + padding: '0 0.5rem', height: '30px', }, '&:nth-child(2n) > td': { @@ -68,7 +68,6 @@ export const getStyles = (theme: GrafanaTheme2) => { color: autoColor(theme, '#888'), whiteSpace: 'pre', width: '125px', - verticalAlign: 'top', }), copyColumn: css({ label: 'copyColumn', @@ -118,19 +117,32 @@ export const LinkValue = ({ link, children }: PropsWithChildren) export type KeyValuesTableProps = { data: TraceKeyValuePair[]; linksGetter?: ((pairs: TraceKeyValuePair[], index: number) => KeyValuesTableLink[]) | TNil; + onlyValues?: boolean; }; export default function KeyValuesTable(props: KeyValuesTableProps) { - const { data, linksGetter } = props; + const { data, linksGetter, onlyValues } = props; const styles = useStyles2(getStyles); return (
    {data.map((row, i) => { - const markup = { - __html: jsonMarkup(parseIfComplexJson(row.value)), - }; + let markup = { __html: '' }; + if (row.type === 'code') { + markup = { + __html: `
    ${row.value}
    `, + }; + } else if (row.type === 'text') { + markup = { + __html: `${row.value}`, + }; + } else { + markup = { + __html: jsonMarkup(parseIfComplexJson(row.value)), + }; + } + const jsonTable =
    ; const links = linksGetter?.(data, i); let valueMarkup; @@ -147,15 +159,17 @@ export default function KeyValuesTable(props: KeyValuesTableProps) { return ( // `i` is necessary in the key because row.key can repeat
    - + {!onlyValues && ( + + )} diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.tsx index f59b1fb4421..29ee78fb866 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.tsx @@ -15,9 +15,10 @@ import { css } from '@emotion/css'; import cx from 'classnames'; +import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; -const getStyles = () => ({ +const getStyles = (theme: GrafanaTheme2) => ({ TextList: css({ maxHeight: '450px', overflow: 'auto', @@ -32,7 +33,7 @@ const getStyles = () => ({ padding: '0.25rem 0.5rem', verticalAlign: 'top', '&:nth-child(2n)': { - background: '#f5f5f5', + background: theme.colors.background.secondary, }, }), }); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index 67a6043f397..627e8fc2ec4 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -29,11 +29,11 @@ import { PluginExtensionPoints, IconName, } from '@grafana/data'; -import { Trans, t } from '@grafana/i18n'; +import { t } from '@grafana/i18n'; import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; import { usePluginLinks } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; -import { TextArea, useStyles2 } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; import { autoColor } from '../../Theme'; @@ -46,7 +46,6 @@ import { formatDuration } from '../utils'; import AccordianKeyValues from './AccordianKeyValues'; import AccordianLogs from './AccordianLogs'; import AccordianReferences from './AccordianReferences'; -import AccordianText from './AccordianText'; import DetailState from './DetailState'; import { ShareSpanButton } from './ShareSpanButton'; import { getSpanDetailLinkButtons } from './SpanDetailLinkButtons'; @@ -99,6 +98,9 @@ const getStyles = (theme: GrafanaTheme2) => { gap: '0 1rem', marginBottom: '0.25rem', }), + content: css({ + fontSize: theme.typography.bodySmall.fontSize, + }), listWrapper: css({ overflow: 'hidden', flexGrow: 1, @@ -340,7 +342,7 @@ export default function SpanDetail(props: SpanDetailProps) {
    {linksComponent}
    -
    +
    )} + {warnings && warnings.length > 0 && ( - - Warnings - - } - data={warnings} + ({ + key: '', + value: warning, + type: 'text', + }))} + showSummary={false} + showCountBadge={true} isOpen={isWarningsOpen} + onlyValues={true} onToggle={() => warningsToggle(spanID)} + label={t('explore.span-detail.warnings', 'Warnings')} /> )} + {stackTraces?.length ? ( - ({ + key: '', + value: stackTrace, + type: 'code', + }))} + onlyValues={true} + showSummary={false} + showCountBadge={true} isOpen={isStackTracesOpen} - TextComponent={(textComponentProps) => { - let text; - if (textComponentProps.data?.length > 1) { - text = textComponentProps.data - .map((stackTrace, index) => `StackTrace ${index + 1}:\n${stackTrace}`) - .join('\n'); - } else { - text = textComponentProps.data?.[0]; - } - return ( -
    - {row.key} - + {row.key} + {valueMarkup}